Lesson 7 of 25
Union and Intersection Types
Combine types with | for "one of these" and & for "all of these", and use literal types to describe a fixed set of exact values.
Real data is rarely just "a string" or "a number" in isolation — it's often one of several possible shapes, or a combination of several requirements at once. Unions and intersections are how TypeScript expresses both.
Union types: "one of these"
A union, written with |, describes a value that could be any one of
several types:
function formatId(id: string | number): string {
return `#${id}`;
}
formatId("abc123"); // fine
formatId(42); // fine
formatId(true);
// Argument of type 'boolean' is not assignable to
// parameter of type 'string | number'.Inside the function, TypeScript only lets you use members common to every type in the union, unless you first check which one you actually have:
function double(value: string | number) {
return value * 2;
// Operator '*' cannot be applied to types 'string | number' and 'number'.
}That's not TypeScript being overcautious — "5" * 2 and 5 * 2 behave
differently in JavaScript, so the compiler is right to insist you settle
which case you're in before doing arithmetic. That's the subject of the next
lesson, narrowing.
Literal types
A string or number can be typed down to one exact value, not just its general type:
let direction: "left";
direction = "left"; // fine
direction = "right";
// Type '"right"' is not assignable to type '"left"'.A single literal type on its own isn't very useful — the power comes from unioning several together to describe a closed, exact set of allowed values:
type Direction = "left" | "right" | "up" | "down";
function move(direction: Direction) {
/* ... */
}
move("left"); // fine
move("forward");
// Argument of type '"forward"' is not assignable
// to parameter of type 'Direction'.This is a genuinely common and useful pattern — a Direction union like
this is stricter than string (which would accept literally any string,
typos included) and, for a small fixed set of options, is usually preferred
over an enum, covered a couple of lessons from now.
Discriminated unions, previewed
Unions of objects — not just primitives — are one of the most useful patterns in the entire language, usually built around one shared literal property that identifies which shape you have:
type Loading = { status: "loading" };
type Success = { status: "success"; data: string };
type Failure = { status: "error"; message: string };
type FetchState = Loading | Success | Failure;Checking state.status narrows which of the three shapes you're holding —
covered in full in the next lesson, since it depends on narrowing to be
useful at all.
Intersection types: "all of these"
Where | means "or," & means "and" — the resulting type must satisfy
every combined type at once:
type WithId = { id: string };
type WithTimestamp = { createdAt: Date };
type Record = WithId & WithTimestamp;
const record: Record = {
id: "abc123",
createdAt: new Date(),
};Leaving out either property is now an error — Record requires both id
and createdAt, because it's the intersection of both types. This is the
type-based equivalent of an interface extending two others, mentioned in
the previous lesson.
Intersections are common for combining smaller, focused shapes into a larger one — a base entity type intersected with feature-specific fields, for instance — rather than duplicating every field into one large interface by hand.
Unions vs intersections: a memory aid
The naming genuinely matches set theory, and it's worth internalising rather than memorising by rote: a union of types is the type that accepts values from any of the source types (a bigger, looser set of allowed values), while an intersection requires a value to belong to all of the source types at once (a narrower, stricter set — usually more properties required, not fewer).
Try it yourself
What to remember
- "|" (union) means "could be any one of these" — a broader, more permissive type.
- "&" (intersection) means "must satisfy all of these at once" — a narrower, stricter type, usually with more required properties.
- A literal type pins a primitive to one exact value; unioning several together describes a closed set of allowed values, often better than plain
string. - Using a union safely requires narrowing which member you actually have — covered next.
Check yourself
4 questions · pass 3/4 to unlock Narrowing
1.What values does
string | numberallow?2.Given
type Both = { a: string } & { b: number };, what must a value of type Both contain?3.What is
"success"as a type, intype Result = "success" | "error";?4.A function returns
string | undefined. What must happen before calling.toUpperCase()on that result?
4 left to answer