TypeScript enums: use cases and alternatives
https://2ality.com/2025/01/typescript-enum-patterns.htmlSince 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.
Enums is going to make your TypeScript code not work in a future where TypeScript code can be run with Node.js or in browser when typings are added to JavaScript[1]
Enums results in runtime code and in most cases you really want type enums. Use `type State = "Active" | "Inactive"` and so on instead. And if you really want an closed-ended object use `const State = { Active: 1, Inactive: 0 } as const`
All of the examples in the article can be achieved without enums. See https://www.typescriptlang.org/play/?#code/PTAEFEA8EMFsAcA2B...
FYI, this is now. Node 23.6 will just run typescript files than can have their types stripped https://nodejs.org/en/blog/release/v23.6.0#unflagging---expe....
There is a seperate --experimental-transform-types flag which'll also transform for enums, but no idea if they ever intend to make this not experimental or unflagged.
The only other twist to import syntax is marking type-only imports with the type keyword so that those imports can be completely ignored by simple type removers like Node's. You can turn that check on today in Typescript's compile options with the verbatimModuleSyntax [1] flag, or various eslint rules.
[1] https://www.typescriptlang.org/tsconfig/#verbatimModuleSynta...
https://github.com/tc39/proposal-type-annotations/commits/ma...
I'm able to do that just fine in VS Code / Cursor.
I set up a union like this:
export type TestUnion = 'foo' | 'bar' | 'baz';
Then use it in another file like this: const bar: TestUnion = 'bar';
const barString: string = 'bar';
If I select 'bar' from the type and choose "Go to references", it shows me the `const bar` line, but not the `const barString` line, which is what I would expect.How is that the conclusion you reach? The proposal you link says types will be treated like comments by the runtime, so it's not about adding types that will be used in the runtime (which begs the question, why even add it? But I digress), but about adding types that other tooling can use, and the runtime can ignore.
So assuming the runtime will ignore the types, why would using enums specifically break this, compared to any other TypeScript-specific syntax?
Great list of such features: https://www.totaltypescript.com/books/total-typescript-essen...
TS has a great type system, the rest of the language is runtime overhead.
type MyEnum = typeof MyEnum[keyof typeof MyEnum];
const MyEnum = {
A: 0,
B: 1,
} as const;
Unfortunately I found it still more verbose and less intuitive than: enum MyEnum {
A = 0,
B = 1,
}
TypeScript enum are also more type-safe than regular union types because they are "nominally typed": values from one enum are not assignable to a variable with a distinct enum type.This is why I'm still using TypeScript enum, even if I really dislike the generated code and the provided features (enum extensions, value bindings `MyEnum[0] == 0`).
Also, some bundlers such as ESbuil are able to inline some TypeScript enum. This makes TypeScript enum superior on this regard.
In a parallel world, I could like the latter to be a syntaxic sugar to the former. There were some discussions [1] for adopting a new syntax like:
const MyEnum = {
A: 0,
A: 1,
} as enum;
[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...One big reason: you can't name it interfaces.d.ts, or import as type, which has widespread implications:
Your types are now affecting your shipped bundles.
Sure that's a small bit of size - but it can actually lead to things like server side code getting shipped to the client.
Whereas if it's all .d.ts stuff you know there's no risk of chained dependencies.
I'd go so far as to say default eslint rules should disallow enums.
If you just need a fixed set of constants, union types with never-based exhaustiveness checks feel simpler and more “ADT–style.” That approach avoids generating the extra JS code of enums and plays nicer with certain “strip-only” TypeScript setups. In other words, if you’ve ever regretted using Enumeration in Scala because pattern matching turned messy or IDs moved around, then you’ll probably want to keep TypeScript enums at arm’s length too—or at least stick to string enums for clarity.
In Go, if something is discouraged (unsafe, runtime, reflection shenanigans), you immediately know why. The language is mostly free of things that exist but you shouldn’t use.
TS was a breath of fresh air when it came out. I never took Node seriously for backend work—it was always something I reluctantly touched for client-side stuff. But TS made some of JS’s warts bearable. Over time, though, it’s added so many crufts and features that these days, I shudder at the thought of reading a TS expert’s type sludge.
Personally I use a plain string union. If I need to lookup a value based on that I’ll usually create a record (which is just a stricter object). Typescript will error if I tried to add a duplicate.
This is all enforced at build time, whereas using a Set only happens at runtime.
type Fruit = ‘apple’ | ‘banana’;
const lookup: Record<Fruit, string> = { ‘apple’: ‘OK’, ‘banana’: ‘Meh’ }
Unions are a more more universal syntax than enums.It isn’t forced to be a 1:1 map of string to string; I’ll often use string to React components which is really nice for lots of conditional rendering.
On a slightly related topic, I also feel that the ‘type’ keyword is far more useful and preferable than ‘interface’. [1]
[1]: https://www.lloydatkinson.net/posts/2023/favour-typescript-t...
https://gist.github.com/forty/ac392b0413c711eb2d8c628b3e7698...
Makes me wonder if it was a mistake to include them at all instead of letting the community converge on patterns naturally, like we did with so many other JS patterns.