A more typesafe and succinct way of defining enum types that compile to strings
enum, string, string enum
default value
initializer
infer
It seems like you should be able to specify that you are using a string enum, and then you'd only have to write out the 'value' once, and it would be automatically made into a string matching the enum code.
enum<string> Action {
LOAD_PROFILE,
ADD_TASK,
REMOVE_TASK
}
or
enum Action: string {
LOAD_PROFILE,
ADD_TASK,
REMOVE_TASK
}
instead of
enum Action {
LOAD_PROFILE = "LOAD_PROFILE",
ADD_TASK = "ADD_TASK",
REMOVE_TASK = "REMOVE_TASK"
}
Thanks for all the great work!
EDIT
Recently updated to reflect variations proposed by @imcotton here and @lostpebble here; as well as search terms proposed by @KennethKo (#36319) and @garrettmaring (#33015)
like this?
enum Action: string {
LOAD_PROFILE,
ADD_TASK,
REMOVE_TASK,
}
That would be perfect!
This would be super great - any updates on this?
Yep, this would be a really great addition.
See this comment on the original pull request which brought string enums to Typescript.
Also mentioned in that thread was the syntax of enum<T = number>
to describe enums. I think this syntax works quite nicely. So for strings we might have:
enum<string> TaskIds {
TASK_RESIZE_IMAGE,
TASK_ADD_PROFILE_PIC,
TASK_DELETE_USER,
}
yup, I'm about this
Hey, can we get an update on this? TypeScript 2.9 just came out and honestly it's a little frustrating that we've gone 4 minor releases without any progress reported to this ticket...
The status on this is the same as it was before
@RyanCavanaugh What additional Feedback are you looking for (referencing 'Awaiting More Feedback')?
This label means we'd like to hear from more people who would be helped by this feature, understand their use cases, and possibly contrast with other proposals that might solve the problem in a simpler way (or solve many other problems at once).
@RyanCavanaugh This is pretty much as simple as its gonna get. We'd just like a way to define string enums without having to repeat everything. I'm not sure what other feedback you require to push through a new feature? Is there just perhaps not enough activity pushing for this?
I think the team should maybe weigh in a bit on what they think on the current proposal, and if there are issues we can move on from there.
I do foresee a slight issue with what I wrote earlier in the thread:
enum<T = number>
This won't interact nicely with the current way string enums work, and I'm sure whichever method is chosen it would have to be backwards compatible.
number
being the default technically means this would be possible with the current implementation:
enum<number> {
SOMETHING = "SOMETHING",
ELSE = "ELSE"
}
Also, it seems like because of the way its currently implemented that using the enum<T>
method is a bit difficult and might be ambiguous - because if someone happens to change enum<string>
to just plain enum
, the actual data type will change completely but it might not be very obvious.
But would be nice if the team could weigh in on a way they might envision this as well.
We're always looking at things holistically. There are over 1,000 open suggestions and it would be a complete disaster to just do all of them; features need to more than pay for themselves in terms of new complexity vs value added.
We have to consider questions like:
The bar is much higher than you probably think it is - once something goes into the language, we can't take it out, and everything that exists is something that future TS learners will need to understand. Features at the language level need to feel like "the thing we've been missing all along", not "this would be nice".
I want to emphasize that I understand and agree with everything you're saying; I don't expect TypeScript to change at the whim of a few users, however I think some of your thinking is overly dismissive.
For example, you say that there is "no strong precedent from other languages implying a need," but I would argue that this is inherent to the concept of an enum. Having to define each state of the enum twice, and potentially introducing conflicts where two enum values correspond to the same string, flies in the face of an enum's existential purpose. From that perspective, this isn't just annoying; it's a fundamental flaw in the initial implementation.
Once again, I completely understand the challenge of maintaining a language used by thousands in a scalable way, but please remember that we're all here because we love TypeScript and we should be working towards the best solution together.
Speaking of which, you say that, "'Convert numeric enum to string enum' refactoring would be trivial to write; we should consider doing that instead" -- but I'm not 100% sure what you mean by that. Is this something we can do in our own codebases? Do you mean just adding a post-transpile hook that changes all the enum values to strings?
Thank you again for all you do.
@thomascmost what's your use-case?
@icholy I like using string enums to define my action sets for NGRX/Redux features
@thomascmost why don't you use regular enums?
Regular enums throw a type error because Redux actions expect a string type
Interesting... maybe I'm thinking of NGRX, or I'm otherwise misinformed. I'll give it another shot
@thomascmost looks like NGRX does expect a string, but I don't see why that couldn't be extended to string | number
Okay, but then I think there's another problem:
If I have an AdminActions
for my admin feature and a RsvpActions
for my rsvp feature, defined as such:
enum AdminAction {
MAKE_USER_MANAGER,
REVOKE_USER_MANAGER
}
enum RsvpAction {
RSVP_TO_OCCURRENCE_REQUEST,
RSVP_TO_OCCURRENCE_SUCCESS
}
...in this case, wouldn't my reducers misfire for both MAKE_USER_MANAGER and RSVP_TO_OCCURRENCE_REQUEST, since they both transpile to a value of 0?
I can definitely see the problem, there. String enums afford the opportunity for unique values while giving us that enum intellisense we all want. My use case for this would clean up a fairly large enum of localization keys.
https://github.com/yamdbf/core/blob/master/src/localization/BaseStrings.ts
Granted, I generate this enum via a runtime script to prevent me from forgetting to add any new strings I may have added so it's no hassle on my part to maintain, but it would definitely look cleaner, and make it less of a hassle if I were to add any new keys manually.
For redux, having string types is valuable because it makes it easier to debug from logs. In an app with over a hundred actions, looking at an action with type PRESSED_THE_RED_BUTTON
is a lot more convenient than 78
My counter proposal would be to add an additional fromString method to the generated enum, e.g.
var Enum;
(function (Enum) {
Enum["FOO"] = "abracadabra";
Enum["fromString"] = function (s) {
// pseudo code here
for each key in Enum {
if key === s then return Enum[key];
if Enum[key] === s then return Enum[key];
}
return undefined; // or throw new Error('undefined literal');
}
})(Enum || (Enum = {}));
An alternative would be to streamline string based enum literals with the standard literals plus a fromString method, e.g.
enum Enum {
FOO = "abracadabra",
BAR = 0
}
var Enum;
(function (Enum) {
Enum[Enum["FOO"] = "abracadabra"] = "FOO";
Enum[Enum["BAR"] = 0] = "BAR";
Enum["fromString"] = function (s) {
for (var key of Object.getOwnPropertyNames(Enum)) {
if (typeof Enum[key] === 'function') continue;
if (key === s) {
if (Enum[Enum[key]] === s) { return Enum[key] } else { return Enum[Enum[key]]; }
}
if (Enum[key] === s) { return s; }
}
return undefined; // or throw new Error('undefined literal');
}
})(Enum || (Enum = {}));
I would like to follow up on my previous comment with a concrete example:
This:
enum September {
Earth,
Wind,
Fire,
}
enum Elements {
Hydrogen,
Helium,
Oxygen
}
console.log(September.Earth === Elements.Hydrogen);
...has a nice little TypeScript error that says:
This condition will always return 'false' since the types 'September.Earth' and 'Elements.Hydrogen' have no overlap.
...but it logs true
.
The TypeScript error, while it would presumably prevent compilation, is actually incorrect. Someone who was playing fast and loose with casting might run into issues, and the Redux problem is illustrated clearly. Earlier, I said to @RyanCavanaugh :
From that perspective, this isn't just annoying; it's a fundamental flaw in the initial implementation.
...and while this is in some sense a separate issue I would argue it's related, especially for the use-case I conveyed to @icholy
EDIT
For a point of reference, the documentation on the Enum.Equals Method in .NET:
https://docs.microsoft.com/en-us/dotnet/api/system.enum.equals?view=netframework-4.7.2
@thomasmost The problem with this is that during runtime, all type information has been erased except for tests of instanceof and similar such mechanisms using for example reflect-metadata. And, also given the fact that TS will impose static type checking on the enum only, it is hard to figure out of what type the enum actually is.
In the past, others and I have come up with a more type safe solution, however, this will require additional runtime overhead and I am not sure whether the TS team is willing to introduce a base enum class from which the custom enum is then derived from, making all literals instances of that class. That being said, such an approach would also allow for custom, both static and dynamic methods, and constructors alike, very similar to the one we can see in Java, or maybe even C#.
Have a look at for example https://github.com/vibejs/enumjs/blob/master/src/lib/http/EHttpStatus.coffee.
Also see http://2ality.com/2016/01/enumify.html.
I think that my approach is actually better, however, it requires translation to typescript and one cannot use the reserved keyword enum when declaring such enums.
@silkentrance This is cool -- I quite like leveraging Symbol
as a solution here, and will check out enumify
as well.
Thank you for the background/context. I understand that one advantage of TypeScript's enum implementation is runtime performance since they get compiled down to pure strings or numbers.
Essentially, I just wanted to make the assertion that adding support for enum<string>
would improve the type-safety and usability of enums (for certain use-cases, especially) without compromising on runtime overhead.
Edit One can even conceive of a world where enum<symbol>
automatically compiles each item in the enum to Symbol(...)
; that would provide really strict type safety for individuals who wanted it.
Still would really love to see this handled better by Typescript. Not only because it is "sugar" but because it can help prevent data errors (which with enums can get pretty messy quickly).
For example:
enum EResponseStatus {
OKAY = "OKAY",
WARNING = "WARING",
ERROR = "ERROR"
}
const code = EResponseStatus.WARNING;
Firstly, if I miss-typed an enum value, like I did WARNING
there (the way I do it now to prevent this is copy paste the enum name on the left - the annoyance of this has already been spoken about). But this could happen through regular use, as people interact with code in different ways - and I still type them out sometimes.
Now because the enum name is not tied to the value, the chances of me seeing this mistake are probably not high - until some actual data would have made use of the incorrect value (and this error would most likely happen outside of my current Typescript program and local tests - which is why its a more critical problem in my eyes).
Secondly - if I refactor one of them in my IDE (Webstorm- though I'm sure it'll be similar in others), we can end up with enums like so:
enum EResponseStatus {
SUCCESS = "OKAY",
WARNING = "WARING",
ERROR = "ERROR"
}
Which is completely wrong - as they should both be renamed to SUCCESS
.
Basically these problems stem from Typescript not giving us a nice native way of defining string enums and being strict about tying those values together. There should be a way to do so and get all the great Typescript safety that comes with that.
At the risk of pointing out the obvious, the current way of doing string enums also introduces quite a bit of line noise. Given that types are more likely than other sections of the code to act as documentation, there is definitely a penalty there.
+1.
Insofar as string enums are useful at all (readability in logs and on the wire), their concise definition is also important. Forcing people to enter the reserved value twice is the opposite of basic DRY.
Does it available to accepting pr whatever sugar or refactor?
This would be really useful for ambient enums. In my case the code running in the cloud injects an enum like object with string values.
In my d.ts file when I use:
export declare enum Type {
SALES_ORDER,
INVOICE,
200 more...
}
There is no way for me to say that the enum contains string values, it is automatically a number and I get type errors when comparing it to a string.
This syntax would be really useful in this case
export declare enum<string> Type {
SALES_ORDER,
INVOICE,
200 more...
}
Hi, Could I make a suggestion, I think it's useful. 馃憞
Desired syntax:
enum Grades {
SILVER,
Gold,
pt,
keys,
}
console.log(Grades.SILVER) // 0
console.log(Grades[0]) // 'SILVER'
console.log(Grades.keys) // 3
console.log(Grades.keys()) // ['SILVER', 'Gold', 'pt', 'keys']
console.log(Grades.values()) // [0, 1, 2, 3]
console.log(Grades.map()) // { SILVER: 0, Gold: 1, pt: 2, keys: 3, '0': 'SILVER', ... '3': 'keys' }
Keep present syntax and add new methods of keys
, values
and map
, like Java, but I don't know how to compile the syntax to run in JavaScript source code. (keys
property and keys()
method may conflict)
String Enum:
enum Grades: string {
SILVER,
Gold,
pt,
keys,
}
console.log(Grades.SILVER) // 'SILVER'
console.log(Grades.keys) // 'keys'
console.log(Grades.keys()) // ['SILVER', 'Gold', 'pt', 'keys']
console.log(Grades.values()) // ['SILVER', 'Gold', 'pt', 'keys']
console.log(Grades.map()) // { SILVER: 'SILVER', Gold: 'Gold', pt: 'pt', keys: 'keys' }
Mixed:
enum Grades: string {
SILVER = 2,
Gold,
pt = 'platinum',
keys,
}
console.log(Grades.SILVER) // 2
console.log(Grades[2]) // 'SILVER'
console.log(Grades.keys) // 'keys'
console.log(Grades.keys()) // ['SILVER', 'Gold', 'pt', 'keys']
console.log(Grades.values()) // [2, 'Gold', 'platinum', 'keys']
console.log(Grades.map()) // { SILVER: 2, Gold: 'Gold', pt: 'platinum', keys: 'keys', '2': 'SILVER', platinum: 'pt' }
Thank you for all you do.
Most helpful comment
We're always looking at things holistically. There are over 1,000 open suggestions and it would be a complete disaster to just do all of them; features need to more than pay for themselves in terms of new complexity vs value added.
We have to consider questions like:
The bar is much higher than you probably think it is - once something goes into the language, we can't take it out, and everything that exists is something that future TS learners will need to understand. Features at the language level need to feel like "the thing we've been missing all along", not "this would be nice".