Discriminated Unions
debt(d7/e5/b3/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate automated detection is 'no', and the tool listed is TypeScript itself — but TypeScript only catches missing cases if you implement an exhaustiveness check (never guard). Without that pattern, new variants silently fall through switch/if-else chains. The common mistake of forgetting the never check means the gap is invisible until code review or a runtime bug surfaces.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes replacing structs with optional fields across mutually exclusive states with properly discriminated union types each carrying a literal 'kind' or 'status' field. This typically requires updating multiple call sites, type guards, switch statements, and downstream consumers — not a single-line patch but a moderate-to-significant refactor within a component or feature area.
Closest to 'localised tax' (b3). Discriminated unions apply to web and cli contexts but are scoped to the specific domain model or state machine they represent. Once established, they impose a mild structural tax on maintainers (every new variant must be added to the union and handled in all switch statements), but this is localised to the relevant module and doesn't shape the broader codebase.
Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception is treating discriminated unions as ordinary union types and missing the power of the discriminant field — specifically, forgetting the never exhaustiveness check so new variants silently fall through. Using boolean discriminants instead of string literals is another common pitfall. These are documented gotchas that developers typically discover after being bitten once, placing this squarely at t5.
Also Known As
TL;DR
Explanation
Each member of the union has a shared property with a unique literal type (kind, type, status, tag). TypeScript uses the discriminant to narrow inside switch/if blocks with no runtime overhead beyond the field check. Pattern replaces: class hierarchies, boolean flags, optional fields that are only set in certain states. Exhaustiveness: add a default: const x: never = value branch to get a compile error when a new union member is added but not handled. Common in Redux actions, API response variants, result types (Ok | Err), FSM states.
Diagram
stateDiagram-v2
[*] --> loading
loading --> success : data fetched
loading --> error : request failed
success --> [*]
error --> loading : retry
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Using a boolean discriminant — booleans only give two variants; use a string literal for extensibility.
- Forgetting the never exhaustiveness check — new variants added later silently fall through.
- Putting shared fields on the union level instead of each member — breaks narrowing.
- Using type assertions instead of discriminant checks — bypasses the safety.
Avoid When
- When variants share most fields and only differ in one optional aspect — a simple optional field is cleaner.
- When the number of variants changes very frequently and exhaustiveness checking becomes a maintenance burden.
When To Use
- Modelling FSM states, async request lifecycle (loading/success/error), or Redux action types.
- Any time you have mutually exclusive variants that carry different data.
Code Examples
// Impossible state possible: what if both error and data are set?
type ApiResponse = {
loading: boolean;
data?: User;
error?: string;
};
if (response.data) {
// error might also be set — ambiguous
// Discriminated union — impossible states are impossible
type ApiResponse =
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; message: string };
function render(r: ApiResponse) {
switch (r.status) {
case 'loading': return <Spinner />;
case 'success': return <UserCard user={r.data} />;
case 'error': return <ErrorMsg text={r.message} />;
default:
const _: never = r; // compile error if a variant is unhandled
}
}