Back to Blog
TypeScriptEnumsUnionsTypes

TypeScript Enums vs Union Types

Compare TypeScript enums and union types. Learn when to use each and their trade-offs.

B
Bootspring Team
Engineering
June 18, 2020
7 min read

Both enums and union types define a set of allowed values. Here's when to use each.

Basic Enum#

1// Numeric enum (default) 2enum Direction { 3 Up, // 0 4 Down, // 1 5 Left, // 2 6 Right, // 3 7} 8 9const move = Direction.Up; 10console.log(move); // 0 11 12// String enum 13enum Status { 14 Pending = 'PENDING', 15 Active = 'ACTIVE', 16 Completed = 'COMPLETED', 17} 18 19const status = Status.Active; 20console.log(status); // 'ACTIVE' 21 22// Heterogeneous enum (not recommended) 23enum Mixed { 24 No = 0, 25 Yes = 'YES', 26}

Basic Union Type#

1// String literal union 2type Direction = 'up' | 'down' | 'left' | 'right'; 3 4const move: Direction = 'up'; 5 6// Number literal union 7type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; 8 9const roll: DiceRoll = 4; 10 11// Mixed literal union 12type Status = 'pending' | 'active' | 0 | 1;

Const Assertions#

1// Object as const (enum-like) 2const Direction = { 3 Up: 'UP', 4 Down: 'DOWN', 5 Left: 'LEFT', 6 Right: 'RIGHT', 7} as const; 8 9type Direction = typeof Direction[keyof typeof Direction]; 10// 'UP' | 'DOWN' | 'LEFT' | 'RIGHT' 11 12// Usage 13function move(direction: Direction) { 14 console.log(direction); 15} 16 17move(Direction.Up); // 'UP' 18move('UP'); // Also valid 19 20// Array as const 21const STATUSES = ['pending', 'active', 'completed'] as const; 22type Status = typeof STATUSES[number]; 23// 'pending' | 'active' | 'completed'

Runtime Behavior#

1// Enums exist at runtime 2enum Color { 3 Red = 'RED', 4 Green = 'GREEN', 5 Blue = 'BLUE', 6} 7 8// Compiled JavaScript: 9// var Color; 10// (function (Color) { 11// Color["Red"] = "RED"; 12// Color["Green"] = "GREEN"; 13// Color["Blue"] = "BLUE"; 14// })(Color || (Color = {})); 15 16// Can iterate over enum 17Object.values(Color); // ['RED', 'GREEN', 'BLUE'] 18Object.keys(Color); // ['Red', 'Green', 'Blue'] 19 20// Union types don't exist at runtime 21type ColorUnion = 'red' | 'green' | 'blue'; 22// No JavaScript output - types are erased 23 24// Need explicit array for iteration 25const colors = ['red', 'green', 'blue'] as const; 26type Color = typeof colors[number];

Const Enums#

1// Const enum - inlined at compile time 2const enum Direction { 3 Up = 'UP', 4 Down = 'DOWN', 5} 6 7const move = Direction.Up; 8// Compiled to: const move = 'UP'; 9 10// No runtime object created 11// Cannot iterate over const enum 12// Object.values(Direction); // Error! 13 14// Benefits: 15// - Smaller bundle size 16// - No runtime overhead 17// - Better performance 18 19// Drawbacks: 20// - Can't use computed values 21// - Can't iterate 22// - Issues with --isolatedModules

Type Safety Comparison#

1// Enum: allows any number for numeric enums 2enum NumericStatus { 3 Pending, 4 Active, 5} 6 7function setStatus(status: NumericStatus) { 8 console.log(status); 9} 10 11setStatus(NumericStatus.Pending); // OK 12setStatus(0); // OK 13setStatus(999); // Also OK! No error (unsafe) 14 15// String enum: type safe 16enum StringStatus { 17 Pending = 'PENDING', 18 Active = 'ACTIVE', 19} 20 21function setStringStatus(status: StringStatus) { 22 console.log(status); 23} 24 25setStringStatus(StringStatus.Pending); // OK 26setStringStatus('PENDING'); // Error! Must use enum member 27 28// Union: type safe 29type UnionStatus = 'pending' | 'active'; 30 31function setUnionStatus(status: UnionStatus) { 32 console.log(status); 33} 34 35setUnionStatus('pending'); // OK 36setUnionStatus('invalid'); // Error!

Discriminated Unions#

1// Union types excel at discriminated unions 2type Shape = 3 | { kind: 'circle'; radius: number } 4 | { kind: 'rectangle'; width: number; height: number } 5 | { kind: 'triangle'; base: number; height: number }; 6 7function area(shape: Shape): number { 8 switch (shape.kind) { 9 case 'circle': 10 return Math.PI * shape.radius ** 2; 11 case 'rectangle': 12 return shape.width * shape.height; 13 case 'triangle': 14 return (shape.base * shape.height) / 2; 15 } 16} 17 18// Enum version (more verbose) 19enum ShapeKind { 20 Circle = 'CIRCLE', 21 Rectangle = 'RECTANGLE', 22 Triangle = 'TRIANGLE', 23} 24 25type ShapeWithEnum = 26 | { kind: ShapeKind.Circle; radius: number } 27 | { kind: ShapeKind.Rectangle; width: number; height: number } 28 | { kind: ShapeKind.Triangle; base: number; height: number };

Bundle Size#

1// Union type: 0 bytes at runtime 2type Status = 'pending' | 'active' | 'completed'; 3 4// Enum: generates runtime code 5enum StatusEnum { 6 Pending = 'PENDING', 7 Active = 'ACTIVE', 8 Completed = 'COMPLETED', 9} 10// ~150 bytes minified 11 12// Const enum: 0 bytes (inlined) 13const enum StatusConstEnum { 14 Pending = 'PENDING', 15 Active = 'ACTIVE', 16 Completed = 'COMPLETED', 17} 18 19// Object as const: only values you use 20const STATUS = { 21 Pending: 'PENDING', 22 Active: 'ACTIVE', 23 Completed: 'COMPLETED', 24} as const; 25// Tree-shakeable!

API Design#

1// Enum: good for internal APIs 2enum InternalStatus { 3 Draft = 'DRAFT', 4 Published = 'PUBLISHED', 5} 6 7// Union: better for external APIs 8type ExternalStatus = 'draft' | 'published'; 9 10// API response can use union 11interface ApiResponse { 12 status: 'success' | 'error'; 13 data?: unknown; 14 error?: string; 15} 16 17// Form values often use unions 18interface FormState { 19 status: 'idle' | 'submitting' | 'success' | 'error'; 20}

Extending and Combining#

1// Unions are easily composable 2type BaseStatus = 'pending' | 'active'; 3type ExtendedStatus = BaseStatus | 'archived' | 'deleted'; 4 5// Enums cannot be extended 6enum BaseStatusEnum { 7 Pending = 'PENDING', 8 Active = 'ACTIVE', 9} 10 11// Must create new enum 12enum ExtendedStatusEnum { 13 Pending = 'PENDING', 14 Active = 'ACTIVE', 15 Archived = 'ARCHIVED', 16 Deleted = 'DELETED', 17} 18 19// Or use spread with const objects 20const BASE_STATUS = { 21 Pending: 'PENDING', 22 Active: 'ACTIVE', 23} as const; 24 25const EXTENDED_STATUS = { 26 ...BASE_STATUS, 27 Archived: 'ARCHIVED', 28 Deleted: 'DELETED', 29} as const;

Working with Values#

1// Get all enum values 2enum Color { 3 Red = 'RED', 4 Green = 'GREEN', 5 Blue = 'BLUE', 6} 7 8const colorValues = Object.values(Color); 9// ['RED', 'GREEN', 'BLUE'] 10 11// Check if value is valid enum 12function isColor(value: string): value is Color { 13 return Object.values(Color).includes(value as Color); 14} 15 16// Get all union values (need separate array) 17const colors = ['red', 'green', 'blue'] as const; 18type ColorUnion = typeof colors[number]; 19 20function isColorUnion(value: string): value is ColorUnion { 21 return colors.includes(value as ColorUnion); 22} 23 24// Reverse mapping (numeric enums only) 25enum NumericDirection { 26 Up, 27 Down, 28} 29 30NumericDirection[0]; // 'Up' 31NumericDirection.Up; // 0

Recommendations#

1// Use union types for: 2// - Simple string/number literals 3// - External API types 4// - Discriminated unions 5// - When bundle size matters 6type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; 7type LogLevel = 'debug' | 'info' | 'warn' | 'error'; 8 9// Use const objects for: 10// - Need runtime values 11// - Want to iterate 12// - Namespace organization 13const HTTP_METHODS = { 14 Get: 'GET', 15 Post: 'POST', 16 Put: 'PUT', 17 Delete: 'DELETE', 18} as const; 19 20// Use string enums for: 21// - Legacy codebases 22// - When team prefers them 23// - IDE autocomplete preference 24enum Permission { 25 Read = 'READ', 26 Write = 'WRITE', 27 Admin = 'ADMIN', 28} 29 30// Avoid numeric enums (type safety issues)

Best Practices#

Union Types: ✓ Prefer for most use cases ✓ Zero runtime overhead ✓ Better tree-shaking ✓ Easy composition Const Objects: ✓ When you need runtime values ✓ For iteration over values ✓ Namespace organization ✓ Tree-shakeable String Enums: ✓ Team familiarity ✓ Legacy codebase compatibility ✓ IDE autocomplete preference ✓ Reverse mapping not needed Avoid: ✗ Numeric enums (type safety issues) ✗ Heterogeneous enums ✗ Computed enum values ✗ Over-engineering simple cases

Conclusion#

Prefer union types for most cases—they're simpler, have zero runtime cost, and compose well. Use const objects when you need runtime values and iteration. String enums are acceptable for team preference but avoid numeric enums due to type safety issues.

Share this article

Help spread the word about Bootspring