Union & Intersection Types
Combining types with unions, intersections, and discriminated unions
TypeScript's type combinators let you mix existing types into new ones.
What you'll learn
Model data with union types (A | B)
Combine objects with intersection types (A & B)
Use discriminated unions with literal type discriminants
Write exhaustive switch statements with never
Apply template literal types to create string unions
Build real-world patterns like Result and AsyncState
Union Types
A value can be one of several types.
type Status = "idle" | "loading" | "success" | "error"
type ID = string | number
function getID(id: ID): string {
return id.toString() // works on both string and number
}
Gotcha
When accessing a union property, TypeScript only allows methods common to all members. Use narrowing to access specific members.
Intersection Types
A value satisfies all types simultaneously.
interface Named {
name: string
}
interface Aged {
age: number
}
type Person = Named & Aged
// { name: string; age: number }
const alice: Person = { name: "Alice", age: 30 }
// Merging conflicting property types — never wins
type Conflicting = { a: string } & { a: number }
// { a: never }
Discriminated Unions
A literal kind property tells TypeScript which variant you have.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number }
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2
case "rectangle":
return shape.width * shape.height
case "triangle":
return (shape.base * shape.height) / 2
}
}
Exhaustiveness with never
When you add a new variant, the compiler catches unhandled cases.
function assertNever(x: never): never {
throw new Error("Unhandled case: " + x)
}
function areaSafe(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2
case "rectangle":
return shape.width * shape.height
case "triangle":
return (shape.base * shape.height) / 2
default:
return assertNever(shape)
// ^ TypeScript error if a new Shape variant is added
}
}
Template Literal Type Unions
Generate string unions from other types.
type CSSUnit = "px" | "em" | "rem" | "%"
type CSSLength = `${number}${CSSUnit}`
// "0px" | "1px" | ... | "0em" | "1em" | ... etc.
type EventName = `on${Capitalize<"click" | "change" | "focus">}`
// "onClick" | "onChange" | "onFocus"
type Color = "primary" | "secondary"
type Size = "sm" | "md" | "lg"
type Variant = `${Color}-${Size}`
// "primary-sm" | "primary-md" | "primary-lg" | "secondary-sm" | ...
State Modeling Approaches
type ViewState = "loading" | "error" | "data" | "empty"
function render(state: ViewState) {
// No data payload — must reach for context
}
Real-World: Result and AsyncState
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }
function safeParseJSON<T>(json: string): Result<T> {
try {
return { ok: true, value: JSON.parse(json) as T }
} catch (e) {
return { ok: false, error: e instanceof Error ? e : new Error(String(e)) }
}
}
// Exhaustive rendering
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T; timestamp: number }
| { status: "error"; message: string; code: number }
function renderState<T>(s: AsyncState<T>): string {
switch (s.status) {
case "idle":
return "Awaiting action..."
case "loading":
return "Please wait..."
case "success":
return `Loaded ${JSON.stringify(s.data)}`
case "error":
return `Error ${s.code}: ${s.message}`
}
}
Model a Form State
Code
typescript
1type FormState =2// Define your discriminated union here3| { status: "idle" }4// ... add remaining states5 6function getSubmitButtonText(state: FormState): string {7// Return:8// idle -> "Submit"9// validating -> "Validating..."10// submitting -> "Submitting..."11// success -> "Done!"12// error -> "Retry"13}Key Takeaways
Union types (A | B) model values that can be one of several types.
Intersection types (A & B) merge multiple types into one.
Discriminated unions use a literal discriminant for type-safe narrowing.
assertNever + exhaustiveness checking catches unhandled variants at compile time.
Template literal types derive string unions from other types.
Pattern Result<T, E> and AsyncState<T> are ubiquitous in production TypeScript.