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)); // FinishedWalk 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.