Require Exhaustive Union Handling in TypeScript
Flags switches and if chains over TypeScript unions that lack a never check, so a newly added variant compiles and is silently ignored or mishandled.
What it flags
import { resizeImage, sendEmail } from './tasks';
type Job =
| { kind: 'send-email'; to: string; template: string }
| { kind: 'resize-image'; key: string; width: number }
| { kind: 'purge-cache'; paths: string[] };
export async function runJob(job: Job): Promise<void> {
switch (job.kind) {
case 'send-email':
await sendEmail(job.to, job.template);
break;
case 'resize-image':
await resizeImage(job.key, job.width);
break;
// 'purge-cache' compiles, runs nothing, and the job is marked done
}
}import { purgeCache, resizeImage, sendEmail } from './tasks';
type Job =
| { kind: 'send-email'; to: string; template: string }
| { kind: 'resize-image'; key: string; width: number }
| { kind: 'purge-cache'; paths: string[] };
export async function runJob(job: Job): Promise<void> {
switch (job.kind) {
case 'send-email':
await sendEmail(job.to, job.template);
return;
case 'resize-image':
await resizeImage(job.key, job.width);
return;
case 'purge-cache':
await purgeCache(job.paths);
return;
default: {
const unhandled: never = job;
throw new Error(`Unhandled job: ${JSON.stringify(unhandled)}`);
}
}
}Why it matters
Adding a variant to a union is a routine change, and the compiler only points to the code that must handle it when that code is written to fail on unknown cases. A switch without a never check, or a default branch that returns a fallback, lets the new variant compile and take the wrong path, such as a job type that is marked done without running. Reviewers see the diff that adds the variant, while the switches that now miss it sit unchanged in other files.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Require Exhaustive Union Handling in TypeScript
Applies to `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts`.
Review TypeScript code that branches on union types for cases the compiler will not report when a new variant is added.
Flag changes that:
- Switch on a union or discriminant such as `kind`, `type`, or `status` in a function that returns `void` or an inferred type, without a `default` branch that assigns the value to `never` or passes it to an `assertNever` helper.
- Add a `default` branch or final `else` over a closed union that returns a fallback, logs, or does nothing, so future variants are handled silently instead of failing to compile.
- Check one or two variants with `if` and treat everything else as the remaining variant, such as `if (status === 'active') { ... } else { ... }` for a union with more than two members.
- Add a variant to an existing union without updating the switches, lookup maps, and filters that branch on it.
- Use `Partial<Record<Union, T>>` or `{ [key: string]: T }` for a per-variant lookup table where `Record<Union, T>` would make a missing entry a compile error.
Do not flag:
- Switches in functions with an explicit non-void return type where every case returns and there is no `default`; with `strictNullChecks`, the compiler already reports a missing case.
- Defaults that deliberately handle values from outside the program, such as webhook event types a provider may add later, as long as they log or report the unknown value.
- Branches where a comment states that every other variant is intentionally handled the same way.
Checks files matching **/*.ts, **/*.tsx, **/*.mts, **/*.cts.
More TypeScript agents
See all TypeScript code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.