Topics
v5.0.0
On this page · sections

Union & Intersection Types

Unions express “one of several alternatives.” Intersections combine multiple shapes into one. Together they model most frontend state machines, API payloads, and component prop variants.

ConstructSyntaxMeaning
UnionA | BValue is either A or B
IntersectionA & BValue must satisfy both A and B
Discriminated unionShared literal fieldTagged variant — safest object union
Literal union"a" | "b"Fixed set of string/number literals

Union types

Definition

A union type means a value can be one of several types:

type Id = string | number; type Theme = "light" | "dark"; function printId(id: Id) { if (typeof id === "string") { console.log(id.toUpperCase()); } else { console.log(id.toFixed(0)); } }

Literal unions are the workhorse of frontend APIs:

type ButtonVariant = "primary" | "secondary" | "ghost"; type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

Object unions

Object unions power most frontend state machines:

type RequestState<T> = | { status: "idle" } | { status: "loading" } | { status: "success"; data: T } | { status: "error"; message: string };

Each member is a distinct shape. You must narrow before accessing fields that are not shared.

function render<T>(state: RequestState<T>) { switch (state.status) { case "idle": return "Ready"; case "loading": return "Loading…"; case "success": return state.data; // data exists only here case "error": return state.message; } }

Interview: Unions model alternatives. Access type-specific fields only after narrowing — the compiler enforces this.

Intersection types

Definition

An intersection combines multiple types. The result must satisfy all members:

type Timestamped = { createdAt: Date; updatedAt: Date }; type Identifiable = { id: string }; type Entity = Timestamped & Identifiable; // { createdAt: Date; updatedAt: Date; id: string }

Mixin-style composition

type Admin = User & { permissions: string[] }; function promote(user: User): Admin { return { ...user, permissions: ["read", "write", "delete"] }; }

Intersection with primitives

Primitive intersections often collapse to never when incompatible:

type Impossible = string & number; // never

Useful intersections usually combine object shapes or function types.

Function intersection

type Logger = { log: (msg: string) => void }; type Metrics = { track: (event: string) => void }; type Instrumented = Logger & Metrics; const service: Instrumented = { log: (msg) => console.log(msg), track: (event) => analytics.track(event), };

Interview: & means “and also.” Use intersections to compose capabilities; use unions to model alternatives.

Union vs intersection — mental model

Definition

A | B → could be A, could be B (wider — more possibilities) A & B → must be both A and B (narrower — more requirements)
type StringOrNumber = string | number; // wider type Named = { name: string }; type Aged = { age: number }; type Person = Named & Aged; // narrower — needs both fields

Distributive behavior in conditional types (preview for advanced topics):

type ToArray<T> = T extends unknown ? T[] : never; type R = ToArray<string | number>; // string[] | number[]

Discriminated unions (tagged unions)

Definition

Every member of a discriminated union shares a discriminant — usually type, kind, or status — with a unique literal value:

type Action = | { type: "increment"; amount: number } | { type: "reset" } | { type: "set"; value: number }; function reducer(state: number, action: Action): number { switch (action.type) { case "increment": return state + action.amount; case "reset": return 0; case "set": return action.value; default: { const _exhaustive: never = action; return _exhaustive; } } }

TypeScript narrows automatically on the discriminant field — no in check needed.

Real-world example — async UI state

type AsyncState<T> = | { status: "idle" } | { status: "loading" } | { status: "success"; data: T } | { status: "error"; error: string }; function UserPanel({ state }: { state: AsyncState<User> }) { switch (state.status) { case "idle": return <p>Press load to start</p>; case "loading": return <Spinner />; case "success": return <Profile user={state.data} />; case "error": return <Alert>{state.error}</Alert>; } }

Each branch only exposes fields valid for that status — no optional data? and error? on the same object.

Form state variant

type FormState = | { step: "details"; name: string; email: string } | { step: "review"; name: string; email: string } | { step: "submitting" } | { step: "done"; confirmationId: string };

Interview: Discriminated unions plus switch plus never exhaustive check equals type-safe state machines without a state library.

Union vs optional properties

Definition

Optional fields on a single object allow invalid combinations:

// ❌ Allows invalid states type BadState = { status: "loading" | "success" | "error"; data?: User; error?: string; }; const invalid: BadState = { status: "success" }; // no data — still valid to TS const alsoBad: BadState = { status: "error", data: user, error: "fail" };

Discriminated unions prevent impossible states:

// ✅ Each status carries only valid fields type GoodState = | { status: "loading" } | { status: "success"; data: User } | { status: "error"; error: string };

When optional fields are fine

Optional fields work when properties are truly independent:

type UserProfile = { name: string; bio?: string; // bio is optional but not tied to a status avatarUrl?: string; };

Interview: If two optional fields are mutually exclusive by business logic, use a discriminated union instead.

Senior Interview Summary

What to say

Unions model “one of”; intersections model “all of.” For React state, discriminated unions on a status or type field beat boolean flags and co-located optional fields. Pair with exhaustive switch and never for compile-time safety.

Key distinctions

A | B → value is A or B A & B → value must satisfy both Discriminant → shared literal field enables automatic narrowing Optional? → independent omission; not a state machine

Best mental model

Union → "could be A or B" Intersection → "must be A and B" Tagged union → "status tells you which fields exist" never → "if I am here, I missed a case"

Practical rule

UI async state → discriminated union on status Mutually exclusive → union, not optional fields on one type Composing traits → intersection Fixed string set → literal union

Last updated on