Introduction
TypeScript's type system becomes expressive when you compose types instead of creating them from scratch. **Union** and **Intersection** types are two fundamental composition tools: unions express alternatives, intersections express combinations.
Union Types
A **union type** lets a value be one of several types using the `|` operator. Use unions when a variable or parameter can validly hold different shapes or primitives.
Example
// value can be string or number
type Id = string | number;
function formatId(id: Id){
if (typeof id === "number") return `#${id.toString().padStart(6, "0")}`;
return id.toUpperCase();
}
Key notes
- Type narrowing (e.g., `typeof`, `in`, user-defined type guards) is essential to work with unions safely.
- Unions are **exclusive alternatives**: a value must match at least one member of the union.
Intersection Types
An **intersection type** combines multiple types into one using the `&` operator. The resulting type must satisfy **all** constituent types.
Example
type Timestamped = { createdAt: string };
type User = { id: number; name: string };
type UserRecord = User & Timestamped;
const u: UserRecord = { id: 1, name: "Ava", createdAt: "2026-02-18T12:00:00Z" };
Key notes
- Intersections are useful to merge capabilities or constraints (e.g., mixins, combining interfaces).
- If members conflict (same property with incompatible types), the intersection becomes impossible to satisfy.
Combining Patterns and Advanced Usage
You can mix unions and intersections to model complex APIs: e.g., a function that accepts several distinct option shapes (union) where each shape shares some common fields (intersection).
Distributive conditional types and transforms
// map a union to a union of promises
type ToPromise = T extends any ? Promise : never;
type U = "a" | "b";
type PU = ToPromise; // Promise<"a"> | Promise<"b">
Advanced type transforms let you convert unions to intersections and vice versa using conditional types and helper patterns when needed.
Comparison Table
| Aspect | Union | Intersection |
|---|---|---|
| Operator | | | & |
| Meaning | Either one of the types | All types combined |
| Use case | Alternative inputs or results | Compose capabilities or constraints |
| Narrowing | Required to access specific members | All members available |
| Failure mode | Runtime checks needed | Conflicting properties make type impossible |
Best Practices
- Prefer explicit discriminants for unions (e.g., `type A = { kind: "a"; ... } | { kind: "b"; ... }`) to simplify narrowing.
- Avoid overly broad unions like `any | T`; prefer precise alternatives.
- Use intersections to express shared capabilities (e.g., `Serializable & Identifiable`).
- Keep types readable — split complex unions/intersections into named aliases and document intent.
More TS articles: