An ode to TypeScript enums
55 comments
·March 2, 2025spankalee
disintegrator
This is spot on but the issue I called out in my post is that there’s nothing drawing devs to import the mapping. Like there’s the immediate convenience of passing a string literal to a field that’s a string union for instance. You’ve done a nice thing in your snippet and named the mapping and corresponding type the same but that’s also uncommon though I’m seeing it more nowadays. So as I see it it’s very possible to end up with a codebase that inconsistently uses the object mapping if that matters to you.
spankalee
I don't know how important it is to bad use of the values directly, but that is also possible.
You have to intersect every value with a brand, like:
type Enum<T> = {
[K in keyof T]: T[K] & {__brand: never};
}
const _Foo = {
one: '1',
two: '2',
three: '3'
} as const;
const Foo = _Foo as Enum<typeof _Foo>;
type Foo = ValueOf<typeof Foo>;
And now, this will work: doSomething(Foo.two);
But this will error: doSomething('2');
davorak
Unlike the enum solution this is not nominal to my understanding.
const t = ('2' as '2' & {__brand: never});
doSomething(t);
Does not trigger an error.So you can do something like
const _Foo2 = {
two: '2',
} as const;
const Foo2 = _Foo as Enum<typeof _Foo>;
type Foo2 = ValueOf<typeof Foo2>;
doSomething(Foo2.two);
without triggering a type error too.With built in enums that would trigger an error
enum Bar {
No = 'No',
Yes = 'Yes',
}
function doSomethingBar(message: Bar): void {
}
// no type error
doSomethingBar(Bar.No);
// type error
doSomethingBar('No');
enum Bar2 {
No = 'No',
Yes = 'Yes',
}
// type error
doSomethingBar(Bar2.No);
null
hassleblad23
Its a shame because I like the enum way of declaration a lot more.
`const Foo = { Bar: 'bar' } as const` - this just feels a bit weird.
spankalee
That's a taste thing. Personally, I like my TypeScript as a superset of JS with types, so I dislike all the custom value-space syntax.
`const Foo = { Bar: 'bar' }` is how I would write an enum-like object in JS, so that's how I want to write it in TypeScript, just with added types.
fixprix
TypeScript/ES6 is such a great language with a feature set far ahead of other languages in many ways. The lack of enums though is a sore spot. I really hope that proposal you mentioned can move forward.
Also you can improve your implementation with Object.freeze(Foo) and { one: Symbol("1") }
cies
When I see this it makes me want to run for ReasonML/ReScript/Elm/PureScript.
Sum types (without payloads on the instances they are effectively enums) should not require a evening filling ceremonial dance event to define.
(any I forgot?)
It's nice that TS is a strict super set of JS... But that's about the only reason TS is nice. Apart from that the "being a strict super set" hampers TS is a million and one ways.
To me JS is too broken to fix with a strict super set.
int_19h
TS does have sum types, so you can already do something like:
type Color = "red" | "green";
What GP is doing is some scaffolding on top to make the values more discoverable and allow associating arbitrary Color-specific metadata with them.pjmlp
That single reason is all that matters, because it maps directly to what the platform actually understands, instead of adding another layer to debug.
hombre_fatal
On the other hand, pasting the `type ValueOf<T> = T[keyof T];` idiom into your TS code so you can use it for your enums is a hell of a lot less ceremony than ditching TS for any of the languages you listed. Especially when you can still just us TS enums if you wish.
And on top of that, each of them has a whole new collection of ceremonies you're going to have to learn.
All for what, to avoid `as const`?
spankalee
> It's nice that TS is a strict super set of JS... But that's about the only reason TS is nice.
I mean, yes, exactly?? That's TypeScript's entire reason for being, and it's no small thing.
I use TypeScript where I would have used plain JavaScript. If I have a reasonable choice of an entirely different language - ie, I'm not targeting browsers or Node - then I would definitely consider that.
I personally haven't seen that any compile-to-JS language is worth the interop tax with browsers or the JS ecosystem, and I've built very complex apps on GWT and used to be on the Dart team working on JS interop.
Freedom2
Civet (https://civet.dev) is probably my favorite one if I want something a bit fancier than Typescript, purely because it shares the same elements that you are as "opt-in" as much as you like, at least in my limited experience.
ninetyninenine
TS sum types are actually more powerful thanks to 'as const'
These are dependent types which none of the languages above can enable. Meaning the type system can actually read values in your code and create types from the code. This is not inferring the type, this is very different.
For example:
const PossibleStates = ["test", "me"] as const
type SumTypeFromArray = (typeof PossibleStates)[number]
let x: SumTypeFromArray = "this string triggers a type error as it is neither 'test' nor 'me'"
So in TS you can actually loop through possible states while in ML style languages you would have to pattern match them individually.zdragnar
Seeing posts like these, I often feel alone preferring enums to string unions.
There are certain situations where refactoring a string in a union will not work but refactoring an enum will. I don't want to type strings when, semantically, what I want is a discrete type. I don't even care that they become strings in JS, because I'm using them for the semantic and type benefits, not the benefits that come with the string prototype.
forty
That precisely one of my problem with enum: almost all TS type is structural typing, why have this exception enums being nominal typing?
zdragnar
Classes aren't interchangable, excepting using a child when a parent is called for.
Likewise, enums represent a discrete and unique set. The fact that there is either a number or a string used under the hood is irrelevant.
I imagine using numbers or strings was useful for interop with vanilla JS (where JS needs to call a TS function with an enum as an argument), so it makes sense to use it instead of Symbols, which is what I typically pretend enumd are.
Tade0
And to add to the confusion Template Types let you compare enums as if they were strings.
jjani
A year back or so I sat down, read through all the pros and cons including many HN posts just like this one, and I came to the same conclusion. Default to string enums. If I really need to iterate over the keys (generally an antipattern anyway), possibly refactor it into a const object literal. Never use const enums, number enums, or implicit enums.
bubblyworld
Interesting point about semantics. I wish there was a way to get the best of both - discrete type (correct semantics) but one that is auto inferred from literals in contexts where the type system expects it (ergonomics of use). Perhaps there are good reasons that doesn't work though, I haven't thought through it much =P
eyelidlessness
You’re not alone! I’ve given up the preference on team projects for pragmatic reasons, but the semantics of (string) enums are still my personal preference.
bubblyworld
One thing I find useful about enums is that they can be used as both types and values, which is ergonomic for decorator-based libraries (like class-validator, nestjs, mikro-orm, etc). The best approach I've found in union land is using const assertions and typeof, which I don't love.
Agree with the author that in almost every other way unions are better though... they play much more nicely with the rest of the type system. I find it endlessly annoying that I have to refer to enum members directly instead of just using literals like you can with union types.
disintegrator
> One thing I find useful about enums is that they can be used as both types and values
Makes sense. You can emulate that behavior by having an object literal with const assertion AND a union type of the same name derived from the object literal.
bubblyworld
Right, yeah - this is what I meant by const and typeof. It's definitely an option, but I'm nervous of relying on the semantics of const like that. But maybe I shouldn't be, it seems pretty idiomatic?
(the typeof part is just so you don't repeat yourself, or did you have something else in mind?)
motorest
Can anyone explain why enums are somehow bad but literal unions are supposed to be good?
I'll be blunt: at the surface level, it looks like literal unions are something that only someone with an irrational axe to grind against enums would ever suggest as a preferable alternative just to not concede that enums are fine.
If the problem lies in the low-level implementation details of enums, I cannot see any reason why they shouldn't be implemented the same way as literal unions.
So can anyone offer any explanation on why enums should be considered bad but literal unions should be good?
moogly
TypeScript enums require codegen, which won't work in a type erasure world. This is explained in the article.
wruza
Parameter properties also gone? I only recently found out about these, so useful for data-ish classes.
disintegrator
In principle you’ll still be able to use all of the features that existed before this flag but you’ll need to compile the code if targeting Node.js. I do think that this new flag is going to draw people away and we’ll probably see a bunch of tsconfig presets and boilerplate projects setting it to true.
If you’re using a bundler then your’re not to going benefit from it in the medium term. It’s possible this will unlock faster build times with them in the future.
DanielHB
General programming languages theory question, is one supposed to iterate over enum entries or is that considered an antipattern? I have found myself needing to do that a few times and it always felt a bit dirty.
panstromek
I wouldn't say it's an antipattern. E.g. listing all possible values of some user input field is a pretty natural use case for that.
williamdclt
No that’s fine and a reasonable thing to do . In fact, I’d say it is one of the main points of enums and one of my biggest gripes against Go is the lack of that capability
floydnoel
Swift can't handle it either, I had to write an extension to add that ability. Felt weird to me.
forty
This is my preferred home made way of doing "Enum" in TS theses days https://gist.github.com/forty/ac392b0413c711eb2d8c628b3e7698... - it includes syntax to migrate from TS enum.
The member documentation point is a good one, I'll look what can be done with my solution.
null
MBCook
What do people find works better as a string enum replacement?
const Thing {
one: “one”,
two: “two”,
three: “three”
} as const
Or just type Thing = “one” | “two” | “three”
I’ve been thinking of getting rid of the simple string enums I have but it’s not clear to me why one is preferred over the other by people.braebo
If all you need is a union type then the latter is plenty.
If you need the actual strings to iterate over or validate against, deriving the value from an const array is helpful:
const THINGS = ['one', 'two', 'three'] as const
type Thing = THINGS[number]
homebrewer
const enums are almost never mentioned by these articles for some reason. They give you the best of both worlds: they're fully erasable, and have good LSP support (do no need to search for strings and bump into false matches — or even worse, for numbers).
furstenheim
Lack of LSP support looks really bad in the article proposed solution :/.
But const enum seems to have several pitfalls. https://www.typescriptlang.org/docs/handbook/enums.html
krona
> const enums are almost never mentioned by these articles for some reason.
I think it's because a lot of tooling (excepting TSC) doesn't support cross-file const enums. But I agree - it's one of the reasons I started using TypeScript way back in 2013. I wouldn't be able to write comprehensible performance sensitive code without it.
o11c
The problem with "just use literal strings/numbers" is that that's exactly the opposite of type safe. With them it is impossible to specify an argument of type `myenum | number | string`, despite that being commonly desired in some form.
When targeting javascript, it seems to me that the obvious approach is to use symbols for enums. But symbols have a lot of WET.
(of course, typescript's safety is unfixably broken in numerous other ways, so why bother?)
DidYaWipe
"Probably my favorite argument in steelmanning enums"
Whatever that's supposed to mean.
crummy
Steelmanning is the opposite of strawmanning.
In other words, it's making the strongest version of an argument for the opposing side of the argument. The author doesn't like enums but is talking about their best attributes.
DidYaWipe
Thanks for the reply. I read a lot, and I've never encountered this term before. Seems like "in defense of" would be every bit as good, and universally understood.
swatcoder
It's an internet term of recent origin, from a specific community, not a traditional one.
It's good to be familiar with the word, as it comes up in adjacent communities like this one, but like with most slang, there are indeed clearer ways to say the same thing.
But also, some people don't realize that they've picked up a slang term or that people outside their community are part of discussions like we have here, so it comes up a lot. Now that you've spotted it, you'll likely see it here a lot.
(FWIW, I hate it and am grateful that nobody can see me roll my eyes when its used. Same for "motte and bailey" and other comically pseudo-erudite slang from those folks)
Const objects really are better than enums, in every way except declaration brevity.
They're erasable syntax, so they work in environments that just strip types. Their emit is just what you write without the types. They can be composed in a type-safe way with standard JS operations.
You can still write JS docs for values, deprecated the, mark them as internal, etc.
Given the TypeScript team's stance on new non-erasable syntax, I have to think this is how they would have gone if they had `as const` from the beginning. Ron Buckton of the TS team is championing an enum proposal for JS: https://github.com/rbuckton/proposal-enum Hopefully that goes somewhere and improves the declaration side of thigns too.