The concept of enumerables is quite simple: an item that can represent a finite number of different things. However, the implementation in languages are very different. Some languages like Rust and F# allow enum branches to be containers other types. Most of them compile away the enums to simple numbers without leaving a trace of their use in the compiled code. TypeScript... doesn't really do well in comparison.
Consider the following enum:
enum EnumImplementations {
Rust,
FSharp,
TypeScript,
}
console.log(EnumImplementation.TypeScript);
Transpilation to JavaScript will yield the following result:
var EnumImplementations;
(function (EnumImplementations) {
EnumImplementations[EnumImplementations["Rust"] = 0] = "Rust";
EnumImplementations[EnumImplementations["FSharp"] = 1] = "FSharp";
EnumImplementations[EnumImplementations["TypeScript"] = 2] = "TypeScript";
})(EnumImplementations || (EnumImplementations = {}));
console.log(EnumImplementation.TypeScript);
This is a lot of complex code just to give more meaning to three numbers. Even worse, many tree shaking strategies will leave it as is.
What we would actually need for TypeScript to emit (used JSDoc for the type definition):
/**
* enum EnumImplementation
* @typedef {(typeof _EnumImplementation_Rust | typeof _EnumImplementation_FSharp | typeof _EnumImplementation_TypeScript)} EnumImplementation
*/
const _EnumImplementation_Rust = 0;
const _EnumImplementation_FSharp = 1;
const _EnumImplementation_TypeScript = 2;
const _EnumImplementation_Keys = ["Rust", "FSharp", "TypeScript"];
console.log(_EnumImplementations_TypeScript);
Not only has that the advantage of being easily tree-shakable, it is also more readable and concise.
That was a long rant, sorry. What's your take on the issue?
Disclaimer: please don't misunderstand me, I really like TypeScript, but this is one of its shortcomings that rather irks me.
Top comments (52)
I always use const objects instead of enums
In ts how do you add the enum to the declaration?
Like this? Or is there a better way?
You could use
const enums
which are removed during compilation. For example, this code:compiles to:
Playground
So the enum value (0,1,2...) is inlined into the code during compilation.
However, I avoid enums completely because I don't see a benefit in using enums vs. plain objects defined with
as const
:Sure, it's more verbose, but since these are normal javascript object, I can easily transform them in other shapes and they also work really well in types and type constraints. Here is some more background on this: TypeScript: Objects vs Enums
const enums are fine if your code is self-contained, but they are certainly not portable.
I agree. TS doesn't provide enums in a way that aligns with other languages. A
const
object comes closer. You can also define a utility type as a list of constants, e g ,type Color = 'red' | 'yellow' | 'green';
But you then miss out on the features offered by modern languages like you must handle all the cases in your switch statement etc.
I don’t know what you mean?
What they mean is that when you have an enum, the IDE (and some languages) can alert you if you are not using up all possible branches in a switch. Languages like Kotlin will break if you don't offer all branches of the enum in the switch and didn't add a default branch. Ideally, in these languages, instead of adding a default, you should add all the other branches together instead of a default, so if in the future the enum receives a new item, the compiler won't let you proceed unless you check every use of that enum in switches to see if they need a special treatment for that new item.
This type of behaviour breaking at compilation time is one of the reasons some people don't like using builder patterns. The builder will never tell you that you need to add another attribute. Which isn't an issue when you are the sole developer, but it's an issue when you don't know how the next dev will deal with this. We have had this problem more than once in my company. We depend a lot on Lombok to make Java a little more bearable, but the builder pattern, although very useful and satisfying, isn't dummy safe
You could also make sure all branches are covered with a union type, so that point is invalid.
I'd honestly not looked at what it compiled to, I'd always imagined it was something like this.
Which feels a bit more natural to me.
You won't need to freeze the object if you assert that it is read only on type level.
Still, the unnecessary level of complexity irks me. Even worse, those types are not even portable. If you re-export them, they may get loaded from two different locations and stop being compatible with one another, even though they are the same type.
The freezing is probably just to prevent modification at runtime instead of treating it as a normal variable, with object keys that could be mutated.
They could also unfreeze the object. If they already violate the types, why would they stop there?
The good guy Matt Pocock talked about this. The best way yet is to do
notice the
as const
part, which translates toObject.freeze()
. The type is very convenient, you could create a generic type to abstract this a little bit and this "enum" is even a little bit more flexible than Typescript's enums. I've been using this instead of enums for the last 6-9 months now, I'll never use enums again until they're standardized in JSAm I the only one who finds this self proclaimed TypeScript expert insufferable? Especially when he talks confidently about stuff he has very little or no practical knowledge what so ever.
If there is something we can all agree it’s that the 2 most important qualities of good code are: 1. it does exactly what it’s supposed to do and 2. it’s easy to read and understand by anyone with basic or even no understanding of the subject.
Your example is the exact opposite of these 2 principles. Might be something you want to consider before blindly trusting “tech influencers”
I wouldn't call Matt a self-proclaimed TypeScript expert, because that would just be an ad hominem that would reflect badly on me rather than on him.
I think good code is a bit more nuanced: the attributes that qualify good code in my understanding are functionality, maintainability and removability. So it has to work, needs to be maintained (which means it is written for humans and represents the least possible complexity required) and has the weakest possible coupling to the rest of the code it is intended to be used with.
Let's look at Matt's example. Does it work? Certainly. Is it written for humans to read and maintain? Mostly. The const assertion and the type are written to satiate TS - but they increase the maintainability. Does it represent the least possible complexity required? I would argue that my solution is slightly less complex, but that's only by a certain margin, so while I would not give him full marks, he still gets a passing grade from me. Has it the weakest possible coupling? The object can easily be replaced by any other objects; however, the identifiers still leak into the code and therefore remain coupled. I would fail him on that one, but two out of three isn't too bad at the end of the day.
So I wouldn't say it is the exact opposite of the best possible solution, because that's what TS enums are at the moment.
It does look like you enjoy writing complex things, I will give you that. Your explanation why this is a reasonable solution is almost as confusing as the original suggestion. I can give you A for effort, but the rest still makes no sense.
In the real world it's hard to justify the use of 2 statements (object definition and type inference) to solve only half of the problem enums are designed for. I mean it's in the name, enumerable... How is the object or the type enumerable? Oh yeah, you can write
Object.keys()
to achieve that. Congrats now you have 3 statements to solve the problem and let's not talk about dependencies between them and how you need to manage 3 separate references in your codebase. Fascinating! You were able to extrapolate a single simple solution into a 3 dimensional problem. Wow. I guess things like performance and scalability (multiplying every enum by 3) are out of scope. That's how you reach the point in the conversation where you need to ask the question "What are we actually trying to do?" and hopefully come to realization that maybe just maybe you need to get out of the rabbit hole.You seem to confuse enumerable with iterable. The latter is not necessarily a property of enums, even though it is nice to have it.
Also, you seem to have misinterpreted my intention. I had hoped tsc could be made to restore the original enum from the type information so it could be made portable beyond just being exported as multiple constants.
I am aware that this is still not an ideal solution, but it is currently the best we can do without creating a new TC39 proposal that does away with the extra complexity.
Lastly, your opinionated and fallacy-laden discussion style detracts from your criticism.
@cmacu I agree you're creating more references in the code with Matt's suggestion, but that still is the worst thing it's happening for me, nothing worse. Moreover, you're just creating a simple and friendly JS literal object in the transpiled code, as the type goes away, while the native "enum" approach creates some ugly IIFE with double the declared properties in the enum instead (check TypeScript Playground's compiled JS panel on the right).
I think we can all agree that native enums would be much better and solve the problem of having a somewhat weird syntax with
as const
like this, but for now that is the "most performant way" and also the most JS-friendly way I'd say.Marginally, enums declared like this both provide the benefit of typing, avoiding literal values in code but also you can use its type in a little more forgiving way. Take this for example
Notice that I can call
selectLanguage()
with both a literal value (maybe coming from external sources like an HTTP request or database) and with the pseudo-enum without being forced to the use real enum, when using the enum as a type. For me, it's a nice thing to haveOK, I had to a double take and according to all definitions I found enumerable means exactly what I was referring to. The ability to be counted/named in orderly fashion (one by one). Which indeed is a subset of iterable given that there are various ways to iterate over collection and is supported by many other data structures. But back to enums. I don't see the fallacy in the statement that neither object as const nor the inferred type fully supports the simple and much needed functionality of enums. I am glad that we both agree on that. Also I don't understand how ideal solutions, TC39 proposals, etc are relevant in a discussion of comparing 2 solutions aiming to solve a basic problem. We could as well just employ other languages or tools for that purpose, which is way beyond the point. I simply stated that the solution you are referring to (and which was suggested by a third party with no credentials) is a non starter. I provided basic and solid justifications to which you responded with dismissive statements and unrelated tangents.
And to make things clear. I think the issues developers are experiencing with enums are simply due to incorrect usage. Often the discussion involves hypothetical scenarios only possible due to fallacy-laden decisions. Enums are widely used without an issue in every major open source project including the TypeScript Compiler and VSCode which are directly supported by Microsoft. Are there trade-offs and concerns? Of course! Give me example of anything that doesn't have issues...
This becomes a real problem when people on the internet start advocating for alternative solutions which don't solve the problem, add additional complexity and have not been battle tested in large codebases. Especially when that's seldomly done just because its promoted by influencers for the purpose of demonstrating expertise as an ability to understand or create complexity. In other words posting BS online and others spreading it...
Nobody claimed their solution was perfect, just less bad than the one currently existing in TS. This is the original point of this discussion. In most languages, enums do not fulfill your definition without extra work. JS does not even support them as part of the language.
Both proposed solutions are clear improvements over what TS currently delivers. That is the point of both them and this post. Your points, while being valid out of context, reframe the whole discussion and would be more apt in a discussions how native ECMAscript enums should look like.
If you don't understand that much, I have nothing more to gain from this discussion and will stop answering.
What is bad about TS enums? If you the transpiled code bothers you you can use string enums. String enums transpile to basic objects. What kind of argument is "JS does not even support them"? JS does not support
as const
either, nor a bunch of other things. So what? Let's make this easy and simple: What is the problem specifically with string enums? Please provide a reference, not an opinion but a real reference/example of string enums being bad or having issues. You want to keep things objective and make me understand. Give me a proof of your claims without shifting the subject.It's you who is shifting the subject. This was decidedly not about string enums. If I wanted to use strings, I'd just use them and a union type, because strings provide meaning themselves. The main point about enums in TS is that they allow you to assign meaning and structure to otherwise meaningless numbers. You may lament they don't do more than that, but that's again you trying to evade the topic, then.
Ok, so looks like you are saying that enums in TypeScript are doing exactly what they are supposed to do. I didn’t see any arguments why enums are bad. Give me a reason why enums are bad and I should not use them and look for alternative solutions that have their own issues.
I mentioned the issues portability and tree shaking. I don't say you shouldn't use them, just that their implementation is worse than it should be.
The
as const
solution doesn't support tree shaking either. Nor even the individual object keys. If you import the type, the object definition has to be imported too, because it's dynamic. Same with any exported iterable value... And how is that more portable? Which part is exactly is portable in your solution?As previously said. Everything has trade offs. Everything. Calling it bad and posting online how people should stop using it is BS. Especially when the proposed solution is at least as "bad" and comes with it's own can of worms. For someone claiming decades of experience in web development you should know better than making such claims.
Can you show me example in a major open source codebase (or your own) where anyone uses
as const
and type inference exclusively instead ofenums
?As const needs no tree shaking, but has zero portability. My solution does, but lacks extensive support by tsc.
Nobody uses as const in major code bases because it is not portable. However, I've seen multiple code bases that consciously omitted the use of enums for the reasons I have named.
Which codebase has omitted the use of enums? Can you send me some references? I am curious to learn the reasoning as well as what solution they went with.
I’ve been searching unsuccessfully for examples for years at this point. I’ve tried some alternative approaches too and I always come back to enums. But I also really dislike “magic string” solutions so there’s that.
Sorry, all closed source products. I prefer "magic numbers" (numeric variables with speaking names) over "magic strings", but whenever I encounter enums in external libraries I'm working with, I avoid re-exporting them and instead add a JSdoc comment pointing out the correct import out of courtesy for the next poor sod who has to use them.
This is true for enums by default in TypeScript, but I typically opt for const enums instead.
My method is lengthy, but also arguably the most powerful:
Note: in all of my code bases I do not use TypeScript's built in enums for aforementioned issues, and other minor implementation inconsistencies. The above method is more powerful as it allows for inline enum variant selections (instead of
EnumImplementation.Rust
you can just put0
, and so on) and doesn't feature any other hidden traps during runtime.Oh yes. I found out my favourite optimization (from C), that makes the code more effective by replacing strings with numbers (number comparison is always faster than eg. string), is not an optimization at all in Typescript.
So I use constants.
I discourage the use of enums whenever possible for a few reasons, them being unnecessary as the primary one. Typescript really doesn't need them and depending on the "guarantees" you can make about a code base (immutability etc.) something like defining an object as const and then inferring the types from that is a lot more practical
Hi Alex Lohr,
Top, very nice and helpful !
Thanks for sharing.
I always thought they were syntax sugar for constants. I guess I'll use more true constants then.
Typescript enums are deprecated and should not be used anymore.
Const objects are the way to go in typescript/javascript.
If you need 1 or more value mappings you use:
Typescript enums aren’t deprecated. Can you share me an official statement from typescript on it? They are only discouraged for use by one side of the community, there are lovers for enums too.
Even on the official typescript website it lists problems, pitfalls using enums (instead of using const objects)
typescriptlang.org/docs/handbook/e...
and it does not even touch frameworks, babel, and other build tools that all had (or still have) problems with typescript enums.
If you run your code everywhere besides your own dev machine you have to care about how it gets build, deployed and run by the user.
If you have 2 possible solutions, one of which is completely problem free and the over is fraught by countless pitfalls -> it may not be officially sunsetted, but is a bad choice to make in any case.
Even official maintainers/creators of typescript regret the creation of enums.
Hence my TLDR summary: enums are deprecated
On the official docs it only lists pitfalls with const enums, which do not apply to enums generally. And even the const enum pitfalls, as it clearly says, only apply if you're emitting or consuming d.ts files.
What problems with frameworks or build tools are you referring to exactly? Personally in years of working with typescript and many different frameworks, and some pretty wild build pipelines, I don't think I recall ever having any issues related to enums specifically.
Sure but what does this have to do with ... enums? How would enums even have any effect on how your code gets deployed and ran by the user
Again, what "countless pitfalls" are you talking about...
Citation needed
Well you're wrong though. They're not deprecated.
It may have problems, yes, but as far as I know it's not deprecated. And officially deprecated is the only kind of deprecated. You could say it's a bad practice to use it in your point of view, but you can't say it's deprecated. Deprecation is a way to say "this should be done differently and may be removed in future versions". I don't see that movement from Typescript.
similar article
dev.to/ivanzm123/dont-use-enums-in...
The first point here is just plain wrong, or maybe outdated — numbers are NOT accepted for numeric enum arguments. Give it a try on the TS playground right now.
The second argument makes no sense. If you use an enum as an arg type, you can't pass a string... Yeah, of course. Because that's the whole point of enums. You can change the string value of an enum member once, and all the literals change everywhere.
I have taken the code directly from the TS playground. And I'm afraid you misunderstood my second argument. It's not about the tooling, but about the resulting code.
Hey, my comment wasn't about your article, but the one linked in the comment I replied to.