What are discriminated unions in TypeScript?

A discriminated union is a union of types that each share a literal "discriminant" field - TypeScript uses that field to narrow the type automatically.

TypeScript

Overview

Discriminated unions (also called tagged unions) are a powerful TypeScript pattern for modelling data that can be one of several shapes. Each variant has a `kind` (or `type`, `tag`) field with a unique literal value. TypeScript uses this discriminant to narrow the type in `if`/`switch` blocks.

Key Points

  • •Every variant in the union must have a common field with a unique literal type.
  • •TypeScript narrows inside `if (x.kind === "...")` or `switch (x.kind)` blocks.
  • •Exhaustiveness: if a new variant is added, TypeScript will error on unhandled `switch` cases when a `never` guard is present.
  • •Great for: async states (loading/success/error), action types in reducers, API responses.
  • •Safer than optional fields: instead of `{ loading?: boolean; data?: T; error?: Error }`, use explicit variant types.

Examples

Async state - the most common use case

typescript
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error';   error: Error };

function render(state: AsyncState<User>) {
  switch (state.status) {
    case 'idle':    return <p>Click to load</p>;
    case 'loading': return <Spinner />;
    case 'success': return <UserCard user={state.data} />;  // data is typed as User
    case 'error':   return <p>{state.error.message}</p>;   // error is typed as Error
  }
}

Event/action types in a reducer

typescript
type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET'; payload: number }
  | { type: 'RESET' };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'INCREMENT': return state + 1;
    case 'DECREMENT': return state - 1;
    case 'SET':       return action.payload; // payload only available here
    case 'RESET':     return 0;
  }
}

Replace optional fields with a union (safer)

typescript
// Messy: many optional fields, unclear which combinations are valid
type UserBad = {
  name: string;
  type: 'admin' | 'regular';
  adminLevel?: number;    // only for admin?
  memberSince?: Date;     // only for regular?
};

// Clean: each variant is explicit and self-contained
type User =
  | { type: 'admin';   name: string; adminLevel: number }
  | { type: 'regular'; name: string; memberSince: Date  };

Pro Tip

Design your discriminated unions so that accessing a field that only exists on one variant is impossible without first narrowing. If TypeScript can access `state.data` without a check, the union is not properly discriminated. The switch-on-`status` pattern makes this impossible by design.