Union and intersection types
Two ways to combine types: union means "one of these," intersection means "all of these at once." They sound like opposites and mostly are, but they compose in ways that trip people up the first few times.
4 min read
Union: "this value is one of these types"
type Status = "pending" | "confirmed" | "cancelled";
let orderStatus: Status = "pending";
orderStatus = "shipped"; // Error: Type '"shipped"' is not assignable to type 'Status'
function formatId(id: string | number) {
// id could genuinely be either — see the narrowing lesson for how to safely use it
}A | B means a value is either an A or a B — narrowing (its own lesson) is what a function actually does to figure out which one it has at any given moment. Status above is a union of three specific string literal types, not the general string type — this is the standard, idiomatic TypeScript way to express "one of a fixed, known set of string values," meaningfully stricter than a bare string that would accept any text at all.
Intersection: "this value is all of these types simultaneously"
type Timestamped = { createdAt: Date };
type Named = { name: string };
type NamedRecord = Timestamped & Named; // has BOTH createdAt and name
const record: NamedRecord = { createdAt: new Date(), name: "Ada" };A & B means a value must satisfy both A and B at once — every field from Timestamped and every field from Named has to be present. This is how you compose several smaller, focused type definitions into one combined shape without redeclaring every field again from scratch — the same motivation behind mixing in multiple small interfaces, expressed with & on type aliases instead.
The counterintuitive part: intersecting primitives usually produces never
type Impossible = string & number; // type Impossible = never
function f(x: Impossible) { }
// This function can never actually be called with a real value —
// nothing is simultaneously both a string and a number& on two incompatible primitive types doesn't error — it silently computes to never, TypeScript's type for "a value that can't exist." This makes sense once stated plainly (nothing is simultaneously a genuine string and a genuine number), but it's a common, confusing first encounter, especially when it shows up indirectly — a generic type parameter accidentally intersected with an incompatible constraint produces the same never, with an error that surfaces far from the actual mistake.
Unions of object types: only shared fields are safely accessible without narrowing
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; sideLength: number };
type Shape = Circle | Square;
function area(shape: Shape) {
shape.radius; // Error: Property 'radius' does not exist on type 'Shape'
// (doesn't exist on Square)
}Given a union of two different object shapes, TypeScript only lets you safely access fields that exist on every member of the union — radius exists on Circle but not Square, so accessing it directly on a bare Shape is an error, since the value might genuinely be a Square at that point. This is exactly the situation the narrowing lesson's discriminated unions solve — checking shape.kind first is what lets TypeScript know which specific member of the union you're actually holding, and only then does the shape-specific field become safely accessible.
unknown behaves like the identity element for intersection, never for union
type A = string & unknown; // A is string — unknown adds no constraint
type B = string | never; // B is string — never contributes nothing to a unionunknown intersected with anything just gives back that same thing, unchanged — it's the type equivalent of "no additional constraint," which makes sense given unknown already means "could be anything." never unioned with anything similarly contributes nothing — a value that's never | string is just string, since "or an impossible value" adds no real alternative. These identities aren't things you'll write by hand often, but they show up naturally out of generic code, and recognizing them explains why a computed type sometimes "simplifies" in ways that look surprising until you know the rule.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does the union type `"pending" | "confirmed" | "cancelled"` actually restrict a value to?
2. What does `type Impossible = string & number` actually evaluate to, and why?
3. Given `type Shape = Circle | Square` (two different object shapes), why can't you safely access `shape.radius` directly without narrowing first?