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

@aleksey-bykov: don't expect to get the type information from nameof operator. That would be a terrible mistake. Maybe type N = number; nameof(N) does not have real use cases, but for the sake of clarity and consistency, it makes perfekt sense. if(true){} also does not have real use case, but you can write it.

zpdDG4gta8XKpMCd commented 8 years ago

@fredgalvao you are talking about the names of the properties (not types) which is a whole different story that makes a lot of sense and i am with you on that

@Liero if you don't care about the type information, maybe we should limit it to the names of properties which aren't types, but rather value declarations because their name belongs to values which are there to stay in the emitted JS as opposed to types that get erased completely

zpdDG4gta8XKpMCd commented 8 years ago

there are 2 different domains that TS deals with:

now

except for... classes!

classes are in the twilight zone because they possess the properties of both values and types : they are values because of the prototype object and the constructor function, they are types because they govern a set of valid operations for its instances to be a part of

together with all trivial values like properties, functions, variables the classes should be a subject to nameof operator

Liero commented 8 years ago

@aleksey-bykov: That is subject for discussion, but I see use case for nameof with name of the class:

describe(nameof(Person)) {
   it(nameof(Person.sayHello) + `() should say "Hello!"`, function() {
      expect(new Person().sayHello()).toBe("Hello!");
   });
}

It allows for easier refactoring. Although the added value is relatively small, I don't see reason to limit it.

FYI, this is how C# works:

class A {}
nameof(A)
"A"
using B = A;
nameof(B)
"B"

No problem what so ever.

YipYipX4 commented 8 years ago

@aleksey-bykov names of properties is what I need for my use case, which I described in detail above. And other examples have been given that have the same requirement. We'd just like to be able to rename some symbols in our code (using refactoring tools) without breaking code that relies on those symbols at runtime (we don't want to hard code the symbols as strings).

zpdDG4gta8XKpMCd commented 8 years ago

@Liero there is no question about whether to let classes be a subject for nameof, because classes are values, so it's natural to let them be there, i see no problem

@YipYipX4 i hear you, properties were born for nameof, so worry no more, they will be nameable

MeirionHughes commented 8 years ago

getting the names of types doesn't make much sense, because there is nothing in the resulting JS that has anything to do with them

Which is entirely the point; interfaces don't have run-time presence at all... which is a problem, especially if you want to do inversion of control based on reflection.

take a look how inversify is solving the problem

basically if you have an interface IFoo you have to define the string "IFoo" somewhere so that you have an actual value when you get to javascript. Being able to have the interface name (emitted via something like nameof would be useful in making sure refraction doesn't break those symbols.

So given the inversify example, you could do:

public constructor(
  @inject(nameof(Katana)) katana: Katana,
  @inject(nameof(Shuriken)) shuriken: Shuriken
)
zpdDG4gta8XKpMCd commented 8 years ago

@MeirionHughes interfaces should not have runtime presence, interfaces are just types that were given names, types are all illusory entities whose only purpose is to assist in typechecking

now, what you wish you could do is to repurpose them by giving them additional responsibilities, namely to be bonds that hold together

interfaces in TS were not supposed to do that as designed, it's not what they do, they have 0 business in runtime

probably what confuses you is the name inteface and your C# background from where you have certain expecations of runtime behaviour, again C# and TS are very different although share some syntax and keywords

back to your injection framework it would work just as well if was using plain strings for linking implementations with places that depend on it:

@injectable('one-thing')
class A {}
@injectable('another-thing')
class B {}
class C {
   constructor(
     @inject('one-thing') katana: Katana,
     @inject('another-thing') shuriken: Shuriken
   ) {}
}
dsherret commented 8 years ago

@aleksey-bykov here's a use case for interfaces.

Sure someone could put all this in constants, but if someone wants to use nameof with interfaces to keep their constant string values in line, I don't see a good reason to not allow them to do so.

zpdDG4gta8XKpMCd commented 8 years ago

@dsherret

i have looked at it already, thanks, using the name of the TS interface would not solve any problem, consider:

@injectable() 
class Broom extends Broom { sweep(): void {} }

@injectable()
class Katana extends Katana { cut(): void {} }

class Ninja{
    constructor(
        @inject(nameof(Broom)) katana: Katana // <-- did i get my sword yet?
    ) { }
}

questions:

dsherret commented 8 years ago

@aleksey-bykov it would prevent you from doing "broom" instead of "Broom" ;)

On a serious note, I agree that it doesn't provide any extra protection over constants (as shown in the inversify example). Nameof for interfaces would just provide a way to let code be more maintainable by updating strings to interface names. That's really the only reason.

zpdDG4gta8XKpMCd commented 8 years ago

@dsherret before i say it's not worth the trouble, let's take it a bit further, so nameof is a new feature that can take an interface, in my ORM framework i have an interface interface IColumn<TValue> { }, now i need to do nameof(Column<number>) what will it give me? (hint, see my first comment today)

dsherret commented 8 years ago

@aleksey-bykov I would say that the type args should be excluded—nameof(Column<number>) should go to "Column". If someone wanted the type arg they could do: nameof(number);. That's what I did here. If that's not done, then there's no way to easily get the name of an interface with type args... or there needs to be an exception in the language to allow it to be written without the type arg (that would get nasty).

frodegil commented 8 years ago

C#:

     nameof(A.B.C.D);  => "D"  // namespace A.B.C.D 
     nameof(A.B.C.D.IX); => "IX"  // interface IX in namespace A.B.C.D
     nameof(variableName); => "variableName";
     nameof(A.B.C.D.IX<int>); => "IX"  // interface IX<T> in namespace A.B.C.D;
     nameof(instance.Name); => "Name" // property Name of object instance

Typescript:

     nameof(A.B.C.D); => "D"  // module A.B.C.D
     nameof(A.B.C.D.IX); => "IX" // interface IX in module A.B.C.D
     nameof(variableName) => "variableName";
     nameof(A.B.C.D.IX<number>); => "IX" // interface IX<T> of module A.B.C.D
     nameof(instance.Name); => "Name" // property name of object instance

nameof strings can be calculated at compile-time and should be possible in Typescript as well. This solves a lots of scenarios where string-names and type-names (1 to 1) is used in different JavaScript frameworks. This would make re-factoring so much easier (module names, controller names, dependency injections in angularjs, etcetera) Why not make nameof work the same way as in C#?

fredgalvao commented 8 years ago

@aleksey-bykov I agree with you that nameof() for types gets messy really easy, but I don't think we need to go there to provide a satisfatory solution/implementation that covers 99% of the scenarios.

Usual stuff

type ClassFromHell = Class<TypeParam<What<Is<Going | On<I | DONT | EVEN>>>>>;

Groundbreaking thing

Would it be the first thing that makes it through the compile void to runtime from TS? Maybe. But is it bad? I don't think so necessarily.

I really can't see nameof() being able to solve every need of the concept, so I won't even ask it to be fully featured. But holding the other 99% of the cases because of that 1% problematic doesn't seem like a good idea. We're not dealing with something that Ecmascript thinks about, so we don't need to strictly follow a draft or an specification, and as such we could accept this faulty version, which is 10^10*awesome to me already.

zpdDG4gta8XKpMCd commented 8 years ago

i am advocating the devil here, when you come with a proposal you'd better make sure you thought of all in/outs, you need to give a definitive answer to all possible cases (not interfaces only), be it compiler errors of defaults, you need to justify why it should work this way, you need to explain it to your users, you'd better be sure they like it, you need to allocate some resources for developing it and maintaining it, and you need to have a plan how you are going to build new features on top of that or prevent them from conflicting with it

now when the idea is simple and clear and the immediate benefits of it is obvious to everyone, that's one story

so would anyone say no? well, if we are talking about one special interface case, that only works without type parameters, and we don't really know what to do with all other types, because the concept simply makes no sense, let alone being intuitive, that seems like a problem already...

now if are we certain that this problem has to be solved, then we'd better have some real life examples that clearly demonstrate the benefits

so far there wasn't an good example as to why we need nameof for interfaces, besides unsupported statements about being so much easier to write code and maintain

fredgalvao commented 8 years ago

I agree again with most of what you said. I wouldn't suggest or vote for a blurry feature. That's why I would love to see a well-defined clearly-limited simple version of this feature than to see no version at all.

I don't get how the examples posted here so far are considered "unsupported statements" though. Most of them are real world scenarios, specially IoC, used in many projects involving many different libraries and frameworks. Being able to write better code and maintain it with ease is the reason Typescript was created to begin with. We don't have HKT on it, for example, but we still have TS reaching v2.0 :wink:. I agree again*again that it won't solve every issue, but it'll solve a lot.

zpdDG4gta8XKpMCd commented 8 years ago

as far as interfaces (not properties, variables, classes which are out of question) i only saw one poor example and a few loud statements

essentially the problem that nameof is trying to solve for interfaces is to associate an interface with a string, so that:

with a little inconvenience this can be done in plain TS as soon as today:

type Wrapper<Type> = string & { '': [Type] };
function toWrapperFor<Type>(name: string) : Wrapper<Type> { return <any>name; }
const associations = {
    one: toWrapperFor<string>('one'),
    another: toWrapperFor<MyMegaControl>('another')
};

// ... later in code

class MyClass<Type>{
    constructor(private wrapper: Wrapper<Type>) {}
    take(value: Type): void { /*...*/ }
    toString(): string { return this.wrapper; }
}

const oneInstance = new MyClass(associations.one); // MyClass<string>
oneInstance.toString(); // 'one'
const anotherInstance = new MyClass(associations.another); // MyClass<MyMegaControl>
anotherInstance.toString(); // 'another'
dsherret commented 8 years ago

@aleksey-bykov the point is to have a string be guaranteed to be the same as an interface name. This is a minor benefit, but a benefit nonetheless. Your example doesn't do that. We already talked about the example with IoC and how it would be nice to have the strings refactor when refactoring the interface name.

To list out some reasons:

  1. It guarantees the strings will be the same as the interface name. This helps with the overall code quality in a small way.
  2. It saves a very small amount of time because when changing an interface name you don't also have to change the string... refactoring tools will do it for you.
  3. Using nameof helps show what the strings represent and because it shows what the strings represent, it allows us to quickly navigate to the interface definition using "go to definition". That helps with code clarity.

Those are the only reasons I can think of at the moment. These aren't loud or unsupported statements.

I would say for types that only interface and type alias types should be allowed—no union/intersect/object/string/etc. types. Maybe even keywords (like number) shouldn't be allowed.

Liero commented 8 years ago

Again, don't reinvent the wheel. C# example:

class A<T> { }
nameof(A<string>)
"A"

when it comes to interfaces, pros and cons have been mentioned. No need to spam discussion anymore. There are @ahejlsberg and other great guys, they will make the right decision at the end of the day. Let's collect some more use cases

RobertoMalatesta commented 7 years ago

nameof is desperately needed if you, like me, use a NoSQL DB and do a lot of queries on string specified fields that loose refactoring, consistency, error-catching & auto-completion the moment you enter the quirky realm of Stringland.

this issue,open since 2014, really represents the saying: best is the enemy of better

--R

Peter-Juhasz commented 7 years ago

AngularJS example, configuring the DI:

angular.module("news", ["ngRoute"])
    .controller("NewsIndexController", App.News.NewsIndexController) // nameof

and referencing services:

    .config(($routeProvider: ng.route.IRouteProvider) => {
        $routeProvider
            .when("/news", {
                controller: "NewsIndexController", // nameof
                templateUrl: "modules/news/views/index.html",
                controllerAs: "vm",
            })
    })
aluanhaddad commented 7 years ago

I think a lot of the uses here could be accomplished using decorators if they were more generally applicable. If function decorators are ever implemented, then they would be able to cover this use case.

mhegazy commented 7 years ago

Many of the scenarios described in this thread should be handled by keyof operator introduced in https://github.com/Microsoft/TypeScript/pull/11929

Liero commented 7 years ago

keyof is absolutelly awesome! But still need something for name attributes, when databinding: <input name={nameof(this.state.firstName)} />

Peter-Juhasz commented 7 years ago

Yes, good to see some results in this area. But the use case for keyof is different. You still can't reference the name of:

thomas-darling commented 7 years ago

@mhegazy

keyof is nice, but it's not enough. nameof would still be extremely useful when e.g. declaring the dependencies of computed properties in Aurelia:

@computedFrom(nameof this.foo.bar)
// should compile to: @computedFrom("this.foo.bar")

Or in error messages for invalid function arguments:

throw new Error(`The value of ${nameof options.foo} must be positive`)
// should compile to: throw new Error(`The value of ${"options.foo"} must be positive`)

This would finally make those things refactoring safe. They are currently a huge pain to maintain, and when you mess them up, it happens silently - you just get performance issues that can be extremely hard to debug, and misleading error messages.

I see some of the posts above talk about different variations of this and concerns around generics, etc. In my opinion, this should be as simple as possible - just give me exactly what I wrote as a string. So nameof Foo<Bar> should compile to exactly that - "Foo<Bar>". If the generics are not needed, those can just be removed later using a regex. I really don't see why this has to be so complicated.

Or if that is unacceptable, just strip away the damn generics, or don't support generic types - just please reconsider this. It is such a useful feature, and very much needed.

aluanhaddad commented 7 years ago

@thomas-darling if it were anything like the C# definition, nameof foo.bar would be "foo", not "foo.bar".

Anyway, you can achieve the type safe, computedFrom decorator for unqualified names, using

// computed-from.ts
export function computedFrom<K1 extends string, K2 extends string, K3 extends string>
  (prop1: K1, prop2: K2, prop3: K3): 
    (target: {[P in K1 | K2 | K3]}, key: string | number | symbol) => void;

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;

and then consume it like this

// welcome.ts
import { computedFrom } from './computed-from';

export class Welcome {
  firstName: string;

  lastName: string;

  @computedFrom('firstName', 'lastName') get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

qualified names like "foo.bar" are also possible.

jods4 commented 7 years ago

@aluanhaddad

qualified names like "foo.bar" are also possible.

This is something I somehow missed, care to elaborate?

aluanhaddad commented 7 years ago

I was thinking it would be possible to overload a tagged template string declaration but but upon further reflection I don't think it will work. I apologize for any confusion.

vytautas-pranskunas- commented 7 years ago

Right, so instead of introducing nameof you suggest to extend all libraries like immutablejs and etc?

fredgalvao commented 7 years ago

@aluanhaddad also these examples you provided don't solve the "issue" that is having code references inside strings, which are pretty much unrefactorable automatically or easily. The main goal of nameof in my PoV is to make those string refactorable and searchable.

jods4 commented 7 years ago

@fredgalvao this might change #11997

styfle commented 7 years ago

@RyanCavanaugh If the new keyof feature really does solve this use case for nameof, can you provide an example...preferably using the TypeScript Playground to prove it works? Thanks!

styfle commented 7 years ago

Scratch that--I tried it myself and it works almost perfectly.

interface User {
    name: string;
    age: number;
}

var str1: keyof User;
var str2: keyof User;
var str3: keyof User;

str1 = "name"; // successful compile
console.log(str1);

str2 = "Age"; // compiler error due to typo
console.log(str2);

str3 = "age"; // successful compile
console.log(str3);

Playground source

When I try to use Rename Symbol (F2) in vs code, it does not rename my string which is what I would expect from a nameof(mySymbol) feature.

Additionally, I don't see a way to handle the use case with function parameters:

function log(name: string, age: number) {
    console.log(nameof(age), ' is ' , age);
}

So keyof gets us 80% of the way there I think but nameof is still better.

Update

I added aluanhaddad's example to cover both use cases here: Playground Source

fredgalvao commented 7 years ago

Function parameter names are a harder beast to tame, because at runtime that name might be something else entirely, for it's one of the sure cases to be minified/uglyfied by 99% of post-processors.

CoenraadS commented 7 years ago

My use-case is for sockets.

Using socket.io, socket emiters/receivers are linked using strings.

So currently I am writing this: (Where showX() are interface implementations both client and server side)

public showLobby(): void {
    this.server.emit("showLobby");
 }

What I want to write:

public showLobby(): void {
    this.server.emit(nameof(this.showLobby));
 }

(I know I can write this.showLobby.name, but it would be nice to have compile time strings)

The second implementation would allow me to refactor my interfaces, and the client/server magic strings would also automatically change. As far as I can see this is not possible with keyof.

aluanhaddad commented 7 years ago

When I try to use Rename Symbol (F2) in vs code, it does not rename my string which is what I would expect from a nameof(mySymbol) feature.

Indeed but at least you get a compile time error letting you know that you must update these use sites. That makes the refactoring operation safe if not optimally convenient.

The same can be done for parameters so the log example works as well as the others and also demonstrates one of the really powerful aspects of this feature

function log<K extends keyof User>(name: K, value: User[K]) {
  console.log(name, ' is ', value);
}

const user: User = {
  name: 'Jill',
  age: 50
};

log(str1, user[str1]); // OK
log(str3, user[str3]); // OK
log(str3, user[str1]); // Error
log('age', user.name); // Error

deeply correlated f-bounds.

jods4 commented 7 years ago

@aluanhaddad I don't think your log example works exactly as you illustrate. My understanding is that the following would be perfectly OK:

function log<K extends keyof User>(name: K, value: User[K]) { }

const user: User = {
  firstName: 'John',
  lastName: 'Doe'
};

log('firstName', user.lastName);  // OK, unfortunately

This is because K is inferred to 'firstName' and hence, U[K] is inferred to string, which lastName satisfies. Your example is nice only because each property had a different type.

jods4 commented 7 years ago

Indeed but at least you get a compile time error letting you know that you must update these use sites. That makes the refactoring operation safe if not optimally convenient.

Most of the time, but not always. You can make changes that result in conflicting names and still compile. Like renaming a method to introduce another method with the same name as the old one.

An even more important tool for me is Find all references, which simply doesn't work. Its substitue is a plain text Find, but (a) you need to get in the habit of never using Find references anymore, which is a shame and (b) for common names, Find is extremely noisy and turns up tons of false positive.

Go vote for #11997!

vytautas-pranskunas- commented 7 years ago

@RyanCavanaugh: how keyof can help with with immutable.js for instance? you cannot extend all immutable classes. nameof could help with that. As far as Angular2 goes with typescript and for performance it goes with immutable.js too, nameof is essential.

Liero commented 7 years ago

@RyanCavanaugh: maybe time to reopen? We've collected enough usecases where keyof does not help. Thanks

CoenraadS commented 7 years ago

I hacked up a gulp task to do it for now. Just run it before the typescript compiler. If anyone is interested:

Probably not safe for production.

var replace = require('gulp-string-replace');

gulp.task("nameof", function () {
    var regexReplace = function (match) {
        // nameof(foo.bar) => foo.bar
        var propName = match.slice(7, -1).trim();
        // foo.bar => bar (also works if no . present)
        var endElement = propName.split(/[.]+/).pop();
        return '"' + endElement + '"';
    };

    return gulp.src("./src/**/*.ts")
        .pipe(replace(/nameof\s*\([^\)]*\)/, regexReplace))
        .pipe(gulp.dest("./nameof"));
});
dsherret commented 7 years ago

@CoenraadS if you are using a gulp task to do this then you might be interested in ts-nameof as most of the work is already done there (I mentioned it earlier in this thread). I've been using nameof in typescript for quite some time now.

vytautas-pranskunas- commented 7 years ago

Cannot understand, if someone did ts-nameof, gulp task - why for ts team this is such a problem!

paulinfrancis commented 7 years ago

@vytautas-pranskunas- Based on the comments in this thread about keyof, I did this to enforce the correct usage of keys in Immutable:

export interface IAppState {
    isConnectedToServer: boolean
}

const checkKey = <T>(key: keyof T) => key;

export const reducer: Reducer<IAppState> = (state = initialState, action: KnownAction) => {
    switch (action.type){
        case 'CONNECTED_TO_SERVER':
            return state.set(checkKey<IAppState>('isConnectedToServer'), true)
        case 'DISCONNECTED_FROM_SERVER':
            return state.set(checkKey<IAppState>('isConnectedToServer12345'), false) //Compile time error :)

Arguably, it's more verbose than nameof would be, and you don't get any help when refactoring, but at least you get compile time checking.

vytautas-pranskunas- commented 7 years ago

@paulinfrancis Yes it can be done like that. But still main question stays unanswered: Why community should do all kind of helper libs and hacks to get such obvious behavior and why ts team refuse to implement this?

thomas-darling commented 7 years ago

And then there's the refactoring support, which we also won't get with keyof. I understand and respect your reluctance to introduce new keywords, but this is a fundamental language feature that is sorely needed, and has clear and indisputable use cases - and the lack of it causes both serious pain and nasty bugs on a daily basis. What more does it take to get this reopened?

gleno commented 7 years ago

Every 3-4 months I come back to this thread only to be once again disappointed by the stubbornness to not include the nameof() operator. It's really a major hassle for a lot of people, and not including it in favor of some other use case is just... unbelievable.

Everyone seems to agree that it should work more or less exactly as C# does it.

I think we should organize a protest - drive up to Redmond and picket MS headquarters... as I'm seriously out of ideas.

zpdDG4gta8XKpMCd commented 7 years ago

not enough peope are giving a shit, creating new issues regarding known problem works the best