Q19 of 38 · TypeScript
What is a discriminated union in TypeScript, and why is it useful for modelling API responses?
Short answer
Short answer: A discriminated union is a union type where every member has a shared literal property (`kind`, `type`, `status`) that uniquely identifies it. TypeScript uses that discriminant to narrow the union in switch/if blocks, giving you type-safe access to variant-specific properties without type assertions.
Detail
A discriminated union (also called a tagged union or algebraic data type) is a pattern where each member of a union type has a discriminant property — a literal type value that uniquely identifies the variant.
Structure:
type Shape = { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number };
The kind field is the discriminant. TypeScript knows that if kind === "circle", radius is available.
Why it's powerful:
- TypeScript narrows automatically in switch/if on the discriminant
- No type assertions needed
- Adding a new variant causes compile errors at all unhandled switch cases (with
neverexhaustive check) - Reads like documentation — the variant's type makes intent clear
API response modelling: The { status: "ok"; data: T } | { status: "error"; message: string } pattern is the canonical use in test automation. It forces handlers to deal with both outcomes explicitly, unlike a nullable data? that can be silently ignored.
In Playwright fixtures: Custom fixture types that vary by authentication state, user role, or environment can be modelled as discriminated unions to provide type-safe access to the correct fixture properties.
// EXAMPLE
// Discriminated union for API results
type ApiResult<T> =
| { status: "ok"; data: T }
| { status: "error"; code: number; message: string }
| { status: "loading" };
function render<T>(result: ApiResult<T>): string {
switch (result.status) {
case "ok":
return JSON.stringify(result.data); // data: T, type-safe
case "error":
return `Error ${result.code}: ${result.message}`; // code and message available
case "loading":
return "Loading...";
default:
const _: never = result; // exhaustive check
throw new Error("Unexpected status");
}
}
// Shape example
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number };
function area(s: Shape): number {
if (s.kind === "circle") return Math.PI * s.radius ** 2;
return s.width * s.height; // TypeScript knows it's rect
}