Union & Intersection Types

Combining types with `|` to mean "one of these" and with `&` to mean "all of these, merged."

What is it?

So far, every type you've seen describes one specific shape. But plenty of real values are more flexible than that. A function that accepts either an id (a number) or a slug (a string) to look up a record can't be described by a single basic type — it genuinely accepts either one.

A union type, written A | B, describes exactly that: a value that is one of several possible types. string | number means "a string, or a number — could be either." TypeScript then requires you to check which one you actually got before doing something that only makes sense for one of them (this checking is covered in the next topic, narrowing).

Going the other direction, sometimes you want a value that satisfies multiple shapes at once — it has to combine all of their properties together, not just be one or the other. That's an intersection type, written A & B. If A has a name property and B has an age property, then A & B requires both — a single object with both name and age.

Explain like I'm 10

A union is like a form field that accepts "either a phone number or an email" — one or the other, your choice. An intersection is like a job application that requires both a resume AND a cover letter — you need every piece, combined into one submission, not just one of them.

Examples

A union type for flexible input

function lookup(idOrSlug: number | string): void {
  console.log(`Looking up: ${idOrSlug}`);
}

lookup(42);        // OK — it's a number
lookup("my-post"); // OK — it's a string
lookup(true);
// Error: Argument of type 'boolean' is not assignable
// to parameter of type 'string | number'.

idOrSlug may be a number or a string — nothing else. TypeScript accepts either, but rejects any value that's neither.

An intersection type combining two shapes

interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

type Person = HasName & HasAge;

const p: Person = { name: "Tariq", age: 41 }; // must have both

const invalid: Person = { name: "Tariq" };
// Error: Property 'age' is missing in type
// '{ name: string; }' but required in type 'HasAge'.

Person is the intersection of HasName and HasAge, so a valid Person must satisfy both interfaces at once — it needs every property from each.

How it works

For a union type A | B, the compiler only lets you do things with the value that are safe for both A and B — anything specific to just one of them requires narrowing the type first (checking which one you actually have at runtime).

For an intersection type A & B, the compiler merges the member lists of both types and requires an object to satisfy every member from both — effectively unioning the requirements, even though the type itself is called an intersection.

Why does it exist?

Real-world data often genuinely varies in shape — an API response might be a success object or an error object, a function might accept a couple of different input forms. Union types let you describe that variability precisely, instead of falling back to any and losing all checking. Intersection types let you build up a bigger shape by combining smaller, reusable pieces instead of repeating properties across several similar interfaces.

When to use it

Use a union whenever a value can genuinely be more than one type — the result of an operation that can succeed or fail, a parameter accepting a couple of related input shapes, or a value read from an external, less predictable source. Use an intersection when you want to combine several smaller, independently useful shapes into one composite type, especially if those shapes are reused elsewhere on their own.

When not to use it

Avoid piling more than a handful of types into a single union — beyond that, the code handling every case tends to get unreadable, and it may be a sign the values should share a common structure instead (see discriminated unions). Avoid intersections that combine incompatible types (like string & number, which produces the unusable never type) — intersections only make sense between compatible object shapes.

Common mistakes

  • Trying to access a property on a union type that only exists on one branch of it, before narrowing which branch you actually have.

  • Confusing | and & — a union (|) means "could be either," while an intersection (&) means "must be both, combined."

  • Intersecting two types that have the same property name with conflicting types (like string in one and number in the other), which silently collapses that property to never.

Practice exercises

  1. Easy:

    Write a type alias Id as string | number, and a function printId(id: Id): void that logs it.

  2. Medium:

    Define two interfaces, Timestamped (createdAt: Date) and Named (name: string), and create an intersection type NamedEvent combining both. Create a valid object of that type.

  3. Hard:

    Write a function that accepts string | string[] and always returns a string[] — wrapping a single string in an array, or returning the array as-is.

Interview questions

What can you safely do with a value typed `string | number` before narrowing it?

Only operations that are valid for both branches — TypeScript refuses anything specific to just one type until you check, at runtime, which one you actually have.

What does `A & B` require of a value, in terms of properties?

The value must satisfy every member of both A and B at once — an intersection combines requirements rather than offering a choice between them.

Why does intersecting two incompatible primitive types, like `string & number`, produce the `never` type?

No value can simultaneously be a string and a number, so the set of values satisfying both constraints is empty — TypeScript represents that impossible, empty set as never.

Given `function f(x: string | number) { x.toUpperCase(); }`, why does this fail to compile even though `x` might genuinely be a string at runtime?

TypeScript checks the type, not a specific call's actual value — since x could also be a number at that point, and toUpperCase doesn't exist on number, the call isn't safe for every possibility the union allows.

If `HasName` and `HasAge` both declare `id: string` with the same type, what happens when you intersect them?

The shared property merges cleanly into one id: string requirement — a conflict only arises when the same property name has incompatible types across the intersected types.

What happens to a property that two intersected types declare with conflicting, incompatible types (e.g. `string` in one, `number` in the other)?

That property's type collapses to never, since no value could satisfy both declared types at once — making the resulting type effectively impossible to construct.

In what sense does an intersection type `A & B` combine "requirements" rather than combine "possibilities"?

A union expands which values are acceptable (either type qualifies), while an intersection shrinks the set of acceptable values down to only those satisfying every required property from both types simultaneously.

How would you type a function that accepts either a single `string` or an array of strings?

As string | string[] — a union expressing that the parameter may genuinely be either shape, which then must be narrowed (e.g. with Array.isArray) before treating it as one or the other.

Why does TypeScript reject calling a method that exists on only one branch of a union, without an explicit runtime check first?

The compiler can't prove which branch you actually have at that point in the code, so it only allows operations that are valid across every branch until a check narrows the type.

What's the practical difference between typing a parameter as `string | number` versus typing it as `any`?

string | number still enforces that the value is one of exactly those two types and requires narrowing before type-specific use, while any disables checking entirely, silently allowing any value and any operation on it.

Is `{ name: "Tariq" }` assignable to `type Person = HasName & HasAge`?

No — the intersection requires every property from both HasName and HasAge, so an object missing age fails the structural check even though it fully satisfies HasName on its own.

Why are intersection types useful for building up a type from smaller, reusable pieces?

You can define small, independently useful interfaces once and combine them with & wherever needed, instead of repeating the same properties across several similar, hand-written interfaces.

Why should you avoid piling more than a handful of types into a single union?

Code that has to handle every possible branch tends to become unreadable past a few cases, and it's often a sign the values actually share a common structure better expressed as a discriminated union.

What's a cleaner alternative to a large union of many related object shapes?

Giving each shape a shared "tag" property with a distinct literal value (a discriminated union), so the branches are represented as one family of related types that narrow cleanly by checking that one property.

Can a union mix primitive and object types, like `string | { id: number }`? What's the implication for using it?

Yes — a union places no restriction on what kinds of types can appear in it, but every branch still needs its own narrowing check (e.g. typeof for the string case) before you can use anything specific to that branch.

Why does `lookup(42)` succeed and `lookup(true)` fail for a parameter typed `number | string`?

42 matches one of the union's listed types, but boolean isn't a member of number | string at all, so it fails to satisfy either branch.

If `A` and `B` each declare a method with the same name but different, incompatible parameter types, what tends to happen when you intersect them?

TypeScript combines the two signatures as an intersection of function types, which in practice usually only accepts arguments valid under both signatures at once — often collapsing to a signature too strict to call in any useful way.

Why is a union type's *value space* the combination of its branches' values, while an intersection type's *value space* is narrower than either type alone?

A union accepts a value matching any one branch, so its space of valid values grows with each branch added; an intersection demands a value satisfy every combined type's requirements at once, so its space of valid values only shrinks (or stays the same) as more types are combined.