Union and Intersection Types
Combine types with | and & — model alternatives, mixins, and why unions need narrowing while intersections require all members.
- typescript
- union-and
Unions (A | B) mean “one of.” Intersections (A & B) mean “all of.” Most frontend modeling is unions for state and props variants; intersections show up when composing object shapes.
Docs: Union Types, Intersection Types.
Unions
type Id = string | number;
type Status = 'idle' | 'loading' | 'done';
function printId(id: Id) {
console.log(String(id));
}
Only common operations are allowed without narrowing:
function len(x: string | string[]) {
// return x.length; // ok — both have length
if (typeof x === 'string') return x.toUpperCase();
return x.join(',');
}
See narrowing.
Discriminated unions
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
Prefer tags over optional fields — discriminated unions for UI state.
Intersections
type Timestamped = { createdAt: string; updatedAt: string };
type WithId = { id: string };
type Entity = WithId & Timestamped;
const e: Entity = {
id: '1',
createdAt: '2026-01-01',
updatedAt: '2026-01-02',
};
Useful for composing mixins and extending props:
type ButtonProps = { variant: 'primary' | 'secondary' };
type WithClassName = { className?: string };
type Props = ButtonProps & WithClassName;
When intersections become never
Conflicting properties:
type A = { x: string };
type B = { x: number };
type C = A & B; // x: string & number → never
That’s a design smell — fix the models.
Union of objects vs intersection
type A = { a: string };
type B = { b: number };
type U = A | B; // has a OR b (after narrowing)
type I = A & B; // must have a AND b
People mix these up in interviews — draw a Venn diagram mentally.
Narrowing checklist for unions
| Technique | Use for |
|---|---|
typeof |
primitives |
instanceof |
classes / DOM |
| equality | literals |
in |
property presence |
| discriminant field | tagged objects |
custom is guard |
complex shapes |
Practical frontend examples
Component variants (union):
type Toast =
| { type: 'success'; message: string }
| { type: 'error'; message: string; retry?: () => void };
HOC / prop merge (intersection):
type Injected = { user: User };
type Own = { title: string };
type Props = Own & Injected;
Footguns
- Optional soup instead of unions —
error?: string; data?: T. - Union with
anyor barestring— collapses precision. - Overusing intersections for variants — use unions.
- Huge unions — split modules; consider maps of configs.
Interview out-loud answer
“Unions are alternatives — narrow before using specific fields. Intersections combine requirements — the value must satisfy every part. I model UI state as discriminated unions and shared props as intersections or extends.”
Assignability intuition
A value of type A is assignable to A | B. A value of type A & B is assignable to A and to B. Function parameters reverse some intuitions (contravariance under strictFunctionTypes) — when callbacks fight you, draw the direction of data flow.
Related on this site
- Type narrowing
- Discriminated unions for UI state
- Literal types
- Interfaces vs type aliases
- never type and exhaustiveness
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.