https://yazanalaboudi.dev/in-defence-of-typescript-enums Yazan's Software Thoughts Follow Yazan's Software Thoughts Follow In Defence of Typescript EnumsIn Defence of Typescript Enums[v] Photo by Jonny Rogers on Unsplash In Defence of Typescript Enums You're (Probably) Using Them Wrong []Yazan Alaboudi's photoYazan Alaboudi's photo Yazan Alaboudi *Mar 31, 2025* 6 min read In the past few years, there's been a growing trend of criticizing TypeScript's enum feature. Videos, blog posts, and tweets call for avoiding them entirely, and many developers are now defaulting to union types or const objects instead. And yes--TypeScript enums do have some quirky behaviors that make them less elegant than their counterparts in other languages. But here's the thing: Most of the complaints about enums aren't about the feature itself--they're about misuse. I'd even go as far as to say that the majority of enum pitfalls in TypeScript are caused by a fundamental misunderstanding of what an enum is meant to be in the first place. This article isn't just a defense of enums. It's a reminder of what enums are for--and why, when used correctly, they're still a powerful and expressive tool in TypeScript. --------------------------------------------------------------------- Enums Are Meant To Be Symbolic Let's get one thing straight: enums are not meant to represent literal values. They're meant to represent symbolic ones. This distinction might sound pedantic, but it's at the core of the misunderstanding. As Wikipedia puts it: "An enumerated type has values that are different from each other, and that can be compared and assigned, but are not specified by the programmer." That last part is key: not specified by the programmer. In other words, we shouldn't care what the underlying value of an enum is. We only care about what it represents. --------------------------------------------------------------------- What Does "Symbolic" Really Mean? Let's make it concrete. Imagine you're building a traffic light simulation. A traffic light can be red, yellow, or green. How should you represent those states in your code? You could do: const RED = "red"; const YELLOW = "yellow"; const GREEN = "green"; Or maybe you're feeling fancier and you use hex codes: const RED = "#FF0000"; const YELLOW = "#FFFF00"; const GREEN = "#00FF00"; Or perhaps localized strings: const RED = "rouge"; const YELLOW = "jaune"; const GREEN = "vert"; None of these are wrong--but none are ideal either. Because what you're really trying to express is the concept of red, yellow, and green. Not a particular string or color code or translation. Just the idea. That's where symbolic values shine. --------------------------------------------------------------------- JavaScript Has a Built-in Symbolic Primitive ES6 introduced the Symbol primitive, and it's a perfect metaphor for this kind of use: const RED = Symbol("Red"); const YELLOW = Symbol("Yellow"); const GREEN = Symbol("Green"); Each call to Symbol() returns a unique value, regardless of the label. That means Symbol("Red") !== Symbol("Red"), unless you store it and reuse the same reference. The value itself doesn't matter--just the identity. This gets to the heart of how enums should behave: they should stand for something abstract, not something concrete. --------------------------------------------------------------------- Where TypeScript Gets a Little Funky Here's where TypeScript throws a curveball: unlike many other statically typed languages, TypeScript allows you to assign actual string or number literal values to enum members. enum TrafficLight { Red = "red", Yellow = "yellow", Green = "green" } This feels convenient at first. You can now log enum values, serialize them into JSON, and display them in UIs directly. But this is a trap. Why? Because you're no longer using enums symbolically. You've turned them into a tightly-coupled representation layer. The enum value is the thing you're going to display, transmit, and compare externally. And that opens the door to a few major risks: 1. Leaky Abstractions By using a string value like "red" directly in your enum, you're tying your logic to that representation. Now every part of your app--your UI, backend, tests--might start to depend on that "red" string. If you later need to localize, change the display name, or serialize differently, you're stuck. Example: enum TrafficLight { Red = "red", Yellow = "yellow", Green = "green" } function renderLight(color: TrafficLight) { return `
`; } At first glance, this seems fine. But what happens when marketing wants the classes to change to "stop", "caution", and "go" instead of "red", "yellow", and "green"? You're now forced to either: * Rename your enum values (breaking everything), * Or inject a weird indirection layer to undo the coupling you introduced in the first place. You've tightly bound your logic layer and representation layer, which makes the system harder to evolve. 2. Fragile Equality Using literal values opens up the door for subtle bugs due to implicit casting, loose comparisons, and typos. For example: if (color === "Red") { // oops - this will never be true if your enum is "red" (lowercase) } Or worse: const userInput = req.body.color; // from frontend form input if (userInput === TrafficLight.Red) { // works... until someone changes the frontend string casing or translation } Now your enum value is effectively part of a contract--and not a very safe one. There's no type safety guarding the string being passed around. A typo or localization mismatch breaks your logic silently. 3. Enum Values Become Public API Once you serialize your enums as strings or numbers and expose them (e.g. in JSON APIs), they become part of your public contract. Now you're stuck maintaining these exact values forever, even if they were never intended to be meaningful. Imagine this: // Backend API { "status": "Processing" } This might come from: enum OrderStatus { Processing = "Processing", Shipped = "Shipped", Delivered = "Delivered", } Now Processing is in your API contract. What happens when you want to rename it to InProgress? You can't--at least not without a breaking change. Instead, the enum should have stayed symbolic: enum OrderStatus { Processing, Shipped, Delivered } And your serialization should happen in a dedicated mapping layer: const statusToApi = new Map