microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.14k stars 12.3k forks source link

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

Closed Taytay closed 5 months ago

Taytay commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

Bobris commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

bryanerayner commented 8 years ago

:thumbsup:

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 commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

This would be fantastic.

fredgalvao commented 8 years ago

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

RyanCavanaugh commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

Valid points. I'll bring it up again (note: we're very busy right now with some upcoming work so suggestion triage is backlogged a bit)

jods4 commented 8 years ago

Sorry I "Ctrl-Entered" my post too soon. I have added several real-life code examples that could benefit.

To make matters more complicated, some APIs above accept paths as well. So @computedFrom('p.address.city') is valid and it would be nice to support it as well...

jods4 commented 8 years ago

In this comment in another issue I proposed some kind of limited Expressions from C#.

They would be a better alternative than nameof for most of the examples in this thread:

Drawback of the last point is that Expressions would not cover cases such as:

function f(x: number) {
  let s = nameof(x);
}

But that's not as common as the other use-cases.

I am copying the description of the idea from the other issue:


Assume we have a special kind of strings to denote expressions. Let's call it type Expr<T, U> = string. Where T is the starting object type and U is the result type.

Assume we could create an instance of Expr<T,U> by using a lambda that takes one parameter of type T and performs a member access on it. For example: person => person.address.city. When this happens, the whole lambda is compiled to a string containing whatever access on the parameter was, in this case: "address.city".

You can use a plain string instead, which would be seen as Expr<any, any>.

Having this special Expr type in the language enables stuff like that:

function pluck<T, U>(array: T[], prop: Expr<T, U>): U[];

let numbers = pluck([{x: 1}, {x: 2}], p => p.x);  // number[]
// compiles to:
// let numbers = pluck([..], "x");

This is basically a limited form of what Expressions are used in C# for.

IanYates commented 8 years ago

https://github.com/basarat/typescript-book/issues/33 at least has some workarounds (although I just ran into an issue where there was no trailing ';' after my return due to ASP.Net's simple minification)

Big :+1: :100: from me for nameof support.

I'm using the workarounds for the reasons many others above have given - I want something refactor-friendly. APIs that accept strings to identify the name of some property on a Javascript object are plentiful.

malibuzios commented 8 years ago

Here's a real-world example where nameof could have been very useful.

I have a class that serves as a 'controller' to functionality that is executed remotely in a worker. The structure of the controller class mirrors exactly the actual implementation class, which is executed within the worker.

I need to safely reference the name of each method in the controller in a string, which would then be stored within the request message sent to the worker. However, the current best solution is not very type-safe:

export class LocalDBWorkerController<V> extends LocalDBBase<V> {
        initializeWorkerDB(options: BrowserDBOptions): Promise<void> {
            return this.executeInWorker("initializeWorkerDB", [options]);
        }

        getEntry(key: string): Promise<DBEntry<V>> {
            return this.executeInWorker("getEntry", [key]);
        }

        getEntries(keys: string[]): Promise<EntryArray<V>> {
            return this.executeInWorker("getEntries", [keys]);
        }

        getAllEntries(): Promise<EntryArray<V>> {
            return this.executeInWorker("getAllEntries");
        }

        // ...
}

If one of the method names is renamed, I need to remember to manually rename its string representation as well, or else I'll get a run-time error (assuming this code is thoroughly tested, otherwise it may be missed).

It would have been great if I could just write:

export class LocalDBWorkerController<V> extends LocalDBBase<V> {
        initializeWorkerDB(options: BrowserDBOptions): Promise<void> {
            return this.executeInWorker(nameof(this.initializeWorkerDB), [options]);
        }

        getEntry(key: string): Promise<DBEntry<V>> {
            return this.executeInWorker(nameof(this.getEntry), [key]);
        }

        getEntries(keys: string[]): Promise<EntryArray<V>> {
            return this.executeInWorker(nameof(this.getEntries), [keys]);
        }

        getAllEntries(): Promise<EntryArray<V>> {
            return this.executeInWorker(nameof(this.getAllEntries));
        }

        // ...
}
roelvanlisdonk commented 8 years ago

Thank you for re-opening this issue, because I would really like to see the nameof operator.

The nameof operator would be a compile time feature, so no impact on performance for the run-time, but we get save refactoring of property names and prevent "magic strings".

RyanCavanaugh commented 8 years ago

Discussed for a while and we think there's a better proposal that solves this use case.

The solution for this general problem space described in #1295 looks to be a better fit for TypeScript. If that proposal were correctly implemented, you'd still get the refactoring and validation benefits because the strings would be contextually typed and identified as property names (so rename / find references / go to def / etc would still work).

1295 also provides better typechecking (because you couldn't nameof the wrong thing) while avoiding the compat problem (you may have a function named nameof already!) and the "what happens if ES7+ adds this operator too" problem.

You'd still also be able to have some good expression-level verification, too, because you could write e.g. <memberof MyType>"SomeMember" (which would be validated!) instead of nameof myTypeInstance.SomeMember. This is even more advantageous because now you don't need an instance of MyType to get its property names, which happens reasonably often.

malibuzios commented 8 years ago
<memberof MyType>"someMember"
  1. Doesn't look very intuitive or friendly to beginners (more abstract, requires understanding of what is a type cast, type modifier).
  2. Longer. Adds additional 'noise' (< >, " ").
  3. Isn't comfortably usable for anonymous types. E.g. let x = { prop1: 123 }; let s = nameof(x.prop1). Unless somewhat verbose syntax is used, E.g. <memberof typeof x>"prop1".
  4. Doesn't suggest a natural extension for nesting. E.g. like nameof(myTypeInstance.someMember.nestedMember).
  5. Doesn't allow for editor auto-completion.
  6. Doesn't provide a target for the rename operation itself (e.g. clicking it and pressing F2).
  7. Less informative error message: e.g. "someMember" is not assignable to... "member1" | "member2" | .. instead of the more accurate 'someMember' is not a member of ....
  8. Doesn't provide a way to exclude private or protected members (unless something like publicMemberof or publicOrProtectedMembersOf is provided, but that may seem a bit too verbose). I consider that very important functionality - this is the kind of safety that programmers need and nameof was designed to provide.

I think #1295 is interesting but seems at most complementary (they could work together nicely)? This seems like a strange decision (especially with the 'Declined' verdict-like tag which I feel is inappropriate here - and in general). I think nameof is a good syntax and concept. There is no need to excessively 'rationalize' or try to justify the decision not to implement it just because it may interfere with future EcmaScript syntax (which seems to be the primary concern here, I believe).

RyanCavanaugh commented 8 years ago

Re: points 1-4 In most cases, you're just write "someMember" and everything would just work. I'd only use the <...>" " syntax when the target didn't provide a memberof type, which is going to be somewhat rare.

Point 5 is correct.

Point 6 is not correct; you'd be able to rename in that position.

Point 7 is not correct when the target is memberof -- we'd be able to provide a good error message there, I think.

Point 8, I agree we'd need to figure that out.

especially with the 'Declined' verdict-like tag which I feel is inappropriate here - and in general

I'm not sure what the alternative is?

malibuzios commented 8 years ago
class Example {
   a: number
   protected b: string: 
   private c: boolean;
}

function getProp(obj: Example, prop: memberof Example) {
  //..
}

For 1-4 (in relation to 8) there is a limitation here, compared to the expected behavior with nameof. The limitation is that with nameof, depending on the context of the call, there would be a different expectation of which members to include as valid. If referenced from outside the class, memberof would be expected to only include "a", from a derived class it would be "a" | "b" and from the inside it would be "a" | "b" | "c". I don't think there is any way to elegantly 'model' that here, as a type is not usually contextually determined (though everything is possible? I guess, but I haven't had the time to really consider the consequences).

For 6. renaming through a string would appear a bit strange. I suppose it could be done but since it is not in a visible context (E.g. someInstance.member) it may not guard as well against human mistakes.


The Edge platform status page uses the terminology:

Based on that:

I think some of these alternatives also better reflect the fact that this is not really a 'community' directed project (say like Node.js). Terms like 'In Discussion' may seem like this may be in discussion the 'community' rather than the design team (at least I thought that way at first). 'Declined' seems a bit extreme (never say never..). To me it feels like it characterizes the feature requests a bit like they were 'demands' or even 'trials'. Sometimes they are just 'ideas', thoughts, or rudimentary expressions of needs, points for investigation etc. Sometimes they may only serve as 'stages' or sources of information to better ones..

I'm willing to put some personal effort to work on these. Maybe I'll open a meta-issue when I'll have a more complete set of terms that I'd feel is good enough.

RyanCavanaugh commented 8 years ago

That's good feedback and it looks like we do have room for improvement here. I'm not a huge fan of "Unplanned" (it sounds like we neglected to plan for it?), but I think the list you have there is nice. I'd welcome the new list if you do come up with something that's less ambiguous and more friendly than what we have now.

malibuzios commented 8 years ago

@RyanCavanaugh

Yes, I realize "unplanned" could be also read as "unscheduled", so I'll need more time to think of something better. Anyway, I have more ideas (like perhaps splitting "By Design" to "Intended Behavior" and maybe something like "Design Limitation") but it's a bit off-topic here. I'm planning to take the complete set of tags that are listed in the FAQ, investigate their usage and try my best to find alternatives. I'll open a new issue when I get to a set of terms I feel is reasonable enough.

malibuzios commented 8 years ago

Here's an update for the best workaround for the use case I gave above. My current solution is to polyfill the ES6 name property for function instances, if needed, for all methods in the target object and in all of its prototypes:

(Version 5.0 of V8 doesn't require this polyfill when targeting ES6, I believe (in chrome 50 .name works for class methods) though in Node 6.0 some of the functionality is still behind a flag):

function polyfillMethodNameProperties(obj: any) {
    while (obj != null && obj !== Function.prototype && obj !== Object.prototype) {
        for (let propName of Object.getOwnPropertyNames(obj)) {
            let member = obj[propName];

            // Note: the latest Edge preview would resolve the name to 'prototype.[name]' 
            // so it might be better to force the polyfill in any case even if the 
            // feature is supported by removing '&& !member.name'
            if (typeof member === "function" && !member.name)
                Object.defineProperty(member, "name", {
                    enumerable: false,
                    configurable: true,
                    writable: false,
                    value: propName
                });
        }

        obj = Object.getPrototypeOf(obj);
    }
}

class LocalDBWorkerController<V> extends LocalDBBase<V> {
        constructor() {
            polyfillMethodNameProperties(this);
        }

        initializeWorkerDB(options: BrowserDBOptions): Promise<void> {
            return this.executeInWorker(this.initializeWorkerDB.name, [options]);
        }

        getEntry(key: string): Promise<DBEntry<V>> {
            return this.executeInWorker(this.getEntry.name, [key]);
        }

        getEntries(keys: string[]): Promise<EntryArray<V>> {
            return this.executeInWorker(this.getEntries.name, [keys]);
        }

        getAllEntries(): Promise<EntryArray<V>> {
            return this.executeInWorker(this.getAllEntries.name);
        }

        // ...
}

I'm not aware of a general purpose solution on how to apply this particular polyfill in a non-targeted way, though it successfuly solves the particular need I have.


A different, but more general approach can be implemented to support members having object types as well (but not members having primitive types like string, number, boolean, null or undefined as they cannot be compared by reference), and also allows for the reassignment of members. The approach I used here relies on an ES6 Map object, if available, to cache the list of properties:

let propertyNameCache: Map<any, string[]>;

function getAllPropertyNames(obj: any): string[] {

    let scanAllPropertyNames = (): string[] => {
        let propertyNames: string[] = [];

        while (obj != null) {
            Array.prototype.push.apply(propertyNames, Object.getOwnPropertyNames(obj));
            obj = Object.getPrototypeOf(obj);
        }

        return propertyNames;
    }

    if (typeof Map === "function") {
        if (propertyNameCache === undefined)
            propertyNameCache = new Map<any, string[]>();

        let names = propertyNameCache.get(obj);

        if (names === undefined) {
            names = scanAllPropertyNames();
            propertyNameCache.set(obj, names);
        }

        return names;
    }
    else {
        return scanAllPropertyNames();
    }
}

function memberNameof(container: any, member: any): string {
    if (container == null || (typeof container !== "function" && typeof container !== "object"))
        throw new TypeError("memberNameof only works with non-null object or function containers");

    if (member == null || (typeof member !== "function" && typeof member !== "object"))
        throw new TypeError("memberNameof only works with non-null object or function values");

    for (let propName of getAllPropertyNames(container))
        if (container[propName] === member)
            return propName;

    throw new Error("A member with the given value was not found in the container object or any of its prototypes");
}

Usage example:

class Base {
    dcba = {};
}

class Example extends Base {
    abcd = {};
}

let example = new Example();
console.log(memberNameof(example, example.abcd)); // prints "abcd"
console.log(memberNameof(example, example.dcba)); // prints "dcba"

(Note this would not detect addition of new members, unless the slower, uncached version is used - which rescans the whole prototype chain at every call. However in the realm of TypeScript there isn't really a need for that as the interface or class definition itself must be known at compile-time to provide the desired level of type safety, anyway)

@RyanCavanaugh The work on the alternative label suggestions is 95% done, I have covered almost all the current labels (this took many hours of work) - the most difficult label turned out to be the Declined label itself :) so this may take some additional weeks or even months.

Liero commented 8 years ago

<memberof MyType>"SomeMember"

This is even more advantageous because now you don't need an instance of MyType to get its property names

in C# you can write var propertyName = nameof(MyType.SomeMember)

it seems like both nameof and memberof makes sense. memberof is usefull for type annotation, nameof for getting name of object's members, variables, function parameters, classes, interfaces, namespaces, etc...

vytautas-pranskunas- commented 8 years ago

So i dont get it - will be there any kind of nameof operator or no? Because ticket is closed but comments keeps comming :)

DaveEmmerson commented 7 years ago

I arrived here when I searched for: TypeScript CallerMemberName

Looking for something like C#'s [CallerMemberName] attribute. Not quite the same as nameof, but used in some cases to write a method so that the caller doesn't even have to use nameof; e.g., notifying that some property has changed, etc.

Is there a separate issue for that, or is it considered to be the same as this?

styfle commented 7 years ago

@DaveEmmerson This is a different topic. CallerMemberName can be found at run-time in javascript.

This issue is for a compile-time burn-in of a variable name, which is not available at run-time.

DaveEmmerson commented 7 years ago

@styfle if you read that thread it can't actually be done at runtime consistently.

I'm talking compile time like CallerMemberName in C# and like nameof. I see them as having a similar mechanism, but I guess if this issue has reached its end then that would have a similar conclusion.

Was just a thought... :)

zpdDG4gta8XKpMCd commented 7 years ago

i don't quite get this reluctance either

nameof is merely a macros that transforms a property name identifier into a string literal at the syntax level, why is it such a big deal to add it?

enoshixi commented 7 years ago

I solved this problem with a babel plugin: https://github.com/enoshixi/babel-plugin-nameof

I've been using this in production sites for a few months and it's worked like a charm. Finally managed to get it up on github.

vytautas-pranskunas- commented 7 years ago

Can that babel plugin be installed to TS application and work together?

YipYipX4 commented 7 years ago

My use-case is a forms library I have developed. I would like to be able to use refactoring-compatible and compile-time-checked syntax such as the following:

// Press F2 to rename properties:
let myObj = { firstName: "John", enableTLAFeature: true, uglyPropName: "" }; 
let myForm = new Form( [ 
  new TextBox  ( myObj, nameof( myObj.firstName ), { required: true, maxLength: 20 } ), 
  new Checkbox ( myObj, nameof( myObj.enableTLAFeature ) ),
  new TextBox  ( myObj, nameof( myObj.uglyPropName ), { label: "Better Label" } )
] );

The component constructors take the object, the property name (as a string), and an optional options object.

Those form components would be able to read & write the property (using obj[propName] syntax) and also generate the display label automatically if none is provided in the options object (each component includes rendering of the label). I can convert the property name into a form label with a helper function. The function converts the first letter to upper case and inserts a space after every lower case letter when followed by an upper case letter and prior to any upper case letter followed by a lower case letter. For example, "firstName" becomes "First Name" and "enableTLAFeature" becomes "Enable TLA Feature". (If we need a better label, then it can be provided explicitly in the options object.)

Note: for nested objects, I just pass the nested object in the first parameter, such as myObj.address and I expect nameof(myObj.address.city) to be converted to "city" at compile-time (I am not doing property path navigation within the components, so I don't need complex expression support). With this approach, myObj, address, and city can all be renamed safely.

The above should be relatively easy to implement (just parse out the last symbol and put quotes around it). One disadvantage, though, is that I wouldn't have type safety for the properties, so I can't ensure that myObj.enableTLAFeature is a boolean. I'm not sure how to solve that, but I really prefer to have the call site specify the property only once (for purposes of reading, writing, and generating the label). If it were possible to pass just myObj.enableTLAFeature (as a boolean and without the nameof operator) and then use a callsitenameof operator within the component constructor (returning "enableTLAFeature" and not returning the constructor parameter name such as "propName"), then that would be fantastic, but I don't know if that would be possible.

MeirionHughes commented 7 years ago

Something like this could be very useful.

nameof<Foo> should appear (for all intents and purposes) as "Foo" that way the compiler / emitter can simply think of it as a string.

but nameof<T> that tracks what T actually is could be very useful for DI containers. For example:

registerConstant<T>(instance:T){
   this.container[nameof<T>] = instance;
}

then you can get around the interface issue for IOC:

binding: ioc.registerConstant<IFoo>(new Foo());

injecting:

constructor(@inject(nameof<IFoo> foo:IFoo){... }
dsherret commented 7 years ago

I created a small experimental library for doing this: https://github.com/dsherret/ts-nameof

It's still a work in progress. Let me know if you have any suggestions.

mmc41 commented 7 years ago

I also need a nameof macro. Unfortunately, the alternative "much smarter solutions" mentioned here does not seem to materialise any time soon. A simple nameof is better than something so complex to design that it never arrives or takes years.

Liero commented 7 years ago

@MeirionHughes: How would it translate into javascript? I don't think it's much better than this very simple alternative: ioc.registerConstant(nameof(IFoo), new Foo());

zpdDG4gta8XKpMCd commented 7 years ago

getting a name of a property or a function (original topic of the discussion) is not the same as getting a name of a type, consider:

let along

this one is interesting too:

wait wait, here is more:

function helloEarthings<a, b, c extends a<b>>(one: number, two? = new class { hi = 'how are you?'  }, ...blah: MyMegaControl<typeof someOtherValueIJustMadeUp>[]) {
}
nameof<typeof helloEarhlings>
nameof<{
    shouldWeGoLexical: 'hm'
    orAlphabetical: 'hm'
}>
MeirionHughes commented 7 years ago

@aleksey-bykov the answer to all of those is literally the type as a string; whether is of any use is a different matter - it gets typescript's types as a tangible string in js.

zpdDG4gta8XKpMCd commented 7 years ago

@MeirionHughes you understand that a type as written (in AST) is not the same as how it was read, stored internally and interpreted by TypeScript, don't you?

example:

const myVariable = 'hey';
type A = typeof myVariable
type B = 'hey';

nameof<A> === nameof<B> // <-- ???
nameof<typeof myVariable> === nameof<A> // <-- ???
nameof<'hey'> === nameOf<A> === nameof<typeof myVariable> // <-- ???
Liero commented 7 years ago

@aleksey-bykov: nameof should not work with anonymous expression by definition. You cannot get a name of something that does not have a name.

however, this should work:

All of this has been already defined in C# and tested in real life and it works well.

I preffer nameof() notation over nameof<> in order to avoid conflict in JSX resp. TSX. This would be probably the most common scenario:

<input name={nameof(this.state.firstName)} />
zpdDG4gta8XKpMCd commented 7 years ago

@Liero, analogy with C# is hard to follow because, well, they are 2 different languages: C# doesn't have unions and intersection types as well as non-local anonymous types, for TS all this is bread and butter

it's not clear to me what the merit is for being able to get the last name at which a type was registered, there is only 40% cases in TS where such question even makes sense due to various anonymous type expressions

besides:

are all 100% same types in TS (thank to structural typing), how would i benefit from seeing them under different names (according to you)?

fredgalvao commented 7 years ago

@aleksey-bykov nameof() is not meant to be used on types, but on pointers mostly if not only. We don't care about the type of that pointer, I just want it's name.

const a: { what: string;}
const b: { what: number;}
const c: { what: any;}
const d: { what: undefined;}
const e: { what: never;}
const f: { what: {};}
const g: { what: typeof a;}
const h: { what: typeof b | typeof c;}
const i: { what: Whatever | You | Need | Ever;}

nameof(a.what) == 'what';
nameof(b.what) == 'what';
nameof(c.what) == 'what';
nameof(d.what) == 'what';
nameof(e.what) == 'what';
nameof(f.what) == 'what';
nameof(g.what) == 'what';
nameof(h.what) == 'what';
nameof(i.what) == 'what';

// they're all the same, regardless of the type

Now, if you're talking about asking the nameof() a type, then I agree with @Liero : the solution is to ignore or have a generic answer to any anonymous type expression, and the real actual answer to any well defined and named type reference. I don't think anyone will ever ask the name of a complex or fuzzy type, that has never passed my mind as a case for this feature.

enoshixi commented 7 years ago

I personally feel like the real value of nameof is not in getting type information at run time, but rather providing type safety when using string literals. As mentioned above, I'm currently using a babel plugin implementing nameof; here's a pretty typical use case for me (using React because that's what I'm currently working with so it's fresh in my mind, but the concept could be applied elsewhere):

interface FooBar {
  foo: string;
  bar: string;
}

interface Props {
  value: FooBar;
  onChange(value: FooBar) => void;
}

const FooBarControl = (props: Props) => {
  const handleChange = (event: React.FormEvent, name: string) => {
    const { value } = event.currentTarget as HTMLInputElement;
    const newValue = Object.assign({}, props.value, { [name]: value });
    props.onChange(newValue);
  };

  return (
    <div>
      <input value={props.foo} onChange={e => handleChange(e, nameof(props.foo))}/>
      <input value={props.bar} onChange={e => handleChange(e, nameof(props.bar))}/>
    </div>
  );
};

I can freely rename the foo or bar properties and nothing breaks, despite updating them using the property name as a literal string during runtime.