🌱BeginnerAdvanced Beginner Types

Union and Intersection Types

Union (|) and intersection (&) types, discriminated unions, and type narrowing in practice.

Overview

TypeScript's most expressive feature is its ability to compose types algebraically. **Union types (`|`)**: a value that can be one of several types. `string | number` accepts both strings and numbers. TypeScript requires you to handle all possibilities before calling type-specific methods. **Intersection types (`&`)**: a value that must satisfy all combined types at once. `User & Admin` needs all properties from both. Useful for mixins and extending shapes. **Discriminated unions** are the killer pattern: a union where each member has a common literal "tag" field (like `kind`, `type`, or `status`). TypeScript narrows the type in switches/if-blocks by checking the tag, giving you full type safety inside each branch.

Key Points

  • •Union: `A | B` value is either A or B; must handle both before using type-specific ops
  • •Intersection: `A & B` value satisfies both A and B; has all properties of each
  • •Discriminated union: each member has a unique literal "tag" field; TypeScript narrows in switch
  • •TypeScript narrows union types with: `typeof`, `instanceof`, truthiness, equality, `in` operator
  • •`string | number` can only call methods that exist on both types (e.g. `toString()`)
  • •After narrowing with `typeof value === "string"`, TypeScript treats `value` as `string` inside the branch
  • •Intersection conflicts: `{ a: string } & { a: number }` gives `a: never` avoid conflicting members

Examples

Union types and narrowing

type ID = string | number;

function printId(id: ID): void {
  // Can't call .toUpperCase() here what if id is a number?
  if (typeof id === "string") {
    console.log(id.toUpperCase());   // TypeScript knows: string
  } else {
    console.log(id.toFixed(0));      // TypeScript knows: number
  }
}

printId("abc-123");   // ABC-123
printId(42);          // 42

// Union with null
function getLength(text: string | null): number {
  if (text === null) return 0;
  return text.length;   // narrowed to string
}

console.log(getLength("hello"));  // 5
console.log(getLength(null));     // 0

Discriminated union the pattern

type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string[] };
type ErrorState   = { status: "error";   message: string };

type State = LoadingState | SuccessState | ErrorState;

function render(state: State): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return `Got ${state.data.length} items`; // state.data is available
    case "error":
      return `Error: ${state.message}`;        // state.message is available
  }
}

console.log(render({ status: "loading" }));
console.log(render({ status: "success", data: ["a","b","c"] }));
console.log(render({ status: "error",   message: "Not found" }));

Intersection types

type Serializable = {
  serialize(): string;
};

type Printable = {
  print(): void;
};

type Document = {
  content: string;
};

type PrintableDocument = Document & Printable & Serializable;

const doc: PrintableDocument = {
  content: "Hello TypeScript",
  print() { console.log(this.content); },
  serialize() { return JSON.stringify({ content: this.content }); },
};

doc.print();
console.log(doc.serialize());

Common Mistakes

  • Trying to use type-specific methods on a union without narrowing `(string | number).toUpperCase()` is an error; narrow first
  • Intersection with conflicting same-named properties `{a: string} & {a: number}` makes `a: never` (impossible to satisfy)
  • Forgetting a discriminant case in a switch use the `never` exhaustiveness check pattern

Note

Discriminated unions are one of TypeScript's most powerful patterns. Model your application state as a discriminated union (loading | success | error) and let TypeScript prove you've handled every case.

Exercises

Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

Why does TypeScript require narrowing before calling string methods on `string | number`?