TypeScript enums: use cases and alternatives
2ality.com
2ality.com
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.
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.
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!
I agree they should just formally deprecate it.
TypeScript versioning is literally a joke
I really don't understand what GP's point was.
1) verbatimModuleSyntax -- https://www.typescriptlang.org/tsconfig/#verbatimModuleSynta...
2) isolatedModules -- https://www.typescriptlang.org/tsconfig/#isolatedModules
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.
Not to mention that you can add documentation to each of the entries.
You can do that with either other solution.
type SomeProcessState =
// when waiting for user interaction
| 'idle'
// busy processing
| 'busy'
// finished processing
| 'success'
const OtherProcessStates = Object.freeze({
/**
Waiting for user interaction
*/
Idle: 0,
/**
Busy processing
*/
Busy: 1,
/**
Finished processing
*/
Success: 2,
} as const)
type OtherProcessState = typeof OtherProcessStates[keyof typeof OtherProcessStates]
The second form those are even working JSDOC comments.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 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.
You used a lot of words to say syntactic sugar.
People love syntactic sugar. It solves their problems and makes their life easier.
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.
They aren't. As others already pointed out, enums greatly simplify some usecases, although they brush the complexity of implementing them under the rug.
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.
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.
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.
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...
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?
Seems weird to me to decide you're OK with the build step and all the other complexity TS adds, but using enums is too much, because maybe in the future JS runtimes might be able to strip away types for you without a build-step.
But we all have different constraints and use cases, I suppose it does make sense for what you're building.
> maybe in the future JS runtimes might be able to strip away types for you without a build-step.
You can't backpedal from that to "speaking about the specification". It's not future JS runtimes. It's a thing you can take advantage of right now.
Parameter properties and namespaces, on the other hand, are kind of wonky TS specific features that nobody expects to be there unless they specifically find them in the docs. Parameter properties offer a little bit of conciseness but don't fill a pressing need. Namespaces solve a problem that just doesn't actually come up that often, and few devs are going to go actively looking for them. Even the example code on typescriptlang.org doesn't show a very compelling case; they show "Validation" used as a namespace for classes that are already namespaced the low tech way by having "Validator" in their names. (Which isn't to say that the feature isn't ever useful; it's just that cases where someone would actually seek it out are niche.)
All of that is to say that sure, enums are just one of a handful of features that will break in runtimes that simply strip out types, but they're also the main feature that's likely to trip people up.
Parameter properties are I think still used quite heavily in Angular, but I don't see them much elsewhere. Again, they're a very old feature, and they don't play well with newer developments in the language (such as native private attributes), so it doesn't seem like much of a problem to avoid them as well.
The other big TS-only feature is old-style decorators, but that shows the danger of relying too much on this TS-based syntax sugar. Decorators have gone through several revisions, and the version that Typescript implemented is long dead. But a number of codebases are still stuck using this legacy system because it's not compatible with the newer versions of decorators that will (eventually, hopefully) be implemented in browsers. The legacy system is still maintained, I believe, and you can still keep on using it, but you'll not get the benefits of using the same system as the wider Javascript ecosystem, and you'll not get the benefits of having the syntax be native to browsers, when that happens.
In general, Typescript works best when you use it as simply a type annotation syntax for Javascript, and not as an additional layer of sugar on top of that. And clearly the Typescript developers see things similarly, because they've stopped implementing sugar-like features and have committed to only implementing the stuff that will also be implemented as new features in Javascript.
Not just the future. Node shipped this with an "experimental flag" in the last LTS and in Current it works without the flag (will ship in the next LTS without a flag). Deno has done something like this for years now, and Bun for nearly as long. The remaining question is if/when Browser support might also exist, which for now remains at Stage 1 discussions with the technical committee (TC-39).
Maybe library compatibility?
My first reaction is that this just further fractures the ecosystem, where some codebases/libraries will have TS that is required to be compiled and some will not, adding a third kind of TS/JS code that's out there.
We’re not talking about the distant future. Node shipped its first version supporting type stripping six months ago.
That I would imagine there are other features being proposed that will continue to develop this compatibility?
> Since Node.js is only removing inline types, any TypeScript features that involve replacing TypeScript syntax with new JavaScript syntax will error, unless the flag --experimental-transform-types is passed.
> The most prominent features that require transformation are:
> Enum > namespaces > legacy module > parameter properties
If you ask me, both features have been de facto deprecated for ages now.
The future has been here for a while.
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. const MyEnum = {
x: 1,
y: 2,
z: 3,
// etc
}
instead of enum MyEnum {
x = 1,
y,
z,
// etc
}
when you want a series of constants each with a unique value but don't particularly care what that value is.TypeScript's enums are particularly weak compared to enums in other languages precisely because there's no JS support for enums. Modern languages have support for ADTs.
In the vast majority of cases, there's no good reason to do that.
Edit: But no, the reason enums in TypeScript suck is not that JS doesn't have them. That wouldn't fix anything other than the type stripping problem. The main reason that they suck is that they use a completely different type model from the rest of the language.
90% of the enums I use are regular integer enums. I don't get much use out of string enums, as you say union types do that job just fine.
export const MyEnumMapping = {
active: 0,
inactive: 1
} as const
export type MyEnum = typeof MyEnumMapping[keyof typeof MyEnumMapping];
So you have the names exposed, but the underlying type is the number.Personally I am definitely not skilled enough at typescript to come up with this on my own before seeing this thread so this was not even an option until now.
export type MyEnum = ValueOf<MyEnumMapping>;
TypeScript not having enough sugar in its built-in utility types is definitely a fair criticism.But more to the point, the above is not usually how you do enums in TS unless you have some very specific reason to want your values to be numbers at all times. There are some cases like that, but usually you would just let the values be strings, and map them to numbers on demand if that's actually required (e.g. for a specific serialization format).
type MyEnum = {
active: 0;
inactive: 1;
}
const MyEnum: MyEnum = {
active: 0,
inactive: 1,
}
const showAge = MyEnum.active;
const showPets = MyEnum.inactive;
It's slightly more duplication, but a lot more readable (imo) to those unfamiliar to utility types. TypeScript also enforces keeping them in sync. function doSomethingWithMyEnum(val: MyEnum[keyof MyEnum])
You could do `val: number`, but now you're allowing any number at all.Ultimately, the type syntax in TypeScript is a key part of the language, and I don't think it's unreasonable to expect developers to learn the basic typeof and keyof operators. If we were talking about something wonkier like mapped types or conditional types, sure, it might make sense to avoid those for something as basic as enums.
Edit: But for what it's worth, yes, the above is still more elegant than enums. The syntax may feel less elegant, but among other things, the above does not depart from the structural type paradigm that the rest of the language uses, all for something as simple as an enum.
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.
its 2025 and node is finally good :)
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...
Apparently they're planning on adding a tsconfig option to disallow these Node-incompatible features as well [1].
Using this limited subset of TS also allows your code to compile with Bloomberg's ts-blank-space, which literally just replaces type declarations with whitespace [2].
[1] https://www.typescriptlang.org/tsconfig/#verbatimModuleSynta...
[2] https://www.typescriptlang.org/tsconfig/#isolatedModules
It would essentially help you produce TypeScript that's compatible with the --experimental-strip-types flag (and ts-blank-space), rather than the --experimental-transform-types flag, which is nice because (as someone else in this thread pointed out), Node 23 enables the --experimental-strip-types flag by default: https://nodejs.org/en/blog/release/v23.6.0#unflagging---expe...
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.
Private properties have been in the works for the last 7-8 years, and were officially added three years ago.
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...> Note that __proto__ also exists as a getter and a setter in Object.prototype. This feature is deprecated in favor of Object.getPrototypeOf() and Object.setPrototypeOf(). However, that is different from using this name in an object literal – which is not deprecated.
In this case, I could write this:
type Activation = "Active" | "Inactive";
const Activation = {
__proto__: null,
Active: "Active",
Inactive: "Inactive",
} as { [K in Activation]: K };
This completely hides `__proto__` and avoid using utility types like `Exclude`.Note that it is safe because TypeScript checks that the type assertion is valid. If I mistype a value, TypeScript will complain about the assertion.
Maybe people should become familiar with Grug: https://grugbrain.dev/
I'll take the t-rex.
> apex predator of grug is complexity. complexity bad. say again: complexity very bad. you say now: complexity very, very bad. given choice between complexity or one on one against t-rex, grug take t-rex: at least grug see t-rex
// in some util.ts or helpers.ts
export function keysOf<T extends string>(obj: { [k in T]: unknown }): readonly T[] {
return Object.keys(obj) as unknown[] as T[];
}
//---
const kOperationsFunctions = {
add(a: number, b: number) { return a + b; },
subtract(a: number, b: number) { return a - b; },
multiply(a: number, b: number) { return a * b; },
divide(a: number, b: number) { return a / b; },
};
const kOperations = keysOf(kOperationsFunctions);
type Operation = (typeof kOperations)[number];
I don't want to declare an enum here. That would just be a waste of typing and an opportunity for things to get out of sync. export const SMS_TYPE = {
BULK: 'bulk',
MARKETING: 'marketing',
PIN: 'pin',
SIGNUP: 'signup',
TRANSACTION: 'transaction',
TEST: 'test',
} as const;
export type SmsType = typeof SMS_TYPE[keyof typeof SMS_TYPE];
ENUMs (at least in my experience, which may be dated) had a number of drawbacks that pushed us to this format. I vaguely remember having issues parsing data from the server and/or sending ENUM values to the server but it's been a long time and I've been using this const pattern for around 5 years or so now. export const SMS_TYPE = Object.freeze({
BULK: 'bulk',
MARKETING: 'marketing',
PIN: 'pin',
SIGNUP: 'signup',
TRANSACTION: 'transaction',
TEST: 'test',
} as const);
export const SMS_TYPE_LIST = Object.freeze(Object.values(SMS_TYPE));
export const SMS_TYPE_SET = Object.freeze(new Set(SMS_TYPE_LIST));
export type SmsType = typeof SMS_TYPE[keyof typeof SMS_TYPE];I think I originally found the const-style on somewhere HN or SO but it has changed a bit over the years due to suggestions people have made so thank you for contributing to improvement of this pattern for me.
Type checks where you need to say, for example, SMS_TYPE_SET.has(someValue)
export type SmsType = typeof SMS_TYPE_LIST[number]; // put this in some util.ts or enum.ts file
function makeEnum<T extends readonly string[]>(keys: T) {
return Object.fromEntries(keys.map((x) => [x, x])) as {
[K in (typeof keys)[number]]: K
};
}
const SMS_TYPES = makeEnum(['bulk', 'marketing', 'pin', 'signup', 'transaction', 'test'] as const);
type SMS_TYPE = keyof typeof SMS_TYPES;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.
From what I can tell, they were an early addition from back before TS had unions, and it feels like they live in their own world within the type system. I would go further than saying you should disallow them with a linter, and say that they should be deprecated in the language. Right now they’re just a foot gun for new TS devs.
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.
I'd welcome TS type system in Python, mypy and co. should steal it outright.
Curious what aspects TS has that Python doesn't? (or that Python doesn't do as well)
For example, typing a decorator means importing `ParamSpec`, `Callable`, and a bunch of other things. In TS, all of that is available in the global scope, so it’s way less cluttered. Plus, the type system in TS is a lot more powerful. Generic record types are way nicer in TS than in Python.
That was my initial assessment as well. Anything JavaScript related I stood clear and far away from using it anywhere near backend systems and relegated it into the list of non-serious technologies to stay away from.
> 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.
TypeScript just repeated the same issues as CoffeeScript and both JS and TS are just as bad for software anyways.
Go and Kotlin have much better type systems, but the rest of the JS ecosystem just reeks with immaturity.
Same. The frontend can sustain this continuous churn but the backend can't. I go for Go or Python to build backends these days.
e.g. for "development" vs "production" environments, you could write a declaration file for each of those envs as such:
// production.d.ts
declare const enum ENVIRONMENT {
PROD = 0,
DEV = 1,
CURRENT_ENV = PROD,
}
And then write in your code something like: // some_file.ts
if (ENVIRONMENT.CURRENT_ENV === ENVIRONMENT.DEV) {
// do something for dev builds
}
It will be replaced by TypeScript at compile-time and most minifiers will then be able to remove the corresponding now-dead code when not in the right env.This is however mainly useful when you're a library developer, as you may not have any "bundler" dependency or any such complex tool able to do that task.
Here, the alternative of bringing a complex dependency just to be able to replace some constants is not worth its cost (in terms of maintenance, security, simplicity etc.), so even if `const enum`s may seem poorly-adapted, they are actually a good enough solution which just works.
I remember being shocked about it when i heard that on ts-congress by, Nathan Sanders a ts contributor, around 4-5 years ago.
I find the ts-enums incredibly poorly designed and advice my juniors to stay away from them generally.
It's almost similar for interface (https://shively-sanders.com/types-vs-interfaces.html)
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://github.com/photostructure/fs-metadata/blob/main/src/...
Usage:
export const Directions = stringEnum("North", "South", "East", "West")
export type Direction = StringEnumKeys<typeof Directions>
(I haven't published this as a discrete npm package--IMHO you should copy and paste this sort of thing into your own tree).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.
Since 1.0 Typescript has been following a plan that every feature needs to be on TC-39's standards track somewhere and since around 2.5/3.0 they've been even more strict that every feature needs to be at least Stage 3 in TC-39's standards track.
That enums still exist at all is mostly a testament to Typescript's backwards compatibility goals. A lot of 0.7-ish code will still compile today with the right flags and can be usefully upgraded by setting new flags one at a time. TS 0.7 code also won't look like modern Typescript, it's so far away now.
A good tsconfig: https://gist.github.com/cecilemuller/80fed1b963171ca4e117f6d...
Or is there critical typing functionality that Typescript accommodates only perversely?
I'm not sure if such perversions exist, it's been a while already since I touched TypeScript. To me the whole system seems perverted in the sense that an inexperienced developer can easily make things unnecessarily complex.
1. They have a runtime representation unlike most of the rest of TypeScript
2. They follow nominal typing instead of structural typing, again unlike the rest of TypeScript
IMO it's best to use string union types instead of enums. If you need to map that to another representation you can use a function or a record.