Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String Enum Initializer #16464

Open
thomasmost opened this issue Jun 12, 2017 · 38 comments
Open

String Enum Initializer #16464

thomasmost opened this issue Jun 12, 2017 · 38 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@thomasmost
Copy link

thomasmost commented Jun 12, 2017

String Enum Initializer

A more typesafe and succinct way of defining enum types that compile to strings

Search Terms

enum, string, string enum
default value
initializer
infer

Proposal

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)

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jun 12, 2017
@imcotton
Copy link
Contributor

like this?

enum Action: string {
    LOAD_PROFILE,
    ADD_TASK,
    REMOVE_TASK,
}

@thomasmost
Copy link
Author

That would be perfect!

@pradyuman
Copy link

This would be super great - any updates on this?

@lostpebble
Copy link

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,
}

@thomasmost
Copy link
Author

yup, I'm about this

@thomasmost
Copy link
Author

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...

@RyanCavanaugh
Copy link
Member

The status on this is the same as it was before

@thomasmost
Copy link
Author

thomasmost commented Jun 5, 2018

@RyanCavanaugh What additional Feedback are you looking for (referencing 'Awaiting More Feedback')?

@RyanCavanaugh
Copy link
Member

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).

@lostpebble
Copy link

lostpebble commented Jun 6, 2018

@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.

@RyanCavanaugh
Copy link
Member

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:

  • Does this enable you to do something you couldn't do before?
    • No - it's pure sugar
  • If not, is the existing way of doing it truly burdensome, or just a bit annoying?
    • It's just annoying
  • How many people are affected by the feature not existing?
    • The answer appears to be 13ish?
  • Could this be solved in some other way, such as tooling?
    • "Convert numeric enum to string enum" refactoring would be trivial to write; we should consider doing that instead
  • Is there strong precedent from other languages implying a need here?
    • No
  • Are there frameworks or libraries whose patterns are greatly simplified by it?
    • No (?)

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".

@thomasmost
Copy link
Author

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.

@icholy
Copy link

icholy commented Jun 6, 2018

@thomascmost what's your use-case?

@thomasmost
Copy link
Author

@icholy I like using string enums to define my action sets for NGRX/Redux features

@icholy
Copy link

icholy commented Jun 6, 2018

@thomascmost why don't you use regular enums?

@thomasmost
Copy link
Author

thomasmost commented Jun 6, 2018

Regular enums throw a type error because Redux actions expect a string type

@icholy
Copy link

icholy commented Jun 6, 2018

@thomasmost
Copy link
Author

Interesting... maybe I'm thinking of NGRX, or I'm otherwise misinformed. I'll give it another shot

@icholy
Copy link

icholy commented Jun 6, 2018

@thomascmost looks like NGRX does expect a string, but I don't see why that couldn't be extended to string | number

@thomasmost
Copy link
Author

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?

@zajrik
Copy link

zajrik commented Jun 7, 2018

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.

@tjfryan
Copy link

tjfryan commented Jan 10, 2019

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

@silkentrance
Copy link

silkentrance commented Feb 14, 2019

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 = {}));

@silkentrance
Copy link

silkentrance commented Feb 14, 2019

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
}
(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 = {}));

@thomasmost
Copy link
Author

thomasmost commented Feb 15, 2019

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

@silkentrance
Copy link

silkentrance commented Feb 16, 2019

@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.

@thomasmost
Copy link
Author

thomasmost commented Feb 18, 2019

@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.

@lostpebble
Copy link

lostpebble commented Feb 19, 2019

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.

@bgracie
Copy link

bgracie commented Feb 19, 2019

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.

@KennethKo
Copy link

+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.

@thomasmost thomasmost changed the title Sugar String Enum identifier Sugar String Enum Initializer Jan 21, 2020
@Kingwl
Copy link
Contributor

Kingwl commented Apr 29, 2020

Does it available to accepting pr whatever sugar or refactor?

@mattgaspar
Copy link

mattgaspar commented Jun 26, 2020

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...
}

@Barrior
Copy link

Barrior commented Oct 13, 2020

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.

@Kingwl
Copy link
Contributor

Kingwl commented Mar 2, 2021

Hey folks. 🤚
I Hope we could revisit this issue.

Recently I have spend about 3 mins on this again.
And In some of my offline feedback, the developers (Who always use TypeScript Enum) wanted features like this that would reduce some of the work.

Currently:

Anyway. I hope we could improve the experience on this one.

Thanks!

@thomasmost thomasmost changed the title Sugar String Enum Initializer String Enum Initializer Mar 2, 2021
@thomasmost
Copy link
Author

As others have pointed out, this isn't just sugar—it has fundamental type safety, usability, and behavioral implications. I've renamed the issue to reflect this.

Thanks to everyone (maintainers and commenters) for your support and patience; modifying languages is a lengthy process!

@hardfist
Copy link

It's really helpful when you write an big enum contains tons of value such as compiler TokenType

@AlexDEVpro
Copy link

This would be a super feature when StringEnumConverter is used on the web API side, for example, for API error codes enum.

@thomasmost
Copy link
Author

@RyanCavanaugh Is it still your opinion that this issue does not merit attention?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.