Bruno Zell
Bruno Zell

Reputation: 8561

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?

Upvotes: 137

Views: 80011

Answers (6)

Qwertie
Qwertie

Reputation: 17196

nameof for properties

I'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 functions

The 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

Eugene Aseyev
Eugene Aseyev

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

Bruno Zell
Bruno Zell

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:

Option 1: Using a library

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"

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;

Upvotes: 158

Masih Jahangiri
Masih Jahangiri

Reputation: 10957

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 */

Upvotes: 6

SalientBrain
SalientBrain

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

Ciantic
Ciantic

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

Related Questions