nameof keyword in Typescript
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?
Solution 1:
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:
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:
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
);
`;