Q9 of 38 · TypeScript

When should you use a TypeScript enum versus a string literal union type?

TypeScriptJuniortypescriptenumsunion-typesas-constfundamentals

Short answer

Short answer: String literal union types (`'pending' | 'active' | 'done'`) are preferred for most cases — they are lightweight, tree-shakeable, and produce readable JavaScript. Enums generate runtime objects and are useful when you need a reverse mapping, a guaranteed set of named numeric codes, or a shared constant object across modules.

Detail

Both mechanisms model a fixed set of values, but they differ in runtime behaviour and idiomatic use.

String literal unions:

  • No runtime output — they are erased at compile time
  • Readable: values in error messages and logs are plain strings
  • Tree-shakeable: no bundle impact
  • Can be used in type guards easily: if (status === "pending")
  • Easy to extend or intersect with other union types

const enum:

  • Inlined at compile time — no runtime object, just the literal values
  • Slightly more performant (no object property lookup)
  • Cannot be used across module boundaries with isolatedModules (affects Babel, Vite, esbuild)

Regular enum:

  • Generates a runtime JavaScript object ({ Pending: "pending", pending: "Pending" } for string enums — or number reverse mapping for numeric enums)
  • Useful when: you need the reverse mapping, you want named constants with guaranteed values shared at runtime, or you integrate with code that expects an enum object
  • Numeric enums have surprising behaviour: unspecified values are auto-incremented integers — if the order changes, values shift silently

Recommendation: Prefer string literal unions in TypeScript codebases. Use enums when the runtime object is explicitly needed or when aligning with a legacy API.

// EXAMPLE

// String literal union — preferred
type Status = "pending" | "active" | "done";
const s: Status = "active";
// s = "unknown"; // Error

// Enum — generates runtime object
enum Direction {
  Up = "UP",
  Down = "DOWN",
}
const d = Direction.Up; // "UP"
console.log(Direction); // { Up: "UP", Down: "DOWN" } — runtime object

// const enum — inlined, no runtime object
const enum Color { Red = "RED", Green = "GREEN" }
const c = Color.Red; // compiled to: const c = "RED"

// Type guard works with both
function isActive(s: Status): s is "active" {
  return s === "active";
}

// Union type from array (common pattern in QA tools)
const LEVELS = ["junior", "mid", "senior", "lead"] as const;
type Level = typeof LEVELS[number]; // "junior" | "mid" | "senior" | "lead"

// WHAT INTERVIEWERS LOOK FOR

Runtime vs compile-time distinction. String union as the default choice. When enums are appropriate. The `as const` + `typeof arr[number]` pattern for deriving unions from arrays. Awareness of const enum's `isolatedModules` limitation.

// COMMON PITFALL

Using numeric enums without explicit values — if the order changes, all downstream values shift silently. Always use string enums or string unions to keep values meaningful and stable.