Enums & Literal Types — Naming Your Constants
Stop sprinkling magic strings like "pending" and "admin" throughout your codebase. Master Enums, Literal Unions, and the modern as const pattern to make your constants bulletproof.
📖 The Story: The Magic String Massacre
A team was building an e-commerce order system. Throughout the codebase, they used the string "pending" to check order status:
if (order.status === "pending") { // File 1 } if (order.status === "Pending") { // File 2 — Typo! Capital P! } if (order.status === "pendng") { // File 3 — Typo! Missing 'i'! }
The code compiled. It ran. But Files 2 and 3 never executed their logic because the strings didn't match. Orders got stuck in a permanent state. It took 3 days of debugging to find two typos.
The fix? An Enum. By defining enum OrderStatus { Pending = "pending" }, every reference becomes OrderStatus.Pending. If you typo OrderStatus.Pendng, the compiler instantly screams Property 'Pendng' does not exist. The Massacre never happens again.
Magic strings are invisible landmines. Enums and literal types are the metal detector.
🎯 Why Enums?
Typo Protection
The compiler catches Pendng instantly. No silent logic bugs.
Autocomplete
Type OrderStatus. and your IDE lists every valid option.
Single Source
Change the value in one place. Every reference updates automatically.
🧒 Explain Like I'm 10
Imagine your school has named classrooms: "Room 101", "Room 102". Instead of remembering the numbers, they give them names: "Science Lab", "Art Room", "Music Hall".
Enums are those name tags. Instead of writing the number 101 everywhere (and accidentally writing 110), you write Room.ScienceLab. The name tag guarantees you walk into the right room.
🎓 Professional Explanation
An Enum is a set of named constants. Unlike plain types, enums generate actual JavaScript objects at runtime. Numeric enums support "reverse mapping" (value → name), while string enums are more debug-friendly in logs. Modern TypeScript often prefers Literal Union Types or const objects for simple cases because they are zero-cost at runtime.
🔢 Numeric Enums
The default enum is numeric. If you don't assign values, TypeScript auto-increments from 0.
enum Direction { Up, // 0 Down, // 1 Left, // 2 Right // 3 } // You can also set custom starting values enum HttpCode { OK = 200, NotFound = 404, ServerError = 500 } // Reverse Mapping: Unique to numeric enums! console.log(Direction[0]); // "Up"
Danger: Numeric enums allow any number to be assigned (not just the defined ones) in some configurations. String enums are safer.
🔤 String Enums (Recommended)
String enums are the most commonly used. They are readable in logs and debuggers.
enum OrderStatus { Pending = "PENDING", Shipped = "SHIPPED", Delivered = "DELIVERED", Cancelled = "CANCELLED" } function processOrder(status: OrderStatus) { if (status === OrderStatus.Pending) { // ... } } processOrder(OrderStatus.Pending); // OK processOrder("PENDING"); // Error!
⚡ Const Enums
A const enum is completely erased at compile time. No JavaScript object is generated; the values are inlined directly.
const enum Color { Red = "RED", Blue = "BLUE" } // Compiles to: if ("RED" === "RED") { ... } // No Color object exists in the output! if (Color.Red === "RED") { }
Trade-off: Const enums break "Isolated Modules" (used by Babel, esbuild). They cannot be iterated over at runtime. Use with caution in modern bundlers.
💎 The Modern 'as const' Pattern
The TypeScript community is increasingly moving away from enums toward as const objects with union types.
// 1. Define a const object const OrderStatus = { Pending: "PENDING", Shipped: "SHIPPED", Delivered: "DELIVERED" } as const; // 2. Derive the type type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus]; // Result: "PENDING" | "SHIPPED" | "DELIVERED" // 3. Usage let status: OrderStatus = OrderStatus.Pending; // OK status = "SHIPPED"; // Also OK (it's a literal union!) status = "EXPIRED"; // Error!
Why this is winning: Zero runtime overhead (it's just a plain object), full autocomplete, and it works perfectly with tree-shaking bundlers.
⚔️ Enum vs Union Type
| Feature | Enum | Literal Union / as const |
|---|---|---|
| Runtime Output | Generates JS object | Zero (fully erased) |
| Iteration | ✅ Object.values(Enum) | ✅ Object.values(obj) |
| Reverse Lookup | ✅ Numeric only | ❌ Manual |
| Bundle Size | Adds bytes | Zero overhead |
| Tree-Shaking | Poor | Excellent |
| Plain String Assign | ❌ Not allowed | ✅ Allowed if it matches |
⚠️ Common Mistakes
Mistake 1: Numeric Enums Auto-Incrementing
// Bad: Inserting a new member in the middle shifts all numbers! enum Priority { Low, // Was 0, still 0 Medium, // Was 1, now 1 Urgent, // NEW! Was nothing, now 2 High // Was 2, NOW 3! BREAKS SAVED DATA! }
If you store numeric enum values in a database, inserting a new member in the middle corrupts existing data. Use string enums for persisted values.
✅ Best Practices
- Always use string enums for values stored in databases or sent over APIs.
- Prefer
as constobjects for new projects targeting modern bundlers. - Never rely on numeric auto-increment for anything persisted.
- Use PascalCase for enum members:
OrderStatus.Pending.
🏗️ Senior Deep Dive
Enum Runtime Code Generation
A regular enum compiles to this JavaScript:
var OrderStatus; (function (OrderStatus) { OrderStatus[OrderStatus["Pending"] = "PENDING"] = "Pending"; })(OrderStatus || (OrderStatus = {}));
This is why bundle-size-conscious teams at companies like the TypeScript team itself recommend as const unions for most use cases. The enum generates an IIFE (Immediately Invoked Function Expression) that runs at module load.
💼 Interview Questions
Show Answer
Enum[0] → "Name"). String enums assign explicit string values, are readable in logs, and are safer for persisted data because adding new members doesn't shift existing values.Show Answer
Show Answer
const obj = {...} as const; type T = typeof obj[keyof typeof obj] provides enum-like safety with none of the downsides.🏋️ Practical Exercise
Kill the Magic Strings
Refactor this code to use an enum. Fix the typo in File 3 while you're at it.
function checkRole(role: string) { if (role === "admin") return "Full Access"; if (role === "editor") return "Edit Access"; if (role === "viewr") return "View Access"; // Typo! }
Task: Create a Role enum and update the function to use it. Make the parameter type-safe.
📝 Quiz
Test your understanding. Click an option to check your answer.
const enum after compilation?as const do to an object?as const makes properties readonly and infers literal types instead of widened ones (e.g., "PENDING" instead of string).📦 Summary
🎯 Key Takeaways
- Enums eliminate magic strings and catch typos at compile time.
- String enums are safer than numeric for persisted data.
- Const enums are erased at compile time but break Isolated Modules.
as constobjects are the modern, zero-cost alternative.- Numeric enums support reverse mapping; string enums do not.
- Never rely on auto-increment for values stored externally.
Comments
Comments
Post a Comment