Frontend
Narrowing unions without a wall of type guards
Discriminated unions do most of the narrowing work for you, if the union is shaped to let them.
Last updated September 17, 2026
A common pattern in TypeScript code that hasn't fully embraced unions yet: a type that's a plain union of shapes, handled with a chain of typeof and in checks to figure out which shape you actually have at runtime.
type Event = { type: string; payload: unknown };
function handle(e: Event) {
if (e.type === "click") {
// payload is still `unknown` here — TypeScript can't narrow it
}
}
The fix isn't more guards, it's giving the union a proper discriminant that TypeScript can actually key off of.
Shaping the union to narrow itself
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; deltaY: number };
function handle(e: Event) {
switch (e.type) {
case "click":
console.log(e.x, e.y); // narrowed to the click variant, fully typed
break;
case "keypress":
console.log(e.key); // narrowed to the keypress variant
break;
}
}
With a literal type field as the discriminant, a single switch (or if (e.type === "click")) narrows e to the exact variant inside that branch — no manual casting, no in checks, no typeof payload === "object" guards standing in for what the type system should be doing.
Exhaustiveness as a bonus
The other benefit that comes for free: a default case that assigns to a never-typed variable catches unhandled variants at compile time.
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
switch (e.type) {
case "click": /* ... */ break;
case "keypress": /* ... */ break;
case "scroll": /* ... */ break;
default: assertNever(e); // compile error if a new variant is added and not handled
}
Add a fourth event variant later, forget to handle it in this switch, and the build fails at the assertNever call instead of silently falling through at runtime. That's the actual payoff of a discriminated union over a plain one — not less typing today, but a compiler that catches the thing you forgot, instead of a runtime bug that shows up later.
Tags
Related posts