TypeScript enums are stupid
A practical insight into the bewildering stupidity of TypeScript enums
The appeal of enums
On paper, enums allow you to define a constrained set of named constants. This provides type safety, useful editor feedback, and intellisense.
enum Color {
Red,
Green,
Blue,
}
Enum values can only be addressed by using the "dot" syntax: EnumName.ValueName
. Eg: Color.Red
.
In addition to the above benefits, this syntax is advantageous for refactoring. If your needed to rename Color.Red
to Color.Crimson
, you could easily find all references to Color.Red
in your code base.
But enums are not what they seem. Let's look at a simple example:
const myFunc = (color?: Color) => {
if (color) {
console.log("I received a color");
} else {
console.log("No color received");
}
};
Seems innocent enough. Until you try calling myFunc
with Color.Red
:
myFunc(Color.Red);
> "No color received"
Hmmmmm... ???
Enums at runtime
So when a developer is confronted with something as seemingly absurd as this, their natural instinct would likely be to try to log the value of the color argument.
const myFunc = (color?: Color) => {
console.log(color);
};
Anticipation building...
myFunc(Color.Red);
> 0
"Ok I don't know what I was expecting but it certainly wasn't that."
Let's log the entire enum:
console.log(Color);
> { '0': 'Red', '1': 'Green', '2': 'Blue', Red: 0, Green: 1, Blue: 2 }
What a mess... Fascinating.
This raises more questions than answers but at least you'll now understand why myFunc(Color.Red)
didn't work. It's because Color.Red
is falsy when evaluated as a boolean. In fact, the first entry in any enum will always evaluate as falsy.
How to safely use enums
Ok so what if instead of letting TypeScript implicitly assign values, we take the time to assign sensible and descriptive values ourselves?
enum Color {
Red = "red",
Green = "green",
Blue = "blue",
}
console.log(Color);
> { Red: 'red', Green: 'green', Blue: 'blue' }
This looks much more sane and it also fixes our falsy problem. But this kind of feels like a workaround and also defeats the purpose of having enums in the first place. We have to manually assign values and repeat ourselves. You might also justifiably feel a bit grossed out by enums in general at this point and decide that they aren't worthy of your trust.
You deserve better
Union types
type Color = "red" | "green" | "blue";
Clean and simple. Don't be so fast to dismiss union types, often this will fulfil all your type safety needs.
Unfortunately you lose the ability to have the "dot" syntax, Color.Red
is not a thing with this approach. This leaves something to be desired if you're attached to the idea of having a single source of truth for your constants.
POJOs
If we're determined to have dot syntax, we might as well just use a plain old JavaScript object.
// color.ts
export const Color = {
Red: "red",
Green: "green",
Blue: "blue",
} as const;
// The object type can be overridden
export type Color = (typeof Color)[keyof typeof Color];
This is a perfectly valid approach that offers the same behaviour as an enum but in a more familiar way.
We can derive a type from the object and in doing so the Color
type ends up as a union of all the values in the object. By overriding the type and giving it the same name as the object, we can conveniently import one variable and consume it as both the type and value. This effectively mimics the behaviour of an enum in a safer way.
// someOtherFile.ts
import { Color } from "./color.ts";
const red: Color = Color.Red;
It's a slightly ugly bit of TypeScript gymnastics but it's just about palatable and it could be made into a generic utility type.
String literal arrays
const ColorsArray = ["red", "green", "blue"] as const;
// again we can derive a union type
type Color = (typeof ColorsArray)[number];
This approach is cleaner than the POJO approach but it comes at the sacrifice of the dot syntax.
One notable advantage of this approach is that you can iterate over the values in the array. If you wanted to do this with the POJO approach, you'd have to convert the object to an array of values.