David John Thammineni
TypeScript08 Dec 20222 min read

TypeScript Narrowing: Discriminated Unions Over Boolean Flags

TypeScript Narrowing: Discriminated Unions Over Boolean Flags

The most impactful TypeScript habit I teach isn't a fancy type — it's modeling state as a discriminated union so that impossible states don't compile.

The boolean-flags trap

interface RequestState {
  isLoading: boolean;
  error?: Error;
  data?: Report[];
}

This type permits { isLoading: true, error: ..., data: ... } — loading and errored and holding data. Every consumer needs defensive checks against combinations that should never exist.

The discriminated union

type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "error"; error: Error }
  | { status: "success"; data: Report[] };

status is the discriminant. Now narrowing is automatic:

function render(state: RequestState) {
  switch (state.status) {
    case "loading": return <Spinner />;
    case "error":   return <Alert error={state.error} />; // error exists here
    case "success": return <Table data={state.data} />;   // data exists here
    case "idle":    return null;
  }
}

Inside each branch, TypeScript knows exactly which fields exist. state.data in the error branch is a compile error, not a runtime undefined.

Exhaustiveness: the free regression test

default: {
  const _exhaustive: never = state;
  throw new Error(`Unhandled: ${_exhaustive}`);
}

Add a "retrying" variant next quarter and every switch missing it fails to compile. This is the pattern's superpower: state machine changes find their consumers.

Apply it at the API boundary

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: { code: string; message: string } };

const result = await fetchReport(id);
if (!result.ok) return showError(result.error); // typed error
processReport(result.data);                     // typed data

No thrown exceptions across layers, no data?.maybe. Callers must handle failure to reach the data — the type system enforcing the code review comment you're tired of writing.

Keep Reading