You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Using enum member with spaces in type position breaks TypeScriptΒ #43171
Ryan Cavanaugh (@RyanCavanaugh) Sorry but I need to come back to this again. I must have confused something while creating the repro yesterday. If I use the enum values instead of numbers it works exactly as I'd expect:
enumFoo{Bar=1,Baz=2,"B A"=3,}declarefunctionfn(foo: Foo.Bar,arg2: number): void;declarefunctionfn(foo: Foo.Baz,arg2: string): void;declarefunctionfn(foo: typeofFoo["B A"],arg2: boolean): void;// why is this necessary ^^^^^^ ?fn(Foo.Bar,"1");// error as expectedfn(Foo.Baz,"1");// no error as expectedfn(Foo.Bar,1);// no error as expectedfn(Foo.Baz,1);// error as expectedfn(Foo["B A"],true);// no error as expectedfn(Foo["B A"],1);// error as expectedinterfaceCB{(foo: Foo.Bar,arg2: number): void;(foo: Foo.Baz,arg2: string): void;(foo: typeofFoo["B A"],arg2: boolean): void;// why? ^^^^^^}declareconstcb: CB;cb(Foo.Bar,"1");// error as expectedcb(Foo.Baz,"1");// no error as expectedcb(Foo.Bar,1);// no error as expectedcb(Foo.Baz,1);// error as expectedcb(Foo["B A"],true);// no error as expectedcb(Foo["B A"],1);// error as expected
I'm surprised that non-identifier enum members are even allowed, since they map (in my mind) to constants exported from namespace/modules. I do see that #40679 was a fix specifically to support an issue with non-identifier enum members, so they are definitely supported.
Types and values can reuse the same names without ambiguity because they exist in different contexts; there's no confusion (for the compiler anyway) between type D = Date and const d = Date even though the name Date refers to different things in the two cases (the instance interface vs the constructor value). And so you can write interface X { a: string } and const X = 123; without any collision.
But while the compiler is happy with this state of affairs, it seems to thoroughly confuse human beings! Especially because class and enum declarations introduce both named values and named types which are related to each other in a consistent way. And this consistency is enough to form a pattern in the minds of developers. And then people write typeof T or obj instanceof T and get confused when the compiler complains that T is only a type and not a value. Oof, and then there's the fact that there's both a typeof runtime operator in JavaScript and a typeof type operator in TypeScript which do completely different things, and again people want to write (typeof obj === MyInterface).
When referring to a value, Foo.bar and Foo["bar"] are equivalent, which makes sense because that's how it works in JavaScript, and if you're writing Foo.bar or Foo["bar"] in a place that expects a value, there's a good chance it's going to be emitted to JavaScript.
But when referring to a type, there is a potential naming conflict. TS has decided that Foo.bar refers to a namespace named Foo with an exported type named bar, while Foo["bar"] refers to a type named Foo with a property key named bar. If you could write them interchangeably, then there would be an ambiguity as to which type Foo["bar"] or Foo.bar was referring to. This restriction solves the problem for the compiler, but human beings tend to get tripped up.
In the enum here, the types Foo.Bar and typeof Foo.Bar are typeof Foo["Bar"] are equivalent, because that's how enums work. The first one means "The type named Bar in the namespace named Foo" while the latter two mean "the type of the value Foo.Bar/Foo["Bar"]". There is absolutely no way to write Foo.B A Z since "B A Z" is not an identifier, so the only way you can get this type is to write typeof Foo["B A Z"] or (typeof Foo)["B A Z"].
Reacted by Ryan Cavanaugh, AlCalzone and Henry Gabryjelski
Reacted by AlCalzone, Taly Hotimsky and Henry Gabryjelski
Activity
RyanCavanaugh commentedon Mar 9, 2021
Foo["B A Z"]refers to a property calledB A Zon a value of typeFoo, but there isn't one. A value of typeFoois a number.AlCalzone commentedon Mar 10, 2021
Ryan Cavanaugh (@RyanCavanaugh) so there is no way to do this
but with enum members instead of the hardcoded number literals?
And at least the auto-completion in VSCode is broken - should I raise a separate issue for that?
RyanCavanaugh commentedon Mar 10, 2021
This can't work as hoped for due to the all-numbers-are-assignable-to-enums rule
We can take a separate bug on the completion list, yes
AlCalzone commentedon Mar 10, 2021
Ryan Cavanaugh (@RyanCavanaugh) Sorry but I need to come back to this again. I must have confused something while creating the repro yesterday. If I use the enum values instead of numbers it works exactly as I'd expect:
Playground
The only weird part about this is the stray
typeofwhich is necessary in the third overload for a reason I don't quite understand.jcalz commentedon Mar 10, 2021
Duplicate of #25083 and the converse of #30267.
I'm surprised that non-identifier enum members are even allowed, since they map (in my mind) to constants exported from namespace/modules. I do see that #40679 was a fix specifically to support an issue with non-identifier enum members, so they are definitely supported.
Types and values can reuse the same names without ambiguity because they exist in different contexts; there's no confusion (for the compiler anyway) between
type D = Dateandconst d = Dateeven though the nameDaterefers to different things in the two cases (the instance interface vs the constructor value). And so you can writeinterface X { a: string }andconst X = 123;without any collision.But while the compiler is happy with this state of affairs, it seems to thoroughly confuse human beings! Especially because
classandenumdeclarations introduce both named values and named types which are related to each other in a consistent way. And this consistency is enough to form a pattern in the minds of developers. And then people writetypeof Torobj instanceof Tand get confused when the compiler complains thatTis only a type and not a value. Oof, and then there's the fact that there's both atypeofruntime operator in JavaScript and atypeoftype operator in TypeScript which do completely different things, and again people want to write(typeof obj === MyInterface).When referring to a value,
Foo.barandFoo["bar"]are equivalent, which makes sense because that's how it works in JavaScript, and if you're writingFoo.barorFoo["bar"]in a place that expects a value, there's a good chance it's going to be emitted to JavaScript.But when referring to a type, there is a potential naming conflict. TS has decided that
Foo.barrefers to a namespace namedFoowith an exported type namedbar, whileFoo["bar"]refers to a type namedFoowith a property key namedbar. If you could write them interchangeably, then there would be an ambiguity as to which typeFoo["bar"]orFoo.barwas referring to. This restriction solves the problem for the compiler, but human beings tend to get tripped up.In the enum here, the types
Foo.Barandtypeof Foo.Bararetypeof Foo["Bar"]are equivalent, because that's how enums work. The first one means "The type namedBarin the namespace namedFoo" while the latter two mean "the type of the valueFoo.Bar/Foo["Bar"]". There is absolutely no way to writeFoo.B A Zsince"B A Z"is not an identifier, so the only way you can get this type is to writetypeof Foo["B A Z"]or(typeof Foo)["B A Z"].RyanCavanaugh commentedon Mar 10, 2021
jcalz has it all correct; I don't have anything to add π
jcalz commentedon Mar 10, 2021
Can I quote that for my rΓ©sumΓ©/epitaph?
typescript-bot commentedon Mar 12, 2021
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.