Union & Intersection Types

Reviewed & published by Brayan K

A union type (A | B) lets a value be one of several types, while an intersection type (A & B) combines several types into one that has all their members at once.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of a payment method. A union is "pay with cash OR card OR voucher" — at checkout you'll use exactly one of them, so the code must check which one before acting. An intersection is a VIP membership card that is also a payment card — a single object that is both at once, carrying the abilities of both. Union = a choice between alternatives; intersection = a mash-up that has it all.

1. Union Types: One of Several

A union is written with the pipe | and means the value can be any one of the listed types — for example string | number accepts both "ab12" and 42. Because the value might be either, TypeScript only lets you use operations valid for every member until you narrow it. The most common narrowing tool is typeof, which you'll see below.

// A union widens what a value is ALLOWED to be:
type Id = string | number;

function formatId(id: Id): string {
  if (typeof id === "string") {
    return "ID-" + id.toUpperCase();   // id is 'string' here
  }
  return "ID-" + id.toFixed(0).padStart(5, "0"); // id is 'number' here
}

// Union of literal types = a fixed menu of allowed values:
type Status = "active" | "paused" | "closed";
let s: Status = "active";   // ✅
// let bad: Status = "open"; // ❌ '"open"' is not assignable to type 'Status'
// A UNION type (written A | B) means "this value is EITHER an A or a B".
// In TypeScript:  function format(id: string | number) { ... }
// At runtime it's just a normal value — the union lives in the types only.
// We check WHICH kind it is (this is called "narrowing") before using it.

function formatId(id) {
  // Is it a string? Then string methods are safe.
  if (typeof id === "string") {
    return "ID-" + id.toUpperCase();
  }
  // Otherwise it's a number here — number behaviour is safe.
  return "ID-" + id.toFixed(0).padStart(5, "0");
}

console.log(formatId("ab12"));  // ID-AB12
console.log(formatId(42));      // ID-00042

// A union of literal strings makes a tidy fixed set of options:
//   type Status = "active" | "paused" | "closed";
function describe(status) {
  return "The account is currently " + status + ".";
}
console.log(describe("active")); // The account is currently active.
console.log(describe("paused")); // The account is currently paused.

// In a .ts file the signatures would be:
//   function formatId(id: string | number): string { ... }
//   type Status = "active" | "paused" | "closed";

2. Intersection Types: All at Once

An intersection is written with the ampersand & and combines shapes: Person & Employee describes an object that has every property of Person and every property of Employee. It's the natural way to express "this thing is both a person and an employee", and at runtime you simply build one object carrying all the fields.

type Person   = { name: string; age: number };
type Employee = { employeeId: string; department: string };

// '&' MERGES the two shapes — Staff must have all four properties:
type Staff = Person & Employee;

const staff: Staff = {
  name: "Ada",
  age: 36,
  employeeId: "E-77",
  department: "Engineering",
};

// Missing even one property is an error:
// const bad: Staff = { name: "Ada", age: 36 };
// ❌ Property 'employeeId' is missing in type ... but required in type 'Staff'
// An INTERSECTION type (written A & B) means "this value has EVERYTHING
// from A AND EVERYTHING from B at the same time". It COMBINES shapes.
// In TypeScript:  type Staff = Person & Employee;
// At runtime that's simply one object holding all the properties.

const person = { name: "Ada", age: 36 };
const employee = { employeeId: "E-77", department: "Engineering" };

// Build a value that satisfies BOTH shapes by merging them:
const staff = { ...person, ...employee };

console.log(staff.name);        // Ada        (from Person)
console.log(staff.age);         // 36         (from Person)
console.log(staff.employeeId);  // E-77       (from Employee)
console.log(staff.department);  // Engineering(from Employee)

// A function that needs BOTH sets of fields can read any of them safely:
function badge(s) {
  return s.name + " (" + s.employeeId + ", " + s.department + ")";
}
console.log(badge(staff)); // Ada (E-77, Engineering)

// In a .ts file:
//   type Person   = { name: string; age: number };
//   type Employee = { employeeId: string; department: string };
//   type Staff    = Person & Employee;        // has ALL four fields

3. Discriminated Unions: The Best of Both

The most powerful pattern combines unions with a shared literal field. Give each member a common tag — often called kind or type — and a switch on that tag lets TypeScript narrow to the exact member in each branch. This discriminated union is how you model "one of several shapes" cleanly and safely.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius ** 2; // 'radius' is known here
    case "rect":   return shape.width * shape.height;  // 'width'/'height' known here
  }
}
// The MOST useful union pattern: a "discriminated union".
// Each member shares a common literal field (the "discriminant", here 'kind')
// that tells you exactly which member you have, so narrowing is clean.

// In TypeScript:
//   type Shape =
//     | { kind: "circle"; radius: number }
//     | { kind: "rect"; width: number; height: number };

function area(shape) {
  switch (shape.kind) {
    case "circle":
      // Here TypeScript KNOWS shape has 'radius'
      return Math.PI * shape.radius * shape.radius;
    case "rect":
      // Here it KNOWS shape has 'width' and 'height'
      return shape.width * shape.height;
    default:
      return 0;
  }
}

console.log(area({ kind: "circle", radius: 2 }).toFixed(2)); // 12.57
console.log(area({ kind: "rect", width: 3, height: 4 }));     // 12

// Checking the 'kind' field is what lets each branch use the right fields.

🎯 Your Turn

Here's a classic union job: a function that accepts either a single string or an array of strings and always returns an array. Fill in the two blanks marked ___, then run it.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// We're modelling a value that is EITHER a string OR a string[].

function toList(value) {
  // 1) If it is a single string, wrap it in an array:
  if (typeof value === ___) {   // 👉 the string "string"
    return [value];
  }
  // 2) Otherwise assume it's already an array and return it as-is:
  return ___;                   // 👉 just return value
}

console.log(toList("apple"));            // [ 'apple' ]
console.log(toList(["a", "b", "c"]));    // [ 'a', 'b', 'c' ]

// ✅ Expected output:
//    [ 'apple' ]
//    [ 'a', 'b', 'c' ]

Common Errors (and the fix)

Pro Tips

Mini-Challenge: Payment Result

No blanks this time — just a brief and a starting outline. Build the discriminated union handler yourself, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: a payment "result" discriminated union
// Model the OUTCOME of a payment with two shapes that share a 'status' field:
//   { status: "ok",   amount: number }
//   { status: "error", message: string }
//
// 1. Write describePayment(result) that switches on result.status.
// 2. For "ok"    return  "Paid £<amount>"
// 3. For "error" return  "Failed: <message>"
// 4. Call it with one of each and log the results.
//
// ✅ Expected output:
//    Paid £19.99
//    Failed: Card declined

// your code here

🎉 Lesson Complete

Practice quiz

A union type A | B means a value is:

  • Both an A and a B at once
  • Neither A nor B
  • Either an A or a B
  • An array of A and B

Answer: Either an A or a B. A union means the value is one of the listed types — either an A or a B.

An intersection type A & B describes a value that:

  • Has everything from A and everything from B
  • Is one of A or B
  • Has only the shared members
  • Cannot be constructed

Answer: Has everything from A and everything from B. An intersection combines shapes; the value must have every member of both A and B.

Before using a string-only method on a string | number value you must:

  • Cast it to any
  • Use an intersection
  • Nothing — it just works
  • Narrow it first

Answer: Narrow it first. TypeScript only allows operations valid for every member until you narrow, e.g. with typeof.

Which operator commonly narrows a string | number union?

  • keyof
  • typeof
  • infer
  • extends

Answer: typeof. Inside if (typeof id === 'string') TypeScript treats id as a plain string in that branch.

A discriminated union relies on:

  • A shared common literal field on each member
  • Every member being a primitive
  • Using & instead of |
  • Having no fields at all

Answer: A shared common literal field on each member. A shared literal tag (often 'kind') lets a switch narrow to the exact member in each branch.

type Status = 'active' | 'paused' | 'closed' is an example of a:

  • Intersection of objects
  • Tuple type
  • Union of string literals
  • Mapped type

Answer: Union of string literals. It is a union of literal strings — a tidy, enum-like fixed set of allowed values.

Omitting a required property from an intersection type gives:

  • A silent any
  • A 'Property is missing' error
  • An automatic union
  • A readonly value

Answer: A 'Property is missing' error. An intersection requires all properties from both shapes, so missing one is an error.

Intersecting { x: string } & { x: number } makes property x:

  • string
  • number
  • string | number
  • never

Answer: never. Incompatible requirements for the same property collapse to never — no value can satisfy both.

Which is the 'or' tool versus the 'and' tool?

  • | is and, & is or
  • | is or, & is and
  • Both are or
  • Both are and

Answer: | is or, & is and. A rule of thumb: 'or' problems are unions (|), 'and' problems are intersections (&).

For a value that is EITHER a string OR a string[], the right type is:

  • string & string[]
  • [string, string[]]
  • string | string[]
  • Record<string, string>

Answer: string | string[]. Either/or alternatives are modelled with a union: string | string[].

Continue this course

Frequently asked questions

When do I use a union (|) vs an intersection (&)?

Use a union (A | B) when a value can be one of several alternatives — it is EITHER an A OR a B. Use an intersection (A & B) when a value must satisfy several shapes at once — it has EVERYTHING from A AND from B. A rule of thumb: 'or' problems are unions, 'and' problems are intersections.

Why does TypeScript stop me using a method on a union value?

Because the method might not exist on every member of the union. If a value is string | number, you cannot call .toUpperCase() directly — it only exists on string. You must narrow first (e.g. with typeof) so TypeScript can prove which member you have in that branch.

What is a discriminated (tagged) union?

It is a union where every member shares one common literal field — the discriminant, often called kind, type, or status. Switching on that field lets TypeScript narrow to the exact member in each branch, so you can safely use that member's unique properties. It is the cleanest way to model 'one of several shapes'.

What happens if I intersect two types that conflict?

If two members of an intersection require the same property with incompatible types (e.g. { x: string } & { x: number }), that property's type becomes never — no value can satisfy both — so it effectively becomes impossible to construct. Intersections work best on shapes with non-overlapping properties.

Related lessons