Reputation: 8561
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?
Upvotes: 137
Views: 80011
Reputation: 17196
nameof
for propertiesI'm surprised to see three different Proxy-based answers that all allocate multiple proxies. There's no need for that performance penalty.
Simply use this:
/** nameIn<Foo>().xyz simply returns 'xyz', but lets the TypeScript compiler
* 1. check that 'xyz' is a property of Foo
* 2. rename the reference to `xyz` when Foo.xyz is renamed */
export function nameIn<T>() {
return _name_proxy as unknown as { [P in keyof T]: P };
}
const _name_proxy = new Proxy({}, { get(target, key) { return key; } });
I call it nameIn<T>
rather than nameOf<T>
because it doesn't return T
itself as a string. Example usage:
// renaming `six` (F2 in VS Code) will rename references after `nameIn`
class Class { six = 6; };
// TypeScript knows the type is "six" (even without the `: "six"`)
const nameOfIt: "six" = nameIn<Class>().six;
Arguably this is too permissive, e.g. the compiler accepts
type Anything = { [k:string]: any };
const fooName = nameIn<Anything>().foo; // inferred type: string
Here's a version that won't permit the things like that:
export function nameIn<T>() {
return _name_proxy as unknown as NamedPropsOf<{ [P in keyof T]: P }>;
}
const _name_proxy = new Proxy({}, { get(target, key) { return key; } });
// Based on "RemoveIndexSignature" from https://stackoverflow.com/a/77814151/22820
/** Keeps named properties of T, removing index props like `[k:string]: T` */
export type NamedPropsOf<T, P=PropertyKey> = {
[K in keyof T as (P extends K ? never : (K extends P ? K : never))]: T[K]
};
nameof
for classes and functionsThe top answer, where nameof(console)
becomes "console"
, requires some kind of compile-time transform. However, there is a built-in JavaScript feature that works like nameof
for classes and functions:
export function Bar() {}
const barName = Bar.name;
// BTW some alternatives to this don't work if the constructor is non-public
export class Foo { private constructor() { } }
const fooName = Foo.name;
Unfortunately the type of .name
is string
rather than the actual name, but you still get the basic benefit of being able to rename the class or function without breaking your program.
Footnote: .name
gives you the name of a function, not the name of the variable that holds the function, but JavaScript has some magic to assign names on const/let/var assignments (tested in Chrome and Firefox):
// square1.name is 'square1'
const square1 = x => x*x;
// square2.name is 'square2'
const square2 = function (x) { return x*x; };
// square3.name is 'Sqr'
const square3 = function Sqr (x) { return x*x };
// square4.name is 'square2'
const square4 = square2;
Footnote: I tried this workaround, but TypeScript always infers S
as string
:
export function nameOf<T extends {name:S}, S extends string>(funcOrClass: T): S {
return funcOrClass.name;
}
const foo = nameOf(Foo); // foo: string
const bar = nameOf(Bar); // bar: string
// And even this doesn't work!
const baz = nameOf({ name: 'baz' as const }); // baz: string
Upvotes: 4
Reputation: 91
Another concise solution could be to use Proxy
export default function nameOf<T extends object>(nameExtractor: (obj: T) => any): keyof T {
const proxy = new Proxy({} as T, {
get(target, prop: string | symbol) {
return prop;
},
});
return nameExtractor(proxy);
}
use it like that
const propName = nameOf<MyObjectType>((obj) => obj.prop);
Upvotes: 3
Reputation: 8561
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:
ts-nameof is a library that provides the exact functionality as C# does (no longer recommended). 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"
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
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;
Upvotes: 158
Reputation: 10957
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 */
Upvotes: 6
Reputation: 2551
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'.
Upvotes: 19
Reputation: 5774
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
);
`;
Upvotes: 14