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.
| Construct | Syntax | Meaning |
|---|---|---|
| Union | A | B | Value is either A or B |
| Intersection | A & B | Value must satisfy both A and B |
| Discriminated union | Shared literal field | Tagged 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; // neverUseful 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 fieldsDistributive 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
switchplusneverexhaustive 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
statusortypefield beat boolean flags and co-located optional fields. Pair with exhaustiveswitchandneverfor 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 machineBest 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