The TypeScript homepage proudly proclaims "TypeScript is JavaScript with syntax for types." It's a superset of JavaScript. Any code that's valid JavaScript is also valid TypeScript. The only thing that's added is types. The problem is that's not entirely true.
Are TypeScript enums even real?
Enums are TypeScript's biggest wart. The problem is they're not just types. Enums are a new data structure introduced in TypeScript that simply doesn't exist in JavaScript. This is a problem because TypeScript gets compiled to JavaScript. This should make you ask, "How can Typescript add a new primitive data structure that doesn't exist in JavaScript?: Let's take a look at what happens when you compile an enum to JavaScript:
// TypeScript:
enum Colors {
"red", "white", "blue"
};
...gets compiled to...
// Javascript (compiled from TypeScript):
var Colors;
(function (Colors) {
Colors[Colors["red"] = 0] = "red";
Colors[Colors["white"] = 1] = "white";
Colors[Colors["blue"] = 2] = "blue";
})(Colors || (Colors = {}));
That JavaScript is a little hard to understand. It's basically creating a Colors
object and then inserting the forward and reverse mappings between the enum names and their underlying numeric representations:
// JavaScript (but easier to understand):
var Colors = {
"red": 0,
0: "red",
"white": 1,
1: "white",
"blue": 2,
2: "blue",
};
Arguments for using TypeScript enums
So if TypeScript enums just turn into objects, why use TypeScript enums at all? Why not just use objects? Well, there is a proposal to add enums to JavaScript, but it doesn't have much support at the moment. If it does get added to JavaScript, then TypeScript's enums will start to make a lot more sense because they'll compile to something different from the objects we already have.
Wishful thinking that the enum proposal will be accepted is pretty much the only reason to use TypeScript enums.
Arguments against using TypeScript enums
Now, why shouldn't you use TypeScript enums? Well, first of all, there is no easy way to enumerate them. If you want a for
loop to run once for each entry in a TypeScript enum, you're out of luck.
Secondly, there's no easy way to determine if a given variable contains a valid enum value or not. Say your enum lists the valid colors for a bicycle, and you want to check if a string you got over the network contains one of those valid colors or not. How do you do that with an enum? You can't just check if the network string exists as a key in the Enum object because of the reverse mappings. 0, 1, and 2 are clearly not valid colors, but they are keys in the Colors
enum we created above.
As you can see, there are some common things we'd like to be able to do with enums that we just can't do with TypeScript enums.
Using union types instead of enums
Let me now introduce you to what you should use instead: union types from const arrays.
We'll first make a const array (read-only tuple) of all our valid values. Then we'll make a union type of those valid values:
// TypeScript:
const colors = ['small', 'medium', 'large'] as const;
type Color = typeof colors[number];
The array lets us iterate over the enum values. It's also const
so we don't have to worry about anyone adding or removing entries at runtime. The union type gives us type safety. Yes, this does take two lines instead of one, but wait until you see the nice things it lets us do.
We can use Color
as a type:
// TypeScript:
function compliment(color: Color) {
console.log(`${color} is a nice color.`);
}
We can enumerate all of our colors
:
// TypeScript:
function printColors() {
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
console.log(color);
}
}
And we can check if some random unknown value is a Color
:
// TypeScript:
function isColor(value: unknown): value is Color {
return colors.includes(value as any);
}
Aside: See "TypeScript: narrowing types via type guards and assertion functions" for more details about user-defined type guards in TypeScript. They're super useful!
And yes, we can even assign them to variables:
// TypeScript:
const bicycleColor: Color = "red";
At this point, it should be obvious that union types from const arrays act more like enums than TypeScript enums. The best part is that all of these TypeScript snippets are identical to their JavaScript equivalents with the types removed.
The image below shows the TypeScript snippets above in the TypeScript playground. The left pane contains TypeScript. The right pane contains the JavaScript that the TypeScript compiles to.
Epilogue and Credits
This method of using union types in place of enums is something I started doing while working at TaskRabbit circa 2020. I don't remember when or how I initially came across this technique. I have a feeling Frederic Barthelemy may have been involved.
After leaving TaskRabbit, I tried to remember the exact syntax so I could use it in a personal project and failed. I searched high and low on the web and found nothing.
More recently, I attempted my search again and found Borislav Hadzhiev's excellent post, "Create a Union type from an Array in TypeScript". Borislav doesn't touch on the fact that union types are excellent enum replacements, so I felt the need to document that fact here.