Why you shouldn't use Enums!
TypeScript Enums
is a feature that gets way more love from the community than it actually deserves. While TypeScript 5 improved some of my original concerns,
Enums
still have one major issue… There is no JavaScript concept of an Enum
. This doesn’t sound like a big deal, right? Wrong! Let’s take a closer look
at the issues and how to fix Enums
!
The Problem
Alright Enums
don’t exist in JavaScript, what’s the big deal, right?
We all heard that statement ‘TypeScript is a superset of JavaScript’, so why shouldn’t
that superset add features to JavaScript.
Well … let me tell you the story of experimentalDecorators
.
In times not too long ago, there was just a TypeScript
poor little compiler flag called experimentalDecorators
.
This flag was trying to act and behave like his big brother the TC39 Decorator
Proposal.
But unfortunately the Decorator
proposal wasn’t all grown up yet, so as it happens to brothers, they grow apart.
End of the story.
TypeScript introduced Decorators, modeled after the TC39 proposal as it was designed at that time, but it happened that the proposal changed while the experimentalDecorator
feature is used and therefore can’t easily
be changed.
To cope with the fact that there is not an enum
construct in JavaScript, TypeScript came up with a clever trick.
This compiles to something like this
A little simplified, but basically TypeScript creates an Object that looks like this
The nice thing with this approach is that is lets us have access to every possible information of the HttpStatusCode
in a bidirectional way.
We can get results by
calling HttpStatusCode["OK"]
& HttpStatusCode[200]
.
From a developer experience, this is nice and opens the door for a lot of different ways to get access to this information,
but it does come with an increase in bundle size.
That bundle size might be negligible.
However, it can easily add up.
Just imagine if I had added all possible values
to my HttpStatusCode
Enum.
I just picked some common one, but just have a quick look at the full list, and it should give you a rough impression what it does
to your bundle size.
There are applications out there where bundle size is not a concern at all, but I still have a problem with this approach.
It breaks the distinct line between TypeScript and JavaScript.
There is one fix to that problem, const enums
in TypeScript don’t create any code at runtime, they just act as a type and their value is automatically resolved when used.
This compiles to something like this
The result of this looks great to me, but you can run into problems with this.
It is getting more and more common to use other transpilers to generate JavaScript code from TypeScript.
Often those transpilers work on a file-by-file basis,
transpiling just a single file without fully understanding the full type system.
Vite and Babel work this way for instance.
Just transpiling a single file does not read imported modules, so there is no way to support const enums
.
If you don’t believe me that you shouldn’t use const enums
maybe the TypeScript Team can convince you (reference #1 and reference #2).
The Solution
Well, first and foremost, don’t use enums!
TypeScript has Literal Types that are a great replacement for enums.
Literal Types in TypeScript can be used for strings and numbers (and booleans but less relevant for this blog post), and the best thing is: those are type safe.
All right, here’s how using literal types for our HttpStatusCode
enum could look like this:
This is a pattern I mostly use for this purpose. It doesn’t support the bidirectional-access of enums, but this is something I rarely use and could be accomplished via copy and paste. It also works the same way with strings instead of numbers. This approach doesn’t come with footguns and is pretty straightforward. I also like the type safety of it.
Last but not least HttpStatusCodes
also works with any kind of exhaustiveness check. Looking at a code example like this:
In case we now decide that we want to support an additional status code in our application, we want the TypeScript compiler to list all the instances we need to make adjustments to.
As long as the noFallthroughCasesInSwitch
compiler option is used, this code will throw an error during compilation!
Helpful Utilities
The TypeScript isolatedModules
provides warning for certain scenarios where file-by-file transpilation can cause runtime errors.
This mostly addresses issues when using a const enum
.
Luckily typescript-eslint can ban entire language features, like enums
.
You can find a detailed description here, or if you are just looking for something quick and easy here’s the relevant code snippet