TypeScript Union Types

Union types let a value be one of several specified types, giving you flexibility while keeping full type safety.

Declaring a Union Type

A union type is written with a pipe (|) between two or more types, meaning a value of that type must match at least one of the listed types.

Basic Union Types

let id: string | number;

id = "abc123"; // ok
id = 123;      // ok
// id = true;  // Error: 'boolean' is not assignable

function printId(id: string | number) {
  console.log(`Your ID is: ${id}`);
}

Narrowing Union Types

Before you can use type-specific operations on a union, TypeScript requires you to narrow it — using checks like typeof, Array.isArray, or property checks — so it knows which branch of the union applies.

Narrowing with typeof

function formatId(id: string | number): string {
  if (typeof id === "string") {
    return id.toUpperCase();
  }
  return id.toFixed(0);
}

console.log(formatId("abc")); // "ABC"
console.log(formatId(42));    // "42"
Note: Common narrowing tools include typeof for primitives, instanceof for class instances, Array.isArray for arrays, and the in operator or a shared discriminant property for object unions.

Discriminated Unions

When union members are objects, adding a shared literal property (often called a discriminant, tag, or kind) lets TypeScript narrow the exact member automatically inside conditionals and switch statements.

A Discriminated Union

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

type FetchState = LoadingState | SuccessState | ErrorState;

function render(state: FetchState): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return `Loaded ${state.data.length} items`;
    case "error":
      return `Error: ${state.message}`;
  }
}

Union Types vs Intersection Types

AspectUnion (A | B)Intersection (A & B)
MeaningValue is A OR BValue is A AND B combined
Property accessOnly shared properties, unless narrowedAll properties from both types
Common useModeling alternatives / variantsCombining/mixing multiple shapes
Note: Accessing a property that isn't shared by every member of a union causes a compile error until the value is narrowed — TypeScript only allows what's guaranteed to exist across all possibilities.

Realistic Use Case: Handling API Results Safely

Union types combined with discriminants are the standard way to model asynchronous results in TypeScript applications, replacing scattered null checks and boolean flags with a single exhaustively-checked type.

Modeling an API Result

type ApiResponse<T> =
  | { kind: "ok"; payload: T }
  | { kind: "notFound" }
  | { kind: "serverError"; retryAfterSeconds: number };

function handleResponse(response: ApiResponse<{ name: string }>): string {
  switch (response.kind) {
    case "ok":
      return `Hello, ${response.payload.name}`;
    case "notFound":
      return "Resource not found";
    case "serverError":
      return `Server error, retry in ${response.retryAfterSeconds}s`;
  }
}
  • Union types (A | B) describe a value that could be any one of several types.
  • Narrowing is required before accessing type-specific members or methods.
  • Discriminated unions add a shared literal field so TypeScript can narrow automatically.
  • The compiler checks that every case in a switch is handled when the union is exhaustive.

Exercise: TypeScript Union Types

What must a value of type `A | B` satisfy?