Concept · Building apps

Narrowing and discriminated unions

Let TypeScript prove which case you’re in from a check you already wrote, and catch the case you forgot.

3 mintype guardsdiscriminated uniontagged unionexhaustiveness check

A union type such as string | number or User | null says "one of these." Narrowing is how TypeScript works out which one from the checks in your code. Inside the if, the variable has the narrower type.

ts
function format(value: string | number | null) {
  if (value === null) return '—'          // value: null
  if (typeof value === 'string') return value.trim() // value: string
  return value.toFixed(2)                 // value: number — the only case left
}

TypeScript understands typeof, instanceof, in, equality checks, truthiness, Array.isArray, and early return/throw.

Discriminated unions

Give every variant a shared literal field, the discriminant, and a single check narrows the whole object:

tsx
type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: Order[] }
  | { status: 'error'; error: string }

function render(s: RequestState) {
  switch (s.status) {
    case 'idle': return null
    case 'loading': return <Spinner />
    case 'success': return <OrderList orders={s.data} /> // data exists only here
    case 'error': return <Alert>{s.error}</Alert>
  }
}

This makes impossible states impossible. With separate isLoading, data and error fields you can end up with isLoading: true and an error. With a union you can't.

Exhaustiveness checks

Make the compiler tell you when you add a variant and forget to handle it:

ts
default: {
  const unreachable: never = s   // error if any status isn't handled above
  throw new Error(`unhandled: ${JSON.stringify(unreachable)}`)
}

Custom type guards and their limit

ts
function isOrder(x: unknown): x is Order {
  return typeof x === 'object' && x !== null && 'id' in x && 'total' in x
}

A type guard is a promise you make to the compiler, and TypeScript doesn't check it. For data from outside your program, such as API responses, localStorage or model output, validate at runtime with a schema library like Zod, and let it produce the narrowed type.

Learn it in depth