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 --isolatedModulesType 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; // 0Recommendations#
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.