nameof keyword in Typescript

TypescriptTypes

Typescript Problem Overview


As I have seen, there is no native nameof-keyword like C# has built into TypeScript . However, for the same reasons this exists in C#, I want to be able to refer to property names in a type safe manner.

This is especially useful in TypeScript when using jQuery plugins (Bootstrap-Tagsinput) or other libraries where the name of a property needs to be configured.

It could look like:

const name: string = nameof(Console.log);
// 'name' is now equal to "log"

The assignment of name should change too when Console.log got refactored and renamed.

What is the closest possible way of using such a feature in TypeScript as of now?

Typescript Solutions


Solution 1 - Typescript

As you have already said, there is no built in functionality on TypeScript as of version 2.8. However, there are ways to get the same result:

Option 1: Using a library

ts-nameof is a library that provides the exact functionality as C# does. With this you can do:

nameof(console); // => "console"
nameof(console.log); // => "log"
nameof<MyInterface>(); // => "MyInterface"
nameof<MyNamespace.MyInnerInterface>(); // => "MyInnerInterface"

ts-simple-nameof offers an alternative. It basically parses a stringified lambda to figure out the property name:

nameof<Comment>(c => c.user); // => "user"
nameof<Comment>(c => c.user.posts); // => "user.posts"

Option 2: Define a helper function

You can easily define your own nameof that adds the type checking, however it will not refactor automatically as you'll still need to type a string literal:

const nameof = <T>(name: keyof T) => name;

It will return the passed property name but will generate a compile time error when the property name does not exist on type T. Use it like so:

interface Person {
    firstName: string;
    lastName: string;
}

const personName1 = nameof<Person>("firstName"); // => "firstName"
const personName2 = nameof<Person>("noName");    // => compile time error

Credits and more information about this

Update on helper function with TypeScript 2.9+

The type keyof T now not only resolves to a string, but to string | number | symbol (ref). If you still want to resolve strings only, use this implementation instead:

const nameof = <T>(name: Extract<keyof T, string>): string => name;

Solution 2 - Typescript

I think we often need more: to get class property names at runtime with compile-time validation. Renaming property will change nameOf expression. This is a really useful feature:

export type valueOf<T> = T[keyof T];
export function nameOf<T, V extends T[keyof T]>(f: (x: T) => V): valueOf<{ [K in keyof T]: T[K] extends V ? K : never }>;
export function nameOf(f: (x: any) => any): keyof any {
	var p = new Proxy({}, {
		get: (target, key) => key
	})
	return f(p);
}

Usage example (no strings!):

if (update.key !== nameOf((_: SomeClass) => _.someProperty)) {
   // ...								
}

Example with existing instance:

export interface I_$<T> {
	nameOf<V extends T[keyof T]>(f: (x: T) => V): valueOf<{ [K in keyof T]: T[K] extends V ? K : never }>;
}

export function _$<T>(obj: T) {
	return {
		nameOf: (f: (x: any) => any) => {
			return nameOf(f);
		}
	} as I_$<T>;
}

Usage:

let obj: SomeClass = ...;
_$(obj).nameOf(x => x.someProperty);
or _$<SomeClass>().nameOf(x => x.someProperty);

resolved to 'someProperty'.

Solution 3 - Typescript

If you only need to access properties as strings, you can use Proxy safely like this:

function fields<T>() {
    return new Proxy(
        {},
        {
            get: function (_target, prop, _receiver) {
                return prop;
            },
        }
    ) as {
        [P in keyof T]: P;
    };
};

interface ResourceRow {
    id: number;
    modified_on_disk: Date;
    local_path: string;
    server_path: string;
}

const f = fields<ResourceRow>();

// In this example I show how to embed field names type-safely to a SQL string:
const sql = `
CREATE TABLE IF NOT EXISTS resource (
    ${f.id}               INTEGER   PRIMARY KEY AUTOINCREMENT NOT NULL,
    ${f.modified_on_disk} DATETIME       NOT NULL,
    ${f.local_path}       VARCHAR (2048) NOT NULL UNIQUE,
    ${f.server_path}      VARCHAR (2048) NOT NULL UNIQUE
);
`;

Solution 4 - Typescript

Recommend: Don't use "ts-nameof" package

> I now recommend not using this package or any other compiler transforms. It's neat, but it creates code that is not portable and makes it hard to switch to new build systems. The current solutions for injecting compiler transforms are hacky and I can't imagine the TS compiler ever supporting this out of the box.

/* eslint-disable no-redeclare, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any */
export function nameof<TObject>(obj: TObject, key: keyof TObject): string;
export function nameof<TObject>(key: keyof TObject): string;
export function nameof(key1: any, key2?: any): any {
  return key2 ?? key1;
}
/* eslint-enable */

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBruno ZellView Question on Stackoverflow
Solution 1 - TypescriptBruno ZellView Answer on Stackoverflow
Solution 2 - TypescriptSalientBrainView Answer on Stackoverflow
Solution 3 - TypescriptCianticView Answer on Stackoverflow
Solution 4 - TypescriptMasih JahangiriView Answer on Stackoverflow