Back to Blog
TypeScriptEnumsPatternsBest Practices

TypeScript Enums and Better Alternatives

Understand TypeScript enums and their alternatives. From const enums to string unions to object patterns.

B
Bootspring Team
Engineering
August 12, 2021
6 min read

TypeScript enums have quirks. Here's when to use them and better alternatives.

Numeric Enums#

1// Basic numeric enum 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// Custom values 13enum Status { 14 Pending = 1, 15 Active = 2, 16 Completed = 10, 17 Cancelled = 20, 18} 19 20// Problem: Reverse mapping allows invalid values 21const status: Status = 999; // No error! 22 23// Computed values 24enum FileAccess { 25 None = 0, 26 Read = 1 << 0, // 1 27 Write = 1 << 1, // 2 28 ReadWrite = Read | Write, // 3 29} 30 31// Bitwise operations 32const access = FileAccess.Read | FileAccess.Write; 33const canRead = (access & FileAccess.Read) !== 0;

String Enums#

1// String enum - more explicit 2enum Color { 3 Red = 'RED', 4 Green = 'GREEN', 5 Blue = 'BLUE', 6} 7 8const color: Color = Color.Red; 9console.log(color); // 'RED' 10 11// No reverse mapping 12// Color['RED'] is undefined 13 14// Safer, but still has issues 15enum OrderStatus { 16 Pending = 'pending', 17 Processing = 'processing', 18 Shipped = 'shipped', 19 Delivered = 'delivered', 20} 21 22// Problem: Can assign any string to enum value 23function processOrder(status: OrderStatus) { 24 // status is actually just a string at runtime 25}

Const Enums#

1// Const enum - inlined at compile time 2const enum HttpStatus { 3 OK = 200, 4 NotFound = 404, 5 ServerError = 500, 6} 7 8const status = HttpStatus.OK; 9// Compiles to: const status = 200; 10 11// No runtime object exists 12// Can't iterate over values 13// Can't use computed expressions 14 15// Benefits: 16// - Smaller bundle size 17// - Better performance 18// - No runtime overhead 19 20// Limitations: 21// - No reverse mapping 22// - Can't be exported from ambient contexts 23// - isolatedModules compatibility issues

String Literal Unions (Preferred)#

1// Better alternative to string enums 2type Direction = 'up' | 'down' | 'left' | 'right'; 3 4function move(direction: Direction) { 5 switch (direction) { 6 case 'up': 7 return { y: -1 }; 8 case 'down': 9 return { y: 1 }; 10 case 'left': 11 return { x: -1 }; 12 case 'right': 13 return { x: 1 }; 14 } 15} 16 17// Type-safe! 18move('up'); // ✓ 19move('Up'); // ✗ Error 20move('foo'); // ✗ Error 21 22// Exhaustiveness checking works 23function exhaustive(direction: Direction): never { 24 // TypeScript errors if not all cases handled 25} 26 27// Extract values 28const directions = ['up', 'down', 'left', 'right'] as const; 29type Direction2 = typeof directions[number]; 30 31// Check if value is valid 32function isDirection(value: string): value is Direction { 33 return ['up', 'down', 'left', 'right'].includes(value); 34}

Object as Enum#

1// Object pattern - runtime values + type safety 2const Color = { 3 Red: 'red', 4 Green: 'green', 5 Blue: 'blue', 6} as const; 7 8type Color = typeof Color[keyof typeof Color]; 9// type Color = 'red' | 'green' | 'blue' 10 11// Usage 12function setColor(color: Color) { 13 console.log(color); 14} 15 16setColor(Color.Red); // ✓ 17setColor('red'); // ✓ 18setColor('purple'); // ✗ Error 19 20// Get all values 21const colorValues = Object.values(Color); 22// ['red', 'green', 'blue'] 23 24// Get all keys 25const colorKeys = Object.keys(Color) as (keyof typeof Color)[]; 26// ['Red', 'Green', 'Blue'] 27 28// More complex object enum 29const HttpStatus = { 30 OK: { code: 200, message: 'OK' }, 31 NotFound: { code: 404, message: 'Not Found' }, 32 ServerError: { code: 500, message: 'Internal Server Error' }, 33} as const; 34 35type HttpStatusKey = keyof typeof HttpStatus; 36type HttpStatusValue = typeof HttpStatus[HttpStatusKey]; 37 38function handleStatus(status: HttpStatusKey) { 39 const { code, message } = HttpStatus[status]; 40 console.log(`${code}: ${message}`); 41}

Discriminated Unions#

1// For complex state 2type LoadingState = { status: 'loading' }; 3type SuccessState<T> = { status: 'success'; data: T }; 4type ErrorState = { status: 'error'; error: Error }; 5 6type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState; 7 8function handleState<T>(state: AsyncState<T>) { 9 switch (state.status) { 10 case 'loading': 11 return 'Loading...'; 12 case 'success': 13 return state.data; // TypeScript knows data exists 14 case 'error': 15 return state.error.message; 16 } 17} 18 19// More practical example 20type PaymentMethod = 21 | { type: 'card'; cardNumber: string; cvv: string } 22 | { type: 'bank'; accountNumber: string; routingNumber: string } 23 | { type: 'paypal'; email: string }; 24 25function processPayment(method: PaymentMethod) { 26 switch (method.type) { 27 case 'card': 28 return chargeCard(method.cardNumber, method.cvv); 29 case 'bank': 30 return bankTransfer(method.accountNumber, method.routingNumber); 31 case 'paypal': 32 return paypalCheckout(method.email); 33 } 34}

Helper Utilities#

1// Create enum-like object with type safety 2function createEnum<T extends readonly string[]>(values: T) { 3 const obj = {} as { [K in T[number]]: K }; 4 for (const value of values) { 5 (obj as any)[value] = value; 6 } 7 return Object.freeze(obj); 8} 9 10const Status = createEnum(['pending', 'active', 'completed'] as const); 11type Status = keyof typeof Status; 12// type Status = 'pending' | 'active' | 'completed' 13 14// Enum with descriptions 15function createEnumWithLabels<T extends Record<string, string>>(obj: T) { 16 return Object.freeze(obj); 17} 18 19const OrderStatus = createEnumWithLabels({ 20 pending: 'Order Pending', 21 processing: 'Order Processing', 22 shipped: 'Order Shipped', 23 delivered: 'Order Delivered', 24}); 25 26type OrderStatusKey = keyof typeof OrderStatus; 27// 'pending' | 'processing' | 'shipped' | 'delivered' 28 29// Get label 30const label = OrderStatus.pending; // 'Order Pending'

When to Use What#

1// Use string literal unions for simple cases 2type Theme = 'light' | 'dark'; 3 4// Use const object for runtime access to values 5const Theme = { 6 Light: 'light', 7 Dark: 'dark', 8} as const; 9type Theme = typeof Theme[keyof typeof Theme]; 10 11// Use discriminated unions for complex state 12type RequestState<T> = 13 | { status: 'idle' } 14 | { status: 'loading' } 15 | { status: 'success'; data: T } 16 | { status: 'error'; error: Error }; 17 18// Use numeric enum for flags/bitwise operations 19enum Permission { 20 None = 0, 21 Read = 1 << 0, 22 Write = 1 << 1, 23 Execute = 1 << 2, 24} 25 26// Avoid regular enums when possible 27// They have confusing runtime behavior

Migration from Enums#

1// Before: TypeScript enum 2enum OldStatus { 3 Pending = 'PENDING', 4 Active = 'ACTIVE', 5 Completed = 'COMPLETED', 6} 7 8// After: Const object 9const Status = { 10 Pending: 'PENDING', 11 Active: 'ACTIVE', 12 Completed: 'COMPLETED', 13} as const; 14 15type Status = typeof Status[keyof typeof Status]; 16 17// Migration is mostly search-and-replace 18// OldStatus.Pending -> Status.Pending 19// Works the same at runtime 20 21// Type annotations update 22// (status: OldStatus) -> (status: Status)

Best Practices#

Prefer: ✓ String literal unions for types ✓ Const objects for runtime values ✓ Discriminated unions for state ✓ const enum only when needed Avoid: ✗ Regular numeric enums ✗ Heterogeneous enums ✗ Enums when simple types work ✗ Over-engineering simple cases Consider: ✓ Bundle size impact ✓ Runtime iteration needs ✓ Type safety requirements ✓ Team familiarity

Conclusion#

TypeScript enums have their place but often better alternatives exist. String literal unions provide type safety without runtime overhead. Const objects give both runtime values and type safety. Discriminated unions handle complex state elegantly. Reserve enums for bitwise operations or when you specifically need their behavior.

Share this article

Help spread the word about Bootspring