Discriminated Unions

A discriminated union is a union of object types that all share one property holding a unique literal value — the discriminant — so that checking that single field lets TypeScript narrow the value to exactly one member and safely access the fields that member carries.

Learn Discriminated Unions in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of mail arriving at a sorting office. Every envelope has a class stamp in the corner — "Letter", "Parcel", "Registered". That stamp is the discriminant. The clerk reads the stamp first, and only then knows what to expect inside: a parcel has a weight, a registered item has a tracking number, a plain letter has neither. You'd never weigh a letter or look for a tracking number on a postcard. A discriminated union works the same way — read the tag, and the compiler reveals exactly which fields are valid for that kind of "envelope".

1. The Discriminant & Narrowing with switch

Give every member of the union the same field — here kind — set to a distinct literal. When you switch on that field, TypeScript narrows the value inside each case to the single member that matches, so only that member's fields are accessible:

2. Exhaustiveness Checks with never

A discriminated union shines when you make the compiler prove you handled every case. In the default branch, assign the value to never (or pass it to an assertNever helper). If a case is missing, the value isn't never and the build fails — a forgotten variant becomes a compile error:

3. Modelling State & Results

The pattern's real payoff is modelling state machines so that impossible states are unrepresentable . A request can be idle , loading , success (with data), or error (with a message) — and a reducer switches on the discriminant to move between them. You can't accidentally read data while loading :

🎯 Your Turn

Complete a function that narrows a Payment union by its method discriminant. Fill in the blanks and match the expected output.

Build an unwrapResult for a boolean-discriminated Result union — return the value on success, the fallback on failure. Follow the outline, run it, and match the example output.

Practice quiz

What makes a union 'discriminated'?

  • It has more than three members
  • All members are objects
  • Every member shares a property set to a unique literal type (the discriminant)
  • It uses the never type

Answer: Every member shares a property set to a unique literal type (the discriminant). A shared property with a unique literal value per member (kind/type/status) is the discriminant.

Common names for the discriminant field include:

  • kind, type, status
  • id, key, ref
  • name, label, tag only
  • value, data, payload

Answer: kind, type, status. Conventional discriminant field names are kind, type, or status.

When you switch on the discriminant, what does TypeScript do inside each case?

  • Nothing — narrowing is runtime-only
  • Widens it to the full union
  • Casts it to any
  • Narrows the value to the single member whose discriminant matches

Answer: Narrows the value to the single member whose discriminant matches. Control-flow analysis narrows the value to exactly the matching member in each case.

Inside 'case "circle":' for a shape union, which field is accessible?

  • width
  • radius
  • size
  • all of them

Answer: radius. After narrowing to the circle member, only its own fields (like radius) are available.

What is the purpose of the never-based exhaustiveness check?

  • To make the compiler error if a new variant is added but a case is forgotten
  • To speed up the switch
  • To throw at runtime always
  • To widen the union

Answer: To make the compiler error if a new variant is added but a case is forgotten. Assigning the value to never (or assertNever) turns a forgotten case into a compile error.

What signature does the exhaustiveness helper typically have?

  • function assertNever(x: any): void
  • function assertNever(x: unknown): boolean
  • function assertNever(x: never): never
  • function assertNever(): void

Answer: function assertNever(x: never): never. assertNever(x: never): never errors at compile time if x is not actually never.

Why must the discriminant be a literal type, not a wide 'string'?

  • Strings are slower
  • Widening to string stops narrowing from working
  • Literals use less memory
  • string is not allowed in unions

Answer: Widening to string stops narrowing from working. If the discriminant is widened to string, TypeScript can no longer narrow per case.

Which is a textbook use of a discriminated union?

  • A single number
  • A plain string array
  • A boolean flag alone
  • Request state: idle | loading | success | error

Answer: Request state: idle | loading | success | error. Modelling request state (idle/loading/success/error) is a classic discriminated-union use.

What is the idiomatic Result type discriminated by a boolean?

  • { value } | { error }
  • { ok: true; value } | { ok: false; error }
  • { status: number }
  • { data?: T }

Answer: { ok: true; value } | { ok: false; error }. A boolean 'ok' field discriminates the success ({ ok: true; value }) and failure ({ ok: false; error }) cases.

What goes wrong if two members share the same discriminant value?

  • Nothing
  • The compiler renames one
  • They collapse together and narrowing breaks
  • It becomes a tuple

Answer: They collapse together and narrowing breaks. Each member needs a UNIQUE discriminant value; duplicates break narrowing.