Ch. 2 · TypeScript

TypeScript Enums vs Union Types

Compare enums with unions of string literals: runtime cost, serialization, exhaustiveness and which one fits application code.

~3 min readintermediateupdated Oct 5, 2026

An enum emits a runtime object with named members, while a union of string literals exists only at compile time. Both restrict a value to a set of options, but they differ in serialization, exhaustiveness checks and how they interact with structural typing.

Before you start

You should be comfortable with literal types, unions and discriminated unions. This article compares the two models; it assumes you already narrow with === or switch.

Step-by-step walkthrough

Step 1: Prefer a union for a plain set of values

type Status = 'idle' | 'loading' | 'done' gives autocompletion, exhaustiveness checking in a switch, and a value that serializes as the same string it is stored as. There is no generated runtime object, so nothing extra ships to the client and no reverse mapping is produced.

Step 2: Know what an enum adds

A string enum provides a named runtime object you can iterate, and a numeric enum adds reverse mapping (Direction[0] returns 'Up'). That runtime presence is the main difference: it is useful when you need the members as values at runtime, and a cost when you do not.

Step 3: Watch the subtle enum rules

Numeric enums are open: a plain number can be assigned to a numeric enum type in some positions, weakening the restriction. const enum inlines values at compile time but is incompatible with isolatedModules and some bundlers, so many teams avoid it. String enums avoid the numeric looseness but are not as widely used as unions.

Worked scenario

A union gives checked values that survive serialization unchanged.

type Status = 'idle' | 'loading' | 'done';
function label(status: Status): string {
  switch (status) {
    case 'idle': return 'Waiting';
    case 'loading': return 'Loading';
    case 'done': return 'Finished';
  }
}
const current: Status = 'done';
console.log(label(current)); // Finished
TypeScript

Walk through the example

The switch is exhaustive: adding a new member to Status makes label fail to return on all paths under strict, which the compiler reports. current accepts only the three literals, and 'done' round-trips through JSON as the same string, so no serialization mapping is needed. The whole construct disappears after compilation.

Common mistake

Reaching for a numeric enum and then comparing serialized values against strings, because the number 0 does not equal 'idle'. Also, relying on a const enum and then switching to isolatedModules or a build tool that compiles each file separately, which cannot inline it.

Verify the behavior

Add a new member to the union and confirm the exhaustive switch now reports a missing case. Compare the emitted JavaScript for a string enum and a union to see the runtime object one produces and the other does not. Assign an arbitrary number to a numeric enum type and observe whether the compiler accepts it.

Interview exercise

An API returns "active" | "paused" and you must both restrict values and send them back unchanged. Enum or union?

Answer and reasoning

A union of the string literals. It restricts the value at compile time, serializes to the exact same strings the API expects, and needs no runtime object or mapping. An enum would add a runtime value and offer no additional safety here, since the wire format is already the string.

Continue learning

Combine this with discriminated unions and never and exhaustiveness. Read the TypeScript enums handbook and try the TypeScript interview questions.

More in TypeScript

read ✓TypeScript · hard

TypeScript Abstract Classes and Contracts

Share behavior with abstract classes, enforce required members, and decide when an interface or composition is the better contract.

~2 min readread →
esc