Equality Narrowing
TypeScript narrows types not only with typeof and instanceof but also through plain equality checks. When you compare a value against a literal, null, undefined, or another variable, the compiler uses the result to refine both operands inside each branch. This powers discriminated-union switch statements, null guards, and the subtle difference between == and ===. This page covers literal comparisons, switch narrowing, loose-equality behavior, and narrowing two variables against each other.
Narrowing against literals
Comparing a value to a literal narrows it to that literal in the equal branch and removes it in the unequal branch.
type Direction = "up" | "down" | "left" | "right";
function step(dir: Direction): [number, number] {
if (dir === "up") {
// dir: "up"
return [0, -1];
}
// dir: "down" | "left" | "right"
return [0, 0];
}
After the if, the literal "up" is subtracted from the union, leaving the three remaining members. This is how switch on a discriminated union becomes exhaustive.
Switch on a discriminant
A switch statement applies equality narrowing per case, which is the cleanest way to handle a discriminated union. Each case narrows to one variant.
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; delta: number };
function handle(event: Event): string {
switch (event.type) {
case "click":
return `click at ${event.x},${event.y}`;
case "keypress":
return `key ${event.key}`;
case "scroll":
return `scroll ${event.delta}`;
}
}
Inside case "click", event is narrowed to the click variant, so event.x and event.y are available while event.key would be an error.
Removing null and undefined
Equality checks against null and undefined are a primary way to satisfy strictNullChecks. A !== null or != null check narrows the value to its non-nullish type.
function length(text: string | null | undefined): number {
if (text === null || text === undefined) {
return 0;
}
// text: string
return text.length;
}
Loose equality and the null/undefined pair
The loose == and != operators treat null and undefined as equal to each other and to nothing else. TypeScript models this exactly: x != null removes both null and undefined in one check.
function trim(text: string | null | undefined): string {
// x != null removes BOTH null and undefined
if (text != null) {
// text: string
return text.trim();
}
return "";
}
| Check | Narrows out |
|---|---|
x === null | only null |
x === undefined | only undefined |
x == null / x != null | both null and undefined |
x === literal | everything except that literal |
x != nullis the one place where loose equality is genuinely idiomatic in TypeScript, because it cleanly collapses thenull | undefinedpair. Elsewhere, prefer strict===/!==.
Narrowing two variables together
Equality narrowing also applies when you compare two variables: in the equal branch, both are narrowed to their common (intersection of possible) type.
function compare(a: string | number, b: string | boolean) {
if (a === b) {
// a: string, b: string — the only overlapping type
console.log(a.toUpperCase(), b.toUpperCase());
}
}
Since a === b can only be true when both hold a string (the sole shared member), TypeScript narrows both variables to string inside the branch.
Best Practices
- Use
switchon a discriminant field for unions with three or more variants. - Prefer strict
===/!==everywhere except the deliberatex != nullidiom. - Use
x != nullto strip bothnullandundefinedin a single, readable check. - Let literal comparisons subtract members from a union to drive exhaustive handling.
- Pair
switchnarrowing with anever-based default to catch unhandled cases. - Remember that comparing two unions narrows both operands to their overlapping type.