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 issuesString 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 behaviorMigration 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.