Union, intersection types in TypeScript. What they are and when to use them.
- Unions ( | ) - in TypeScript it means that the value can be one of many types (e.g., A or B or C, etc.)
1type Status = "success" | "error" | "loading"; 2 3let state: Status; 4 5state = "success"; // ✅ 6state = "error"; // ✅ 7state = "pending"; // ❌ Error: not assignable 8
When to use?
- Modeling limited sets of string values ('on' | 'off')
- Handling errors or nulls (User | null)
- Backend responses that may vary depending on the state or source
- API status types, variants, enums, and more
- Intersections (&) - in TypeScript it means that the value must satisfy all given types at once
1type WithId = { id: string }; 2type WithTimestamps = { createdAt: Date; updatedAt: Date }; 3 4type Entity = WithId & WithTimestamps; 5 6const post: Entity = { 7 id: 'abc123', 8 createdAt: new Date(), 9 updatedAt: new Date(), 10}; 11
When to use?
- You want to combine reusable pieces of type logic
- Extending types in a composable way
- Merging types from different domains (e.g. DB record + UI metadata)
⚠️ Common Mistake: Confusing | and &
1type A = { foo: string }; 2type B = { bar: number }; 3 4type Wrong = A | B; // either A or B 5type Right = A & B; // both A and B 6 7const w1: Wrong = { foo: 'yes' }; // ✅ 8const w2: Wrong = { bar: 123 }; // ✅ 9const w3: Wrong = { foo: 'yes', bar: 123 }; // ✅, but not type-safe unless narrowed 10 11const r: Right = { foo: 'hi', bar: 123 }; // ✅ must have both 12
Discriminated Unions – the Real Power Move
Discriminated Unions (also called tagged unions or algebraic data types) are one of the most powerful patterns in TypeScript. They allow you to write code that is both flexible and fully type-safe — without unnecessary casting or manual checks. It's a special kind of union type where:
- Each variant shares a common field (called the discriminator)
- That field has a unique literal value in each variant
- Each variant has its own structure (shape)
This allows TypeScript to automatically narrow the type based on that field, giving you perfect IntelliSense and safety.
🔍 Example:
1type Shape = 2 | { kind: 'circle'; radius: number } 3 | { kind: 'square'; size: number } 4 | { kind: 'rectangle'; width: number; height: number }; 5
- Kind is the discriminator
- The value of kind identifies which exact variant you're dealing with
- TypeScript uses this field to narrow types automatically
✅ In practice:
1type Shape = 2 | { kind: 'circle'; radius: number } 3 | { kind: 'square'; size: number } 4 | { kind: 'rectangle'; width: number; height: number }; 5 6function getArea(shape: Shape): number { 7 switch (shape.kind) { 8 case 'circle': 9 return Math.PI * shape.radius ** 2; 10 case 'square': 11 return shape.size ** 2; 12 case 'rectangle': 13 return shape.width * shape.height; 14 } 15} 16
✅ Why it’s powerful:
- Compiler ensures exhaustiveness (especially with never)
- You avoid manually checking for individual keys
- Cleaner and safer than duck-typing
Type Narrowing with Unions TypeScript can automatically narrow union types based on logic checks:
1type Result = string | number; 2 3function printFormatted(result: Result) { 4 if (typeof result === 'string') { 5 console.log(result.toUpperCase()); 6 } else { 7 console.log(result.toFixed(2)); 8 } 9} 10
It works with:
- typeof (for primitives)
- in (for property checks)
- equality checks
- instanceof (for class instances)
- discriminated union fields (kind, type, status, etc.)