Skip to content
TypeScript ts narrowing 3 min read

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 "";
}
CheckNarrows out
x === nullonly null
x === undefinedonly undefined
x == null / x != nullboth null and undefined
x === literaleverything except that literal

x != null is the one place where loose equality is genuinely idiomatic in TypeScript, because it cleanly collapses the null | undefined pair. 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 switch on a discriminant field for unions with three or more variants.
  • Prefer strict ===/!== everywhere except the deliberate x != null idiom.
  • Use x != null to strip both null and undefined in a single, readable check.
  • Let literal comparisons subtract members from a union to drive exhaustive handling.
  • Pair switch narrowing with a never-based default to catch unhandled cases.
  • Remember that comparing two unions narrows both operands to their overlapping type.
Last updated June 29, 2026
Was this helpful?