Skip to content

Suggestion: Add the nameof compile-time operator to convert property and function names into strings #1579

Description

@Taytay

I would like to see the nameof operator be considered for Typescript.

This feature was just added to C# description, and it is an elegant solution to a common issue in Javascript.

At compile time, nameof converts its parameter (if valid), into a string. It makes it much easier to reason about "magic strings" that need to match variable or property names across refactors, prevent spelling mistakes, and other type-safe features.

To quote from the linked C# article:

Using ordinary string literals for this purpose is simple, but error prone. You may spell it wrong, or a refactoring may leave it stale. nameof expressions are essentially a fancy kind of string literal where the compiler checks that you have something of the given name, and Visual Studio knows what it refers to, so navigation and refactoring will work:

(if x == null) throw new ArgumentNullException(nameof(x));

To show another example, imagine you have this Person class:

class Person {
    firstName: string
    lastName: string
}
var instance : Person = new Person();

If I have an API that requires me to specify a property name by string (pretty common in JS), I am forced to do something like this:

   someFunction(personInstance, "firstName");

But if I misspell firstName, I'll get a runtime error.
So this is the type-safe equivalent:

   someFunction(personInstance, nameof(Person.firstName));

Activity

  1. NoelAbrahams commented on Dec 31, 2014

    @NoelAbrahams

    See also #394 and #1003.

    A fix for the magic string problem is a common requirement on the forums. Hoping to see an official proposal for this soon.

  2. danquirk commented on Jan 6, 2015

    @danquirk
    Member

    Yeah, closing this as a dupe, suffice to say we definitely get the pain people feel here.

  3. Bobris commented on Jul 27, 2015

    @Bobris

    I don't see this as dupe of any mentioned issues. This could be critical to support advanced minification together with metaprogramming. Currently there is no way how to get member string after minification.
    Though I don't have a clue how to add it to language without conflicting with other features...

    In C# this works other way - you get unminified string. In Typescript I would need minified string. So maybe this could be actually different proposal :-) As unminified would be good too. Just some thoughs...

  4. frodegil commented on Jul 28, 2015

    @frodegil

    Agree. Doesnt look like a duplicate. A nameof operator should be considered as a very good idea. Probably "easy" to implement too.
    I use typescript with angularjs and I like to create static string constants inside my controller/directive classes that I use when I register these classes in an angular module. Instead of hardcoding these names I would like to use nameof(MyDirectiveClass) instead.
    Its the same kind of problem that we experienced earlier with PropertyChanged events in WPF (before nameof/CallerMemberName) when you renamed stuff.

  5. RyanCavanaugh commented on Jul 28, 2015

    @RyanCavanaugh
    Member

    Good point -- the other issues don't quite cover what this is talking about

  6. bryanerayner commented on Aug 19, 2015

    @bryanerayner

    👍

    I'm using Typescript in a project at work, and am putting in a run-time duck type checker. The use case is de-serializing URL parameters.

    If this feature was in the language, this is how I would expect the syntax to work:

    class Foo {
        prop: string;
    }
    
    function isFoo(obj: Foo): boolean;
    function isFoo(obj: any) {
        return (typeof obj === 'object') && 
                  (obj !== null) && 
                  (nameof(Foo.prop) in obj);
    }
    
    // Output for isFoo()
    function isFoo(obj: any) {
        return (typeof obj === 'object') && 
                  (obj !== null) && 
                  ('prop' in obj);
    }
    

    Is that a correct assumption?

  7. bryanerayner commented on Aug 19, 2015

    @bryanerayner

    Frode Gilberg (@frodegil) , that would be an awesome use case. Would reduce a lot of code-smell and repetitive copy/pasting from my daily work-flow.

    Boris Letocha (@Bobris), one would hope that the minified strings for the purposes of my example, I think if minification were to make its way into the tsc, this would be desired behavior.

  8. frodegil commented on Aug 19, 2015

    @frodegil

    Bryan Rayner (@bryanerayner) , your assumption is correct. The nameof operator is only used during compilation to convert type information into a string.

    module pd {
       export class MyAngularControllerClass {
          public static IID : string = nameof(MyAngularControllerClass);  // "MyAngularControllerClass"
          public static $inject: string[] = [Model1.IID, Model2.IID, "$scope"];
          constructor(model1: Model1, model2:Model2, $scope:angular.IScope) {
          }
          public get nameOfThisGetter() : string {
             return nameof(this.nameOfThisGetter);    // "nameOfThisGetter";
          }
       }
       angular.module(nameof(pd)).controller(MyAngularControllerClass.IID, MyAngularControllerClass);
       // angular.module("myapp").controller("OldName", MyAngularControllerClass); < out of sync
    }

    Its so easy to get these hardcoded strings out-of-sync during refactoring

  9. alan-compeat commented on Aug 25, 2015

    @alan-compeat

    This would be fantastic.

  10. 166 remaining items

  11. goodmind commented on Apr 9, 2019

    @goodmind

    decorators is not good example to follow

  12. RyanCavanaugh commented on Apr 9, 2019

    @RyanCavanaugh
    Member

    On the other hand, nameof isn't a reserved word in JS, so the chances of TC39 adding it as an operator is quite low.

    Unary prefix operators don't need to be reserved words because op expr is already an invalid expression for any op that isn't an operator. For example, await was not previously a reserved word, but there wasn't any problem adding it to the language because it only ever appears in an unambiguous unary operator position.

  13. dragomirtitian commented on Apr 9, 2019

    @dragomirtitian
    Contributor

    Might I also point out ew do have keyof which is actually much more powerful than nameof, the result of nameof is just a string, keyof lets us constrain input much better.

    While not as pretty as the nameof operator, we can use keyof to do something similar enough using a function IMO:

    function nameof<T>(k: keyof T) : keyof T {
        return k
    }
    class Person {
        static defaultName: string;
        name!: string;
        studies!: {
            highSchool: string
            unversity: string
        }
    }
    let foo : { bar: { baz : { x: number }}}
    let k1 = nameof<Person>("name")
    let k2 = nameof<Person['studies']>("unversity")
    let k3 = nameof<typeof Person>("defaultName")
    let k4 = nameof<typeof foo['bar']['baz']>("x")

    For the simple case it looks decent, for nested paths, it does look a bit wonky, and you can't use it on privates.

  14. fredgalvao commented on Apr 9, 2019

    @fredgalvao

    Titian Cernicova-Dragomir (@dragomirtitian) even though that acomplishes the {access members by name in a type safe approach} issue (which is awesome), that won't make the compiler automatically help developers on refactorings, nor would it help the IDE look for references of a member without doing text search.

  15. markusmauch commented on Apr 9, 2019

    @markusmauch

    andretshurotshka (@goodmind) I really don't see your point. I agree that decorators turned out to be quite different than originally anticipated but that's what 'experimental' means. It means 'use it but be aware that it might change'. Anyway, I'm glad I can use them now and when they become part of the JavaScript language I will either change my code accordingly or keep the compiler switch in place. The same could be done with nameof. Decorator support was added to win Googe over. But obviously the needs of the community are not equally important.

  16. dsherret commented on Apr 9, 2019

    @dsherret
    Contributor

    Titian Cernicova-Dragomir (@dragomirtitian) it's been talked about in the collapsed posts within this issue.

    To summarize, keyof helps in the scenarios you mentioned, but still doesn't get some common use cases where class or interface/type identifier names would be nice to keep synced up.

    Ex. when using dependency injection frameworks (@inject(nameof(Something))), throwing nice argument error messages...

    function add(a: number, b: number) {
        throwArgumentErrorIfNaN(a, nameof(a));
        throwArgumentErrorIfNaN(b, nameof(b));
    
        return a + b;
    }

    ..., in test description names...

    import { CustomCollection } from "some-library";
    
    describe(nameof(CustomCollection), () => {
        describe(nameof<CustomCollection>(c => c.add), () => {
        });
    });

    ..., when writing log statements (logger.log(nameof(ClassName), LogLevel.Info, "Some log message.")), and probably more.

    There definitely is some benefit to adding nameof, but in my opinion it shouldn't be done by TypeScript as this suggestion doesn't meet this guideline:

    • This could be implemented without emitting different JS based on the types of the expressions.

    Edit: My bad, was thinking of this non-goal:

    • Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

    Also, I don't see what motivation TC39 would have for adding this to JS as JS developers wouldn't benefit from this because writing nameof(Identifier) is only a little bit better than writing "Identifier" in JS due to no compile time errors when using an incorrectly named identifier (I guess there could be runtime ones, but that's not as nice). Also, TC39 wouldn't include support for nameof for identifiers in the type domain (such as interface and type alias names).

    Its place is probably for this to be a compiler plugin (shameless plug) that people can choose to include in their projects.

  17. jpierson commented on Apr 14, 2019

    @jpierson

    With regarding whether this type of feature is the a good fit for TypeScirpt based on the guideline mentioned by David Sherret (@dsherret) I wonder if there is a more library centric way that could be used by linters to pick up stringly typed code that encodes the intention more clearly to access the name of a variable or property. I tried playing around quickly with a concept using ES6 tagged template literals but unfortunately it does appear that one can access the original template string including the template parameter syntax which would have been a nice way to both provide runtime checking as well as allowing simple one liner cases for variables. The basic idea works reasonably for simple one level deep properties though and the syntax is arguably a bit better than a simple function call with parameters passed in.

    const props = {
      name : "Little John",
      slogan: "Rolls are rolls and tools are tools."
    }
    
    console.log(nameof`${props}.slogan`) // slogan
    console.log(nameof`${props}.foo`) // ERROR: Property 'foo' not found on object 

    Ultimately what I think we really want is a somewhat more common way to express the name symbol name resolution that could be caught easier by some compiler/transpiler, linter, or at least runtime in that preferred order to reduce bugs and to provide for more resilient code needs to have these names available in some form at run-time.

  18. JLWalsh commented on Apr 18, 2019

    @JLWalsh

    There definitely is some benefit to adding nameof, but in my opinion it shouldn't be done by TypeScript as this suggestion doesn't meet this guideline:

    • This could be implemented without emitting different JS based on the types of the expressions.

    Could you clarify as to what different JS means in this context? For example, if nameof<InterfaceA> and nameof<InterfaceB> both output a string that have different values but share the same type, is that considered different?

  19. dsherret commented on Apr 18, 2019

    @dsherret
    Contributor

    James L. Walsh (@JLWalsh) I think I might have misread that as not emitting different JS based on the kinds of expressions (I was half going from memory when I copied and pasted it). So taking a call expression and emitting a string literal. I'm not sure now exactly what that point means now, but my brain is fried after programming for way too long today.

    I was thinking of this non-goal:

    • Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.
  20. JoshZA commented on May 22, 2019

    @JoshZA

    👍 Please add this

  21. snys98 commented on Jun 24, 2019

    @snys98

    James (@rjamesnw)
    In case of numbers in expression, I did some modification:

    function nameof(selector: () => any, fullname = false) {
        var s = '' + selector;
        var m = s.match(/return\s+([A-Z0-9$_.]+)/i)
            || s.match(/.*?(?:=>|function.*?{)\s*([A-Z0-9$_.]+)/i);
        var name = m && m[1] || "";
        return fullname ? name : name.split('.').reverse()[0];
    }
    
  22. locked and limited conversation to collaborators on Jun 25, 2019
  23. RyanCavanaugh commented on Jun 25, 2019

    @RyanCavanaugh
    Member

    Locking Waiting for TC39 threads as policy since there's not really anything to talk about except complaining that TC39 hasn't done it yet 🙃

  24. RyanCavanaugh commented on Jan 25, 2024

    @RyanCavanaugh
    Member

    Closing since there's no action available on our side. Will reopen if/when something happens at committee.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    SuggestionAn idea for TypeScriptWaiting for TC39Unactionable until TC39 reaches some conclusion

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions