AniUI Academy

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.

9 min read

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

Try it yourself
Loading playground...

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

up to 50
  1. 1.What values does string | number allow?

  2. 2.Given type Both = { a: string } & { b: number };, what must a value of type Both contain?

  3. 3.What is "success" as a type, in type Result = "success" | "error";?

  4. 4.A function returns string | undefined. What must happen before calling .toUpperCase() on that result?

4 left to answer