microsoft / TypeScript

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

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

Closed Taytay closed 8 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));
gcnew commented 7 years ago

I've not checked the transforms API but I'd imagine it would be relatively easy to implement nameof in a transform. The transform may even be shipped with the compiler, but not an official part of it. From this point of view, it's been a really smart move from the team not to implement it prematurely.

gleno commented 7 years ago

@gcnew What easy? nameof([put valid identifier here]) -> last part of that identifier as string. I mean it's a glorified macro. In fact, at work we are just about to add a function nameof(thing:any) that gets replaced with exactly what @frodegil suggested above at post-compile with regex or whatever. I'm sure there are some edge cases that technically might require some more thought, but this is basically covering all use cases expressed in this thread. It would just make an infinity of sense to add nameof() it to a language plagued with magic strings and lack of reflection - instead we get keyof(). Palm. Face.

gcnew commented 7 years ago

@gleno keyof is a very useful feature. The fact that the problems it solves are different from what people want from nameof is another story.

From my point of view nameof should not be implemented because it scavenges a valid identifier and adds non-standard expression level syntax. When Transforms API gets merged-in, it should be fairly straightforward to write a transfrom yourself or use the best community one.

zpdDG4gta8XKpMCd commented 7 years ago

@gcnew my understanding that transforms are from existing syntax -> existing syntax, nameof is new syntax so it's missing out, i hope i am wrong

gcnew commented 7 years ago

@aleksey-bykov Yes, I think so as well. However if you have a function declaration such as:

declare function nameof(x: any): string;

and a transform that replaces all global nameof invocations with its argument stringified, it should work.

Edit: such a solution will not be sufficient where a string literal is expected. Nontheless it's a nice first step and the more compiler APIs become available the more it could be improved on.

Liero commented 7 years ago

@gcnew: If this is gonna be an official solution for nameof scenarios, then please write it to documentation, for example here: https://www.typescriptlang.org/docs/handbook/gulp.html

Peter-Juhasz commented 7 years ago

@gcnew If you look at Roslyn, the extension points are diagnostics, fixes and refactorings which provide help during development and have no risk of using any of them. The last thing I want to do is adding random packages which modify the very heart of the build pipeline, the compilation and supporting these non-existing language constructs with different hacks. And then end up with problems like "we can't upgrade to TypeScript 2.x, because "nameof package" 0.1.2 does not support it yet, so 1) we will have to wait or 2) remove all usages of nameof from our code and loose it; or 3) reimplement it ourselves and maintain it forever" in a few months.

zpdDG4gta8XKpMCd commented 7 years ago

@Peter-Juhasz who makes you download it from npm? make it yourself and be your own 24/7 customer service

transformation api has very little to do with these circumstances

gcnew commented 7 years ago

@Peter-Juhasz I feel for you and I do agree that nameof would be useful and add safety. However TypeScript is about modelling ECMAScript, not making up a new better language. Features such as keyof are purely type level, that's why they are getting implemented. And even for them there is resistance to allocate a keyword. In contrast, nameof is strictly an expression level construct. I don't think the team will ever be convinced to implement it, unless it's in the ECMAScript spec. I myself don't like the idea of scavenging the nameof identifier.

This leaves us with two options:

The first option will take years (if it ever gets approved), the second should be decent enough. I have no experience with using the TS APIs, but I'd expect them to be stable and backward-compatible, thus the nameof package should be stable as well.

tomsdev commented 7 years ago

I disagree Marin, nameof isn't a language feature, it only enables type-checking (similarly to keyof), once compiled nameof disappears and its argument is left as a string. I don't see why this would need to be part of ecmascript if keyof doesn't.

On Feb 14, 2017 19:19, "Marin Marinov" notifications@github.com wrote:

@Peter-Juhasz https://github.com/Peter-Juhasz I feel for you and I do agree that nameof would be useful and add safety. However TypeScript is about modelling ECMAScript, not making up a new better language. Features such as keyof are purely type level, that's why they are getting implemented. And even for them there is resistance from the team to allocate a keyword. In contrast, nameof is strictly an expression level construct. I don't think the team will ever be convinced to implement it, unless it's in the ECMAScript spec. I myself don't like the idea of scavenging the nameof identifier.

This leaves us with two options:

  • make an ECMAScript proposal and hope it gets traction
  • use the public Transfroms API and have a good approximation

The first option will take years (if it ever gets approved), the second should be decent enough. I have no experience with using the TS APIs, but I'd expect them to be stable and backward-compatible, thus the nameof package should be stable as well.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/1579#issuecomment-279790055, or mute the thread https://github.com/notifications/unsubscribe-auth/ABOZ9c-85P3x5AMakVDuxlNkH56me3POks5rcfAYgaJpZM4DNVgi .

RobertoMalatesta commented 7 years ago

At least for interfaces, that go poof after compiling (no code produced and no impact on JS Standard) we could have helper functions that expose all their metadata. It's rather trivial (I'm currently doing it with a bash script), and it would be really useful to have not only nameOf, but implements/extends informaton, the list of fields, fields names and their arity (signature)as well.

Honk me if interested, we could write a proposal and in the worst case a pre-processor. (time and motivation permitting)

--R

Liero commented 7 years ago

@gcnew: 1. nameof will never be in ES spec. I'm pretty confident to say so, because nameof doesn't make much sense in EcmaScript. Here's why: Because of dynamic typing in javascript, nameof(myObject.MyProperty) is exactly the same as "myProperty". Both are basically "magic strings". There is no reason to use the verbose nameof syntax in javascript. Or do you see any advandage of using nameof in javascript?

As @RyanCavanaugh mentioned, only standardized ES features or features that will never be in ES spec can be considered for typescript in order to avoid confusion like it happend with modules. This is the second case.

  1. The very basic purpose of TypeScript is to provide static typing and allow for better refactoring tools. Property names are required very often in DataBinding scenarios and doing so in type safe manner is exactly what you would expect from TypeScript.

I'm a pragmatic programmer and I don't care how it can be done if it can be done. Choose whatever you think's best for the product considering ES compatibility, easy-of-use, performance or whatever you think is relevant. If you decide to go with Transfroms API, let it be so, but for god's sake, this issue deserves a solution.

gcnew commented 7 years ago

@Liero You can never know whether a valid identifier would be used in future versions. It might not be used by ECMA directly or not for the purpose discussed in-here, but it might find its use in browser APIs. Consider a global function nameof(node: Node): string | undefined which reads the name attribute of a DOM node. Will such a function ever be standardised? Probably not this one but maybe one concerning certificates or Symbols. The point is nameof cannot be borrowed as a keyword. There is code already using it for a local variable names and future APIs may use it as well. It would be an unfortunate breaking change, whilst diverging from the ECMAScript specification.

The solution is either to lift nameof to the type level, or implement nameof as an optional pre/post processing step, that is not part of the official language.

A type-level, fully erasable nameof might be:

declare const options: {
    easing?: string,
    duration: number
};

if (!options.easing) {
    throw new Error(<nameof options.easing> 'easing');
}
fredgalvao commented 7 years ago

Level

nameof(path.to.identifier) will never survive the compilation process.Seeing nameof in the final compiled code is definitely and exactly NOT what everyone asking for the feature wants. We want it to disappear, and leave behind the string "identifier", which makes the implementation of nameof as a compile/type level precisely what it needs to be. The only difference between:

type AliasToUnion = A | B; and nameof(object.property)

is the outcome: one gives an empty string and the other gives a non-empty string. Both are type level structures, and both have their shell disappear once the compilation is done.

Collision

If now the argument is that we can't risk a keyword collision between typescript's nameof and future ecma's nameof, then I'd say the same argument applies to pretty much everything typescript has that is not in the intersection with es8 drafts, which is basically a lot of type stuff, thus making this argument quite bogus.

Desire

Even if we don't go the keyword way, this pretty much sums for me why I still think it's optimally typescript's job to do this (be it a transformation, a compile level function, a type level structure, an ecma proposal, etc):

The very basic purpose of TypeScript is to provide static typing and allow for better refactoring tools. Property names are required very often in DataBinding scenarios and doing so in type safe manner is exactly what you would expect from TypeScript.

Suggestion

<nameof options.easing> 'easing'

Seems like it almost does it, but we'd have to mannually refactor that not-so-magical string anyway, with the only difference that we'd maybe have the compiler tell us that it's value does not represent the actual name of the identifier. It's a refactor->compile->see_error->go_back_and_properly_refactor flow instead of a refactor_properly one. With that, I'd rather insist in a more complete version instead of this proposal. Now, @gcnew, if we could make <> a structure that didn't need a value to be cast, that'd be awesome (I don't think we can atm).

zpdDG4gta8XKpMCd commented 7 years ago

guys, nameof is sure needed, in mean while there is 90% capable workaround which only takes a bit of extra typing:

type NamesOf<T> = { [P in keyof T]: P }
interface Data {
    name: string;
    value: number;
}
const propertyNames: NamesOf<Data> = {
    name: 'name,' // <-- not a magic string anymore, CAN ONLY BE `name`
    value: 'value' // <-- not a magic string anymore, CAN ONLY BE `value`
}

image

Liero commented 7 years ago

@gcnew:

Consider a global function nameof(node: Node): string | undefined

usage in existing javascript files - no problem at all. In typescript conflicts can be avoided by:

var _nameof = window.nameof; //problem solved    

BTW, in javascript the function would be probably called nameOf

@fredgalvao: your suggestion: <nameof options.easing> already conflicts with JSX, resp TSX

fredgalvao commented 7 years ago

@Liero that was not my suggestion, I was just commenting on @gcnew 's suggestion.

gleno commented 7 years ago

Ok, what if there was typeside nameof operator?

Something that would look like this:

const obj = {x:1};
function abc(thing:nameof(obj.x)){ }

Which would behave exactly as

function abc(thing:"x"){ }

It's not as clean as nameof(accessor)->string, but I think it could be quite useful.

frodegil commented 7 years ago

I consider nameOfas a compile-time keyword only, and can't see any runtime or global use of it (never say never, but its unlikely, IMO - call it TSNameOf, and it's even more unlikely)

Should we be afraid of collisions? If, or when, EcmaScript becomes a language that need the nameOf keyword in the future, I assume it has evolved so far, and adopted all features of TS so that we won't need TS any longer.

nameOf in TS should behave the exact same way as nameOfin C#

C# (v5) specs:

The nameof expression is a constant. In all cases, nameof(...) is evaluated at compile-time to produce a string. Its argument is not evaluated at runtime, and is considered unreachable code (however it does not emit an "unreachable code" warning).

I've spent too much time "refactoring" string-literals in my TS/Angular code by now. After all, the main purpose of Typescript is to be a strongly-typed alternative, right?

Hurry, include the compile-time nameOf keyword before TS becomes obsolete :)

gcnew commented 7 years ago

What about a custom nameOf function like the following?

declare const options: {
    easing: string,
    duration: number,
};

function nameOf<T, K extends keyof T>(_: T, key: K) {
    return key;
}

nameOf(options, 'easing'); // 'easing'
nameOf({ options }, 'options'); // options

PS: keyof refactoring is tracked by https://github.com/Microsoft/TypeScript/issues/11997.

dsherret commented 7 years ago

I have a very simple implementation of nameof implemented with the transformation api (#13940) that I did quickly this morning (See this branch of ts-nameof—specifically this file and used in this file).

If we had a way in tsconfig.json to specify paths to external before & after transformations while compiling I feel that this would be more powerful than having implementations directly in the compiler.

Edit: The library now supports babel and the typescript compiler.

jakkaj commented 7 years ago

For my two cents, I'd love to see nameof purely to get rid of strings in bindings with Inversify.

rjamesnw commented 7 years ago

I was just looking into this as well. I was hoping this would be an obvious feature implemented by now. I was hoping to get event string names from method names in a custom event setup (bla bla bla ;)), among other things (such as storing namespace and type metadata, etc.). Hopefully, we won't need to discuss this for another 2 years.

markusmauch commented 7 years ago

I would love to see that, too. I'm working with some ASP.NET AJAX legacy code and have to deal with stuff like:

DerivedClass.registerClass( "DerivedClass", BaseClass );

It would be really nice to do this instead:

DerivedClass.registerClass( nameof( DerivedClass ), BaseClass );

Currently, renaming DerivedClass will break the Code.

yuba commented 7 years ago

nameof() would be nice for using Immutable.js Record.

This code

class MyRecord extends Record({value: ""}) {
  readonly value: string;

  withValue(newValue: string) {
    return this.set("value", newValue) as this;
  }
}

can be rewritten like

class MyRecord extends Record({value: ""}) {
  readonly value: string;

  withValue(newValue: string) {
    return this.set(nameof(this.value), newValue) as this;
  }
}

, which is safe for renaming.

jankaspar commented 7 years ago

Potential workaround for name of class or variable:

function nameof<T, P extends keyof T>(descriptor: {[P in keyof T]?: T[P]; }): P
{
    for(var key in descriptor) {
        if(descriptor.hasOwnProperty(key)) {
            return key as P;
        }
    }
}

class Test { }
var test= ""

var x = nameof({ Test })  // x: "Test" = "Test"
var y = nameof({ test })  // y: "test" = "test"
vytautas-pranskunas- commented 7 years ago

@jankaspar Is it possible to have Test.foo with your example where Test - class, foo - class property?

jankaspar commented 7 years ago

@vytautas-pranskunas- No, for that you are much better using the mapped types. Something like this:

class Test {
    foo: string
}

function propertyName<T>(name: keyof T){ 
    return name;
}
propertyName<Test>("foo") 
propertyName<Test>("bar")  // error
emmanueltouzery commented 7 years ago

note that it's not perfect. I have attempted using things like that and it failed, because the interfaces were too complex, and the compiler, instead of generating "field1" | "field2" for the keyof T, generated something like any, and anything would compile.

miensol commented 7 years ago

Thank you! Spark by Readdle

niemyjski commented 7 years ago

Is there any consensus on how to do this with classes and interfaces? I see how I can just keyof but basically I want to use nameof(type) so it would make logging easier e.g., LogManager.getLogger(nameof(MyClass))

styfle commented 7 years ago

@niemyjski This is available with standard JavaScript.

class MyClass {
   hello(n) { return 'hello ' + n; }
}
console.log(MyClass.name);

See Function.name

dpogue commented 7 years ago

@styfle That fails if the code is minified, which is why this is asking for a compile-time operator that produces a string in the output and won't be affected by minification

nevir commented 7 years ago

@dpogue most minifiers give an option to disable name mangling (globally, or for annotated functions/classes); that might satisfy?

It may also be surprising to some that nameof(MyClass) != MyClass.name (in the minified case)

Also, #8 and #16037 might have some related discussions to keep an eye on

niemyjski commented 7 years ago

Yes, we need something reliable...

Thanks -Blake Niemyjski

On Wed, Jul 19, 2017 at 11:47 AM, Ian MacLeod notifications@github.com wrote:

@dpogue https://github.com/dpogue most minifiers give an option to disable name mangling (globally, or for annotated functions/classes); that might satisfy?

Also, #8 https://github.com/Microsoft/TypeScript/issues/8 and #16037 https://github.com/Microsoft/TypeScript/issues/16037 might have some related discussions to keep an eye on

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/1579#issuecomment-316447179, or mute the thread https://github.com/notifications/unsubscribe-auth/AA-So1aBKzLHIsS4-wYSeZmtNOf55B9Rks5sPjMYgaJpZM4DNVgi .

jakkaj commented 7 years ago

@nevir and @dpogue this solution may not work where you're attempting to resolve by interface and not a concrete type via DI - i.e. does not solve for interface name availability at runtime.

aluanhaddad commented 7 years ago

I have found a need for something like this recently sincekeyof does not allow qualified identifiers.

Since the API I used in my example now supports qualified names using dotted path strings, the augmentation I was using to improve its type safety:

export function computedFrom<K1 extends string, K2 extends string>
  (prop1: K1, prop2: K2): (target: {[P in K1 | K2]}, key: string | number | symbol) => void;

  export function computedFrom<K extends string>
    (prop: K): (target: {[P in K]}, key: string | number | symbol) => void;

can no longer express its capabilities.

grokky1 commented 7 years ago

Why after all this time, TS still has no mechanism to (e.g. nameof) to eliminate MAGIC STRINGS?

I don't understand why this thread has so much begging, justifications, and use cases. It's a no brainer - this should be a top priority. It's the most horrible part of JS which TS has not yet solved.

andrewf-work commented 7 years ago

+1

this would be a useful feature

worthy7 commented 6 years ago

So what's the story people, is this happening?

reduckted commented 6 years ago

As @jakkaj mentioned earlier, this would be a really great feature to use with inversify.

Here's an example of what you currently have to do when you use interfaces:

interface Weapon {
    hit(): string;
}

// Define an object that contains all of the type 
// symbols so that we only use magic strings once.
let TYPES = {
    Weapon: Symbol("Weapon")  // Magic String! Oh No!
};

@injectable()
class Ninja {
    constructor(@inject(TYPES.Weapon) weapon: Weapon) { }
}

With a nameof operator, you could at least get rid of the magic string, but you could also get rid of the object that holds all the type symbols:

interface Weapon {
    hit(): string;
}

@injectable()
class Ninja {
    constructor(@inject(Symbol(nameof(Weapon))) weapon: Weapon) { }
}

I believe that strings can also be used to declare what is injected, so you could even change the constructor to this:

@injectable()
class Ninja {
    constructor(@inject(nameof(Weapon)) weapon: Weapon) { }
}
aluanhaddad commented 6 years ago

@reduckted

Here's an example of what you currently have to do when you use interfaces:

interface Weapon {
    hit(): string;
}

// Define an object that contains all of the type 
// symbols so that we only use magic strings once.
let TYPES = {
    Weapon: Symbol("Weapon")  // Magic String! Oh No!
};

@injectable()
class Ninja {
    constructor(@inject(TYPES.Weapon) weapon: Weapon) { }
}

Actually, that is not a magic string. You could just as well write

const types = { // this should be frozen in real code.
  Weapon: Symbol()
};

and the implications would be the same (all consuming code must reference the Weapon property of the types object).

With a nameof operator, you could at least get rid of the magic string, but you could also get rid of the object that holds all the type symbols:

interface Weapon {
    hit(): string;
}

@injectable()
class Ninja {
    constructor(@inject(Symbol(nameof(Weapon))) weapon: Weapon) { }
}

This would likely not have the desired effect as each time Symbol is called it creates a new unique value

Symbol("x") === Symbol("x") // false

I believe that strings can also be used to declare what is injected, so you could even change the constructor to this:

@injectable()
class Ninja {
    constructor(@inject(nameof(Weapon)) weapon: Weapon) { }
}

If the proposed functionality is analogous to C#'s nameof operator, this would result in @inject("Weapon"), introducing hidden global dependencies and token collisions.

Something like

interface Weapon {
    hit(): string;
}
const Weapon = Symbol();
export default Weapon;

seems preferable but it is still not ideal since. The trouble is keeping the names in sync to sustain declaration merging, but is not related to the actual values.

There are plenty of other reasons to want nameof, but I don't think it is a good fit for DI.

reduckted commented 6 years ago

@aluanhaddad

This would likely not have the desired effect as each time Symbol is called it creates a new unique value

Ah yes, that's true. Symbol.for would fix that, but as you point out, the name you use for the symbol is somewhat irrelevant if you only create the symbol once and export it.

this would result in @inject("Weapon"), introducing hidden global dependencies and token collisions.

Yes, there is the potential for token collisions, but on a small project where all interfaces have a unique name, it would work perfectly fine (and InversifyJS makes it really easy to manage, as I've recently discovered 😁).

I can see nameof being a good fit for dependency injection if you're aware of the limitations. Just like, as someone mentioned earlier in this thread, there would be limitations when using nameof with minified code. For example, you might use nameof to get the name of a parameter. During minification, the parameter name is changed, but the compiler-generated "magic string" keeps the original name. Maybe you want the original name, or maybe you want the modified name. As long as you're aware of the limitations that would come with nameof, you'll be fine.

Vasim-DigitalNexus commented 6 years ago

+1

rchanou commented 6 years ago

Here's a couple more hacky workaround functions that work for my purposes so far, which can get the name or path of a nested prop with static checking.

function nameOf<T>(obj: T) {
  let name: string | undefined;

  const makeCopy = (obj: any): any => {
    const copy = {};
    for (const key in obj) {
      Object.defineProperty(copy, key, {
        get() {
          name = key;
          const value = obj[key];
          if (value && typeof value === "object") {
            return makeCopy(value);
          }
          return value;
        }
      });
    }
    return copy;
  };

  return (accessor: { (x: T): any }): string | undefined => {
    name = undefined;
    accessor(makeCopy(obj));
    return name;
  };
}

function pathOf<T>(obj: T) {
  let path: string[] = [];

  const makeCopy = (obj: any): any => {
    const copy = {};
    for (const key in obj) {
      Object.defineProperty(copy, key, {
        get() {
          path.push(key);
          const value = obj[key];
          if (value && typeof value === "object") {
            return makeCopy(value);
          }
          return value;
        }
      });
    }
    return copy;
  };

  return (accessor: { (x: T): any }): string[] => {
    path = [];
    accessor(makeCopy(obj));
    return path;
  };
}

Example usage:

screen shot 2018-02-26 at 15 59 00
goodmind commented 6 years ago

Why this is expression-level operator when it should be on type-level?

Like this:

type Meme = {}
type S = nameof Meme // NameOf<Meme> perhaps better?

const wrong: S = "wrong" // Type '"wrong"' is not assignable to type '"Meme"'.
const ok: S = "Meme"
rjamesnw commented 6 years ago

@goodmind Why on earth would you want to assign the name of “Meme” (theoretically a string) to a type? That makes no sense. Did you mean “typeof”?

stamminator commented 6 years ago
const Why_does_TypeScript_not_have_this_yet: Explanation = { provided: false, reason: null };
console.log(`${nameof(Why_does_TypeScript_not_have_this_yet)}?`);
console.log(Why_does_TypeScript_not_have_this_yet);

if (!Why_does_TypeScript_not_have_this_yet.provided && (new Date()).getFullYear() >= 2018)
    this.me.sad = true;
dsherret commented 6 years ago

I highly doubt they would ever implement this because nameof is not in the type namespace and not in any ECMAScript proposals.

That's not a problem though... if #14419 ("Plugin Support for Custom Transformers") were implemented then we could use our own implementations of nameof, which could be more powerful than a standard nameof function.

For example, if #14419 were implemented, then we would be able to specify the transformation plugin (example nameof implementation here) in tsconfig.json:

{
  "compilerOptions": {
    "customTransformers": {
      "before": ["node_modules/ts-nameof"]
    }
  }
}

So overall, I think it makes more sense to ask for #14419 than this issue because it opens the door to nameof and other transformations being easily integrated into a project.

RyanCavanaugh commented 6 years ago

Honestly it would be better if this was straight up closed with "We don't want this, sorry not sorry" then the silence and lack of triage.

ಠ_ಠ