One thing I'm missing in the comments here is that enums are a very early TypeScript feature. They were in there nearly from the start, when the project was still trying to find clarity on its goals and principles.
Since then:
- TypeScript added string literals and unions, eg `type Status = "Active" | "Inactive"`
- TypeScript added `as const`, eg `const Status = { Active: 0, Inactive: 1 } as const`
- TypeScript adopted a stance that features should only generate runtime code when it's on a standards track
Enums made some sense back when TS didn't have any of these. They don't really make a lot of sense now. I think they're effectively deprecated, to the point that I wonder why they don't document them as deprecated.
I think they also haven't gotten very much attention in the last few years as new features have been added. Nine times out of ten, if I hit a weird case where TS doesn't understand some type that it really seems like it should understand, it involves an enum. And if I rewrite the enum as a union type and update the other code that uses it, my issue goes away.
I think that's part of the underlying issue in every case, but then there sometimes seems to be some kind of bug where TS won't agree that the value actually has that nominal type, despite it originating from the enum itself. I usually then can't reproduce these issues with more minimal examples.
At least some of the time an enum doesn't agree with itself it's an import graph issue where the enum is getting imported from more than one place (perhaps because of multiple versions of a dependency in the middle) and the nominal typing is getting overly conservative that those things despite the same contents may be different things. I have a supposition that this is indirectly because what remains of nominal typing is Symbol types and Symbols do have to be extremely careful about import boundaries, especially when bundlers are involved.
They still make sense in terms of clarity, readability and reusability. I use enum every time there are more than 2 entries -- literal types and "as const" are just ugly in comparison.
Not to mention that you can add documentation to each of the entries.
I think the point is that enums aren't real in Typescript because enums aren't real in JS. They are fakes that generate a bunch of JS code that you are better off writing by hand (such as examples above and elsewhere, or with libraries like io-ts or Zod or other great options). They are fakes that exist mostly because of a desire for backwards compatibility with the type system of Typescript < 1.0, which also means they are increasingly out of touch with modern types.
No, I used very few words to try to make it clear that enums in Typescript are syntactic salt. Might look sweet from a distance, but it's doing the wrong thing in multiple ways and if you expect it to taste sweet you are in for a surprise. The code generation is ancient and wrong for modern JS. The types are ancient and wrong for modern TS.
People do love syntactic sugar. That's why it is important to point out why sometimes it is table salt and they should avoid trying to put it on their desserts.
> No, I used very few words to try to make it clear that enums in Typescript are syntactic salt.
They aren't. As others already pointed out, enums greatly simplify some usecases, although they brush the complexity of implementing them under the rug.
The complexity they brush under the rug is dangerous and has rough edge cases that have caused real damage in applications I and others have worked on. I'm trying to warn these others that think enums "greatly simplify" things that they don't really simplify things, especially by how much they can hurt. The modern alternatives are all just as simple, but far more reliable. Believe me or not, it is still a fair warning that enums aren't the syntactic "sugar" that they appear to be, they aren't as sweet as they look, and you may get a "salty" surprise (apologies if that sounds more vulgar than intended due to slang overloading) if you keep using them. You don't have to trust my warning on it (or any of the others in this and nearby threads), but that doesn't make it not worth making the warning and suggesting alternatives.
Yes, I've got eslint errors turned on disallowing any use of enums at this point, because I see it as a code smell/risk, but that doesn't mean I'm going to take away the feature from your codebase, that's up to you and your judgment.
can you name some use-case it actually would greaty simplify as you say?
Because neither TFA nor this thread has yet proposed anything that isnt better served by later introduced (say TS 3~4.x+) language features or even external libraries if you actually do want to use them for their generated runtime library (enums) and not just their type-aspects.
I have only seen them complicate things.
You could use it to make typescript prevent forking and code-duplication within a codebase of engineers you dont trust, I guess. But it doesn't seem like a great use either.
> You could use it to make typescript prevent forking and code-duplication within a codebase of engineers you dont trust, I guess. But it doesn't seem like a great use either.
Right, for that you are better off using unique Symbols. That's definitely the modern JS way to nominally type something in a private way that stays inside your API boundary and is very much more useful because it is also enforced at runtime, too, and not just an accident of old type mechanics.
Because they are nominally typed, which causes issues for users.
For example, if you already depend on package foo (depending on package baz@^1.0.1, resolving to 1.0.1) and then add a dependency on package bar (depending on package baz@^1.0.2, resolving to 1.0.3), then the same enums from package baz from the two transitive imports are not compatible, since they are not the exact same instance. So TS won't accept a baz enum returned from foo being passed to a function in bar expecting a baz enum. In this example you could fix it by letting the package manager "dedupe" your lockfile since foo could actually happily also use baz@1.0.3. But if the ranges are incompatible, your best hope is aligning resolutions/overrides. Or fall back to forking and patching packages.
And if you're writing your own library interfacing with baz enums, you have to include the full exact version of the originating package to get the right reference. So if baz also has 200MB of total dependencies, you can't opt out of those if you want to reference that 10-line enum in a function signature. As opposed to interfaces and const-types, which you can just vendor (copy-paste exactly what you want) and TS figures it out. You could break the type out to a subpackage. Not so with enums.
If you want to extend a union type, you can just add your own with the new element and it will still typecheck. You can not with enums. So you resort to encapsulation or ad-hoc conversion-functions, which gets frustrating and messy very quickly.
This is only a concern with enums (and classes, where there is good reason for it as the implementation does matter at runtime in a way that enum primitive values do not). The alternatives don't have this issue - as they are structurally typed, TS will "merge" them at type erasure.
If you are 100% sure that your enums stay private inside your module and are never exposed via references in public APIs, at least you're mitigating much of this. But why paint yourself into that corner? (at the point that readability of comments is a concern I suspect you need to reconsider)
I know it’s a bit of an extra bullshit, but in the situation you’re describing, can’t you just run the value through a type guard to make the two enums essentially interoperable?
Given Typescript has preferred opt-in strictness flags for its recommendations, the two big places that seem to be Typescript's best documentation of "deprecated" features seems to be:
Between the two of those flags, enums and namespaces and a few other things get disabled.
The flag documentation doesn't explain what the modern equivalents are, though. I suppose that's currently left to blog posts like the one linked here.
Does that mean I'm supposed to interpret "... oh wait..." as "it would be very easy for them to deprecate a feature because they have lots of sufficiently major releases"? Because it comes across as implying they wouldn't be able to do it.
I'm not trying to be cheeky here. They have literally joked about how TypeScript versions means nothing really. So they can't just announce a new major version and drop enums completely. Maybe with a feature flag this is possible but even then, a fresh tsc --init not supporting enums is not really how TypeScript works
Changing values (after a change in an external interface), tracking use and renaming is harder in the first case. In the second case, the code can change the value at runtime.
> Changing values (after a change in an external interface), tracking use and renaming is harder in the first case.
You can rename the elements of a string union with the typescript language server. In VS Code at least, it's just like renaming a variable, and it updates the usages which use the type.
> In the second case, the code can change the value at runtime.
You can always freeze the object if you're worried about that.
> Changing values (after a change in an external interface), tracking use and renaming is harder in the first case.
FWIW in VS Code I can rename a string literal (in the type definition) and it's renamed everywhere. Similarly I can use "Find All References", it just works. Pretty cool!
Since then:
- TypeScript added string literals and unions, eg `type Status = "Active" | "Inactive"`
- TypeScript added `as const`, eg `const Status = { Active: 0, Inactive: 1 } as const`
- TypeScript adopted a stance that features should only generate runtime code when it's on a standards track
Enums made some sense back when TS didn't have any of these. They don't really make a lot of sense now. I think they're effectively deprecated, to the point that I wonder why they don't document them as deprecated.