Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Taytay opened this issue Dec 30, 2014 · 175 comments
Labels
Suggestion An idea for TypeScript Waiting for TC39 Unactionable until TC39 reaches some conclusion

Comments

@Taytay
Copy link

Taytay commented Dec 30, 2014

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));
@NoelAbrahams
Copy link

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.

@danquirk
Copy link
Member

danquirk commented Jan 6, 2015

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

@danquirk danquirk closed this as completed Jan 6, 2015
@danquirk danquirk added the Duplicate An existing issue was already created label Jan 6, 2015
@Bobris
Copy link

Bobris commented Jul 27, 2015

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...

@frodegil
Copy link

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.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus and removed Duplicate An existing issue was already created labels Jul 28, 2015
@RyanCavanaugh
Copy link
Member

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

@RyanCavanaugh RyanCavanaugh reopened this Jul 28, 2015
@bryanerayner
Copy link

👍

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?

@bryanerayner
Copy link

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

@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.

@frodegil
Copy link

@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

@alan-compeat
Copy link

This would be fantastic.

@fredgalvao
Copy link

Fantastic indeed. So much could be done with such an operator.

@RyanCavanaugh RyanCavanaugh added Revisit An issue worth coming back to and removed In Discussion Not yet reached consensus labels Oct 5, 2015
@RyanCavanaugh
Copy link
Member

We want to see how things 'feel' once #394 and #1003 land. It's possible we can solve these use cases in the type system without having to resort to adding new expression-level syntax.

In the meantime it'd be good to collect any use cases that wouldn't be adequately addressed by those proposals.

@fredgalvao
Copy link

Well, the case partially described by @frodegil about managing components that are registered somehow with a name (that most of the times is the same name of the Function/Class that is registered), as is the case with Angular, is a case that, as far as I can see, cannot be handled by either #394 's memberof nor by string literal types from #1003 (I'm really looking forward to the later though!).

I agree and understand how adding a new sintax element is undesired and even goes against typescript's main goal (to be a thin superset of javascript), but I still consider this proposal a very nice one.

Another small case

I use pouchdb and I make my own doc IDs, for performance and consistency reasons. These docIds are generated using a few fields, and one of them is based on the name of the model being saved, like:

For now I need to maintain a property called className on all my models:

class Person {
  className = 'Person';

  _id: string;
  $_dateCreated: number;
}

var p:Person;
p._id = `${this.className}_${window.appMetadata.cordovaUuid}__${this.$_dateCreated}__${window.uuid.v4()}`
pouchdb.put(p)

which could be turned into:

class Person {
  _id: string;
  $_dateCreated: number;
}

var p:Person;
p._id = `${nameof Person}_${window.appMetadata.cordovaUuid}__${this.$_dateCreated}__${window.uuid.v4()}`
pouchdb.put(p)

Foreseeable issues

I can see it becoming a bit harder to figure out the nameof of some things when dealing with generics or inheritance, so I will agree that this feature needs a lot of thought.

Thin alternatives

Considering that all cases that deal with property names can be handled completely by either #394 or #1003, I'd say the exposure of a getClass() and getClassName() (see example here) would solve the remaining cases I know of without needing to create new sintax elements. It could just as well be a core decorator for example.

@federico-ardila
Copy link

A use case where neither #394 , #1003 ,getClass() nor GetClassName() would help would be this in Angular2:

    @Component({
        selector: 'my-selector',
        template: `<h1>{{${nameof MyComponent.prototype.title}}}</h1>`
    })
    class MyComponent { 
        public title = "Hellow world";
    }

It would makes the code refactoring proof, but it also makes it a lot less readable, so I'm not sure if is such a good idea. Just a thought.

@kamranayub
Copy link

Adding suggestion here to keep Enums in mind for nameof, as a shortcut for toString'ing an enum:

nameof(MyModule.Enums.FooEnum.Bar) === "Bar"

would compile to:

MyModule.Enums.FooEnum[MyModule.Enums.FooEnum.Bar] === "Bar"

or just:

"Bar" === "Bar"

@bgever
Copy link

bgever commented Feb 26, 2016

A good use case is for unit tests where SinonJS is used for spying/stubbing. With the proposed nameof keyword, it's possible to pass the names of the methods and properties to be spied or stubbed. When method names are refactored, the unit tests won't break because of a string that needs to be updated.

var obj = new MyClass();
sinon.stub(obj, 'methodName', () => null); // Prone to break.
var obj = new MyClass();
sinon.stub(obj, nameof(MyClass.methodName), () => null);

Or, if the stub would support to pass in a predicate to resolve the name:

var obj = new MyClass();
function stub<T>(targetObject: T, targetMethodPredicate: (T)=>string, methodStub: Function){}
sinon.stub(obj, x => nameof(x.methodName), () => null);

@styfle
Copy link
Contributor

styfle commented Mar 11, 2016

I have a use case for constructing SQL strings. Here is a code snippet:

interface User {
    id: string;
    name: string;
    birthday: Date;
}

// Current implementation
var sql = `SELECT id,name FROM Users`;

// Desired implementation
var sql = `SELECT ${nameof(User.id)},${nameof(User.name)} FROM ${nameof(User)}s`;

The nameof() feature would make this type safe and refactor safe. For example renaming the User table to Account would be a quick refactor rather than searching through the code and missing dynamic strings like this.

I agree this should be a compile time transformation like it is in C# so we can burn in the type name.

@jods4
Copy link

jods4 commented Mar 22, 2016

@RyanCavanaugh
Five months later:
#1003 has landed but doesn't help in many scenarios (see below).
#394 is closed in favour of #1295. The latter seems nowhere near landing and while it solves an interesting part of the problem (typing) it doesn't help with the string/nameof part.

Maybe time to revisit?

Strings are bad because you can't "Find All References" them, you can't "Refactor/Rename" and they are prone to typos.

Some examples, using Aurelia:

let p: Person;

// Observe a property for changes
let bindingEngine: BindingEngine;
bindingEngine.observeProperty(p, 'firstName')
             .subscribe((newValue: string) => ...);

class Person {
  firstName: string;

  // Declare a function to call when value changes
  @bindable({ changeHandler: 'nameChanged'})
  lastName: string;

  nameChanged(newValue: string, oldValue: string) { ... }

  // Declare computed property dependencies
  @computedFrom('firstName', 'lastName')
  get fullName() {
    return `${firstName} ${lastName}`;
  }
}

// Declare dependencies for validation
validation.on(p)
  .ensure('firstName').isNotEmpty()
  .ensure('fullName', config => config.computedFrom(['firstName', 'lastName'])).isNotEmpty();

There you have five different APIs that would all benefit from having a nameof operator.

@RyanCavanaugh RyanCavanaugh added the In Discussion Not yet reached consensus label Mar 22, 2016
@rayncc
Copy link

rayncc commented Aug 27, 2018

@Injectable()
class Ninja {
constructor(@Inject(nameof(Weapon)) weapon: Weapon) { }
}

Exactly what I want

@ffMathy
Copy link

ffMathy commented Sep 6, 2018

@rayncc no need to use nameof here if you use a framework that relies on decorators, such as TypeScript IOC, Angular 2+, or @fluffy-spoon/inverse.

@JLWalsh
Copy link

JLWalsh commented Feb 8, 2019

@ffMathy

I disagree, nameof would be especially useful for injecting interfaces, which only exist in name, but not as an actual JS object. Which is why you can't do @Inject(MyInterface), but instead you have to do @Inject('MyInterface'). If nameof was atleast available for interfaces, we could do @Inject(nameof MyInterface), which would be much cleaner than a string (which is prone for typos), and more importantly, would work well with refactors.

@stasberkov
Copy link

С# has nameof why not add alike feature in TypeScript? It will be very helpful. If will improve code robustness. I.e. instead of if (protoPropertyIsSet(msg, "expiration_utc_time")) we could write
if (protoPropertyIsSet(msg, nameof(msg.expiration_utc_time))).

@dragomirtitian
Copy link
Contributor

@stasberkov C# is in full control of their syntax, Typescript has to tip-toe around JS and any future JS developments. Except for enums, type annotations and type assertions are the major extensions to expression level syntax. Any new expression level syntax has the danger of diverging from JS and making TS incompatible at some level with JS. This is why adding nameof is not an easy decision. Implementing it is trivial, but the long term implications might end up biting back.

@RyanCavanaugh RyanCavanaugh added Waiting for TC39 Unactionable until TC39 reaches some conclusion and removed Declined The issue was declined as something which matches the TypeScript vision Revisit An issue worth coming back to labels Apr 3, 2019
@rayncc
Copy link

rayncc commented Apr 8, 2019

@stasberkov C# is in full control of their syntax, Typescript has to tip-toe around JS and any future JS developments. Except for enums, type annotations and type assertions are the major extensions to expression level syntax. Any new expression level syntax has the danger of diverging from JS and making TS incompatible at some level with JS. This is why adding nameof is not an easy decision. Implementing it is trivial, but the long term implications might end up biting back.

The expression nameof(XXX) will be compiled to a constant string, which I think is totally compatible with JS

@Pauan
Copy link

Pauan commented Apr 8, 2019

@rayncc What he meant is: what if JS later adds in a nameof operator which behaves differently than TypeScript's nameof operator?

Then there would be a conflict, which has to be resolved in some way (and there aren't any easy answers for that).

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. So I think it's pretty safe for TypeScript to add it.

@ghost
Copy link

ghost commented Apr 8, 2019

I don't think that this case is different from generics or typings that can be implemented in JavaScript in a differrent way in the future... TypeScript then should support it while also supporting the TS way of implementing those concepts.

@markusmauch
Copy link

The are so many use cases for this, for example NativeScript's notifyPropertyChange( nameof( property) ) method. BTW, they have done this before with the decorator stuff. The solution there was to simply hide it behind an 'experimental' compiler flag.

@goodmind
Copy link

goodmind commented Apr 9, 2019

@markusmauch lol, look how this ended up for them

@goodmind
Copy link

goodmind commented Apr 9, 2019

decorators is not good example to follow

@RyanCavanaugh
Copy link
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.

@dragomirtitian
Copy link
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.

@fredgalvao
Copy link

@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.

@markusmauch
Copy link

markusmauch commented Apr 9, 2019

@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.

@dsherret
Copy link
Contributor

dsherret commented Apr 9, 2019

@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.

@jpierson
Copy link

With regarding whether this type of feature is the a good fit for TypeScirpt based on the guideline mentioned by @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.

@JLWalsh
Copy link

JLWalsh commented Apr 18, 2019

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?

@dsherret
Copy link
Contributor

@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.

@JoshZA
Copy link

JoshZA commented May 22, 2019

👍 Please add this

@snys98
Copy link

snys98 commented Jun 24, 2019

@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];
}

@microsoft microsoft locked and limited conversation to collaborators Jun 25, 2019
@RyanCavanaugh
Copy link
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 🙃

@RyanCavanaugh
Copy link
Member

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Suggestion An idea for TypeScript Waiting for TC39 Unactionable until TC39 reaches some conclusion
Projects
None yet
Development

No branches or pull requests