TypeScript Enums vs Union Literals
Compare TypeScript enums with string union literals. Learn the trade-offs in runtime cost, exhaustiveness, ergonomics, and when each is the right choice.
What you'll learn
- ✓How TypeScript enums compile and behave at runtime
- ✓How union literal types compare in safety and ergonomics
- ✓Trade-offs around tree-shaking, iteration, and exhaustiveness
- ✓Const enums and their pitfalls
- ✓Which to pick for new code
Prerequisites
- •Basic TypeScript
- •Comfort with union types
What and Why
TypeScript gives you two main ways to model a closed set of named values: enum and union literal types. Both let you say “this variable can only be one of red, green, or blue.” They differ sharply in how they compile, how they interact with the rest of the language, and what they cost at runtime.
The choice matters because these constructs show up everywhere: status fields, configuration flags, route names, action types in a reducer. The wrong pick can leak code into bundles, complicate testing, or simply read worse. Understanding both lets you pick the cheaper, clearer option per situation.
Mental Model
Think of an enum as a runtime object plus a type alias generated from it. The compiler emits a JavaScript object so values like Color.Red exist at runtime, and it also declares the type Color. Enums are dual citizens of the value and type worlds.
Think of a union literal as a pure type. type Color = 'red' | 'green' | 'blue' adds nothing to your bundle. The values are just strings; the type is just a constraint. There is no Color object to import, no reverse lookup, no namespace.
The mental shorthand: enums are objects with types. Unions are types over existing values.
Hands-on Example
Here is the same domain modeled twice.
// Enum version
enum Status {
Pending = 'pending',
Active = 'active',
Closed = 'closed',
}
function label(s: Status) {
switch (s) {
case Status.Pending: return 'Waiting';
case Status.Active: return 'Live';
case Status.Closed: return 'Done';
}
}
label(Status.Active);
// Union literal version
type Status = 'pending' | 'active' | 'closed';
function label(s: Status) {
switch (s) {
case 'pending': return 'Waiting';
case 'active': return 'Live';
case 'closed': return 'Done';
}
}
label('active');
Both compile-time check call sites. Both narrow inside the switch. The enum version requires importing Status everywhere, even in JSON-facing code where you just want the string. The union version uses raw strings, which match whatever the API or database already returns.
For iteration, unions need a companion array.
const STATUSES = ['pending', 'active', 'closed'] as const;
type Status = typeof STATUSES[number];
STATUSES.forEach(s => console.log(s));
The as const plus indexed access derives the type from the array, keeping the two in sync. With enums you would write Object.values(Status) and pay a runtime cost.
enum Status { Pending = 'pending', Active = 'active' }
|
v
JS output:
var Status;
(function (S) {
S["Pending"] = "pending";
S["Active"] = "active";
})(Status || (Status = {}));
type Status = 'pending' | 'active'
|
v
JS output:
(nothing, types are erased) For exhaustiveness, both work identically with never.
function ensure(x: never): never { throw new Error('unreachable'); }
// works for enum or union
Common Pitfalls
Numeric enums are the biggest footgun. They allow any number to flow in: let s: Status = 99 compiles. Prefer string enums or unions for domain values.
const enum removes runtime cost by inlining values at use sites, but it breaks under isolatedModules, certain bundlers, and ambient declarations across packages. Avoid it in libraries; it surprises consumers.
Enums in shared types between front and back end create coupling. If both sides import the enum from a shared package, version drift hurts. Strings flow more naturally across boundaries.
Union literals have their own pitfalls. Without as const, an array of strings widens to string[], losing the link to the union. And once you have many variants, the union declaration grows; some teams find an enum-style indirection more readable.
Finally, enums get a structural type identity that surprises people. Two enums with the same members are not assignable. Two unions with the same strings are interchangeable. Predict accordingly.
Best Practices
For new code, default to union literals plus a const array when iteration is needed. They are lighter, friendlier to bundlers, and play nicely with JSON.
Reach for string enums when you need a namespace, when you want auto-imported member access, or when you maintain code that already uses them and consistency matters.
Avoid numeric enums except when interoperating with bit flags or external APIs that require them.
Skip const enum in shared library code. The compatibility cost is rarely worth the saved bytes.
Always include an exhaustiveness check with never so adding a new variant produces a compile error rather than a silent bug.
Wrap-up
Enums and union literals solve the same problem with different trade-offs. Enums give you a runtime object and a namespace at the cost of bundle size and footguns. Union literals give you pure types that compile away entirely, at the cost of a little ceremony for iteration.
For most new TypeScript code, union literals with as const are the modern default. They are smaller, simpler, and align with how strings flow through real systems. Use enums when their dual nature genuinely earns its weight.
Related articles
- TypeScript TypeScript Branded Types Tutorial
Use branded types in TypeScript to add nominal safety on top of structural types. Stop mixing up UserIds, emails, and raw strings at compile time.
- TypeScript TypeScript Conditional Types Tutorial
Learn conditional types in TypeScript: distribution, infer, and how to build expressive utility types that adapt to the input shape.
- TypeScript TypeScript Discriminated Unions
How to model variant types in TypeScript with discriminated unions: design, narrowing, exhaustiveness checks, and real-world patterns.
- TypeScript TypeScript Generics with React
A practical guide to using TypeScript generics in React components, hooks, and props for safer, more reusable building blocks.