📖 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:

typescript · The Nightmare
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.

typescript
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.

typescript
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.

typescript
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.

typescript · Modern Pattern
// 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

FeatureEnumLiteral Union / as const
Runtime OutputGenerates JS objectZero (fully erased)
IterationObject.values(Enum)Object.values(obj)
Reverse Lookup✅ Numeric only❌ Manual
Bundle SizeAdds bytesZero overhead
Tree-ShakingPoorExcellent
Plain String Assign❌ Not allowed✅ Allowed if it matches

⚠️ Common Mistakes

Mistake 1: Numeric Enums Auto-Incrementing

typescript · Bad
// 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

  1. Always use string enums for values stored in databases or sent over APIs.
  2. Prefer as const objects for new projects targeting modern bundlers.
  3. Never rely on numeric auto-increment for anything persisted.
  4. Use PascalCase for enum members: OrderStatus.Pending.

🏗️ Senior Deep Dive

Enum Runtime Code Generation

A regular enum compiles to this JavaScript:

javascript · Compiled Output
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

Beginner
What is the difference between a numeric and string enum?
Show Answer
Numeric enums auto-assign numbers (0, 1, 2...) and support reverse mapping (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.
Intermediate
What is a const enum and when would you use it?
Show Answer
A const enum is fully erased at compile time. The values are inlined directly at each usage site, generating zero runtime code. However, it breaks with isolatedModules (Babel/esbuild) and cannot be iterated at runtime.
Advanced
Why is the TypeScript community moving toward 'as const' objects over enums?
Show Answer
Three main reasons: 1) Zero runtime overhead (enums generate JS objects), 2) Better tree-shaking support in modern bundlers, 3) Literal union types allow direct string assignment while enums don't. The pattern const obj = {...} as const; type T = typeof obj[keyof typeof obj] provides enum-like safety with none of the downsides.

🏋️ Practical Exercise

🎯 Hands-On Practice Easy

Kill the Magic Strings

Refactor this code to use an enum. Fix the typo in File 3 while you're at it.

typescript · Before">
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.

1. What is the first value of a numeric enum without explicit assignments?
A) 1
B) 0
C) "0"
Answer: B — Numeric enums auto-increment starting from 0 by default.
2. Which enum type supports reverse mapping (value → name)?
A) String enums
B) Numeric enums
C) Const enums
Answer: B — Only numeric enums generate the reverse lookup object at runtime.
3. Why should you avoid numeric enums for database-persisted values?
A) They take more storage space.
B) Inserting a new member in the middle shifts all subsequent numbers, corrupting existing data.
C) Databases cannot store numbers.
Answer: B — Auto-increment makes enum values unstable. Use strings for persistence.
4. What happens to a const enum after compilation?
A) It becomes a global variable.
B) It is completely erased; values are inlined at usage sites.
C) It becomes a JSON file.
Answer: B — Const enums leave zero runtime trace; the compiler replaces references with literal values.
5. What does as const do to an object?
A) Makes it immutable at runtime.
B) Infers each property as its narrowest literal type with readonly.
C) Converts it to an enum.
Answer: B — 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 const objects are the modern, zero-cost alternative.
  • Numeric enums support reverse mapping; string enums do not.
  • Never rely on auto-increment for values stored externally.