Union Types
Union Types
Union types allow a variable to hold one of several types.
Basic Unions
// A variable can be string OR number
let id: string | number;
id = 'abc-123'; // OK
id = 42; // OK
// id = true; // Error
// Function with union parameter
function formatId(id: string | number): string {
if (typeof id === 'string') {
return id.toUpperCase();
}
return id.toString().padStart(5, '0');
}
formatId('abc'); // 'ABC'
formatId(42); // '00042'
Union of Object Types
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
side: number;
}
type Shape = Circle | Square;
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
}
}
Nullable Types
// With strictNullChecks
let name: string | null = 'Alice';
name = null; // OK
// name = undefined; // Error
function greet(name: string | null): string {
return name ? `Hello, ${name}` : 'Hello, stranger';
}
Enums
Enums
Enums define a set of named constants.
Numeric Enums
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
const move: Direction = Direction.Up;
console.log(move); // 0
// Explicit values
enum HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500
}
function getStatus(code: HttpStatus): string {
switch (code) {
case HttpStatus.OK: return 'Success';
case HttpStatus.NotFound: return 'Not Found';
case HttpStatus.ServerError: return 'Server Error';
}
}
String Enums
enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE'
}
const c: Color = Color.Red;
console.log(c); // 'RED'
// String enums are better for debugging and serialization
Heterogeneous Enums
enum BooleanLike {
No = 0,
Yes = 'YES'
}
Enums in Objects
enum Status {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending'
}
// Use enums as object keys
const translations: Record<Status, string> = {
[Status.Active]: 'Activo',
[Status.Inactive]: 'Inactivo',
[Status.Pending]: 'Pendiente'
};
Discriminated Unions
Discriminated Unions
Use a common literal property (discriminant) to distinguish between union members.
Pattern with kind Discriminant
interface TextMessage {
kind: 'text';
content: string;
}
interface ImageMessage {
kind: 'image';
url: string;
width: number;
height: number;
}
interface VideoMessage {
kind: 'video';
url: string;
duration: number;
}
type Message = TextMessage | ImageMessage | VideoMessage;
function processMessage(msg: Message): string {
switch (msg.kind) {
case 'text':
return `Text: ${msg.content}`;
case 'image':
return `Image: ${msg.width}x${msg.height}`;
case 'video':
return `Video: ${msg.duration}s`;
}
}
Exhaustive Checking
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function processMessage(msg: Message) {
switch (msg.kind) {
case 'text': return msg.content;
case 'image': return msg.url;
case 'video': return msg.url;
default:
return assertNever(msg); // compile error if case missing
}
}
Real-World Example: API Response
interface SuccessResponse {
status: 'success';
data: unknown;
}
interface ErrorResponse {
status: 'error';
message: string;
code: number;
}
interface LoadingResponse {
status: 'loading';
}
type ApiResponse = SuccessResponse | ErrorResponse | LoadingResponse;
function handleResponse(res: ApiResponse) {
switch (res.status) {
case 'success': return res.data;
case 'error': throw new Error(res.message);
case 'loading': return null;
}
}
Const Enums and Best Practices
Const Enums and Best Practices
Const Enums
// Const enums are inlined at compile time - no runtime object created
const enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}
const move = Direction.Up;
// Compiles to: const move = 'UP';
When to Use What
| Use Case | Recommendation |
|---|---|
| Named constants | Enums or union of literals |
| Finite states | Discriminated unions |
| Configuration | Const object with as const |
| External data | Union types |
Union vs Enum
// Union of literals (preferred for most cases)
type Status = 'active' | 'inactive' | 'pending';
// Enum (better when you need runtime values or iteration)
enum StatusEnum {
Active = 'active',
Inactive = 'inactive',
Pending = 'pending'
}
// Const object (good middle ground)
const Status = {
Active: 'active',
Inactive: 'inactive',
Pending: 'pending'
} as const;
type Status2 = typeof Status[keyof typeof Status];
Common Pitfalls
// Numeric enums have reverse mapping
color enum Color {
Red,
Green,
Blue
}
Color[0]; // 'Red' - reverse mapping exists
// This can cause unexpected behavior
if (Color.Red === 0) {
console.log('This is true');
}
// Avoid numeric enums for this reason - use string enums instead
- Prefer string enums over numeric enums for clarity
- Use discriminated unions instead of type guards
- Use
const enumwhen you need compile-time inlining