Manuals / TypeScript / Ch 4

B · ModelingIntermediate50 min read

4. Unions, narrowing & discriminated unions

TypeScript · 44 pages source format

Union types model "A or B". Narrowing refines unions with typeof, instanceof, in, and truthiness checks. Discriminated unions add a shared literal field for exhaustive switch — the pattern for API states and UI machines.

What you'll learn

  • Union types
  • Type narrowing
  • Discriminated unions
  • Exhaustiveness checking
  • never type

Basic unions

type Id = string | number. Functions accepting unions must handle all cases or narrow first.

Do this now

Write formatId(id: string | number): string handling both.

Clear?

Narrowing techniques

typeof for primitives. instanceof for classes. "field" in obj for object shapes. Truthiness for null/undefined.

function printValue(val: string | number | boolean) {
  if (typeof val === "string") console.log(val.toUpperCase())
  else if (typeof val === "number") console.log(val.toFixed(2))
  else console.log(val ? "yes" : "no")
}

Do this now

Write printValue(val: string | number | boolean) using typeof narrowing.

Clear?

Discriminated unions

Shared literal field (kind/status) enables exhaustive switch. Compiler warns on missing cases with never.

type FetchState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string }

Do this now

Model fetch state: idle | loading | success | error with discriminated union. Render function with switch.

Clear?

Exhaustiveness check

default: const _exhaustive: never = state catches unhandled cases at compile time.

Do this now

Add a new status to union. See compiler error until switch updated.

Clear?

Result type pattern

type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }. Safer than throw for expected failures.

Do this now

Wrap getPost in Result<Post> instead of throwing. Caller narrows on ok.

Clear?

Checklist