Overview

TypeScript’s control-flow analysis understands certain patterns and narrows the type of a variable to a more specific subtype within a branch. This card catalogs every narrowing mechanism, when to use each, and the exhaustive-switch pattern for discriminated unions. The rationale for narrowing over casting lives in typescript-narrowing.

Primitive narrowing guards

Use these for standard JS value checks.

GuardExampleNarrows to
typeof x === "string"if (typeof x === "string")string
typeof x === "number"if (typeof x === "number")number
typeof x === "boolean"if (typeof x === "boolean")boolean
typeof x === "undefined"if (typeof x === "undefined")undefined
typeof x === "function"if (typeof x === "function")Function
x === nullif (x === null)null
x != nullif (x != null)Removes null and undefined from the type.
Truthinessif (x)Removes null, undefined, 0, "", false.
function printLength(value: string | number | null) {
  if (value === null) return;          // null removed
  if (typeof value === "string") {
    console.log(value.length);         // value: string
  } else {
    console.log(value.toFixed(2));     // value: number
  }
}

Object and class narrowing

GuardExampleNarrows to
instanceof Cif (err instanceof TypeError)TypeError (subtype of Error).
"prop" in objif ("name" in obj)Type that includes the name property.
Optional chainingobj?.propDoes not narrow; use in or type predicate instead.
Array checkArray.isArray(x)x: T[] within the branch.
function handle(err: Error | Response) {
  if (err instanceof Response) {
    console.log(err.status);  // err: Response
  } else {
    console.log(err.message); // err: Error
  }
}

Type predicates

Write a function that tells TypeScript the exact type when it returns true.

PatternSignature formWhen to use
Single-type predicatefunction isX(v: unknown): v is XNarrow from unknown or a union to one concrete type.
Falsy predicatefunction isDefined<T>(v: T | null | undefined): v is TReusable null/undefined guard.
Branded type guardPredicate that checks a _brand property.Distinguish opaque types that have the same structural shape.
interface Cat { meow(): void; }
interface Dog { bark(): void; }
 
function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}
 
function makeNoise(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow(); // animal: Cat
  } else {
    animal.bark(); // animal: Dog
  }
}

Discriminated unions

Add a shared literal field (kind, type, tag) so TypeScript can narrow on its value.

RequirementExampleEffect
Common discriminant fieldtype: "circle" | "square"Must be a literal type (string, number, boolean).
Exhaustive switchswitch (shape.type) with default: assertNever(shape)Compile error when a new variant is added but not handled.
Inline narrowingif (shape.type === "circle")Works without a switch; useful for early returns.
type Shape =
  | { type: "circle"; radius: number }
  | { type: "rect"; width: number; height: number }
  | { type: "triangle"; base: number; height: number };
 
function area(s: Shape): number {
  switch (s.type) {
    case "circle":   return Math.PI * s.radius ** 2;
    case "rect":     return s.width * s.height;
    case "triangle": return 0.5 * s.base * s.height;
    default:         return assertNever(s);
  }
}

Assertion functions and assertNever

PatternSignatureEffect
assertNeverfunction assertNever(x: never): never { throw new Error(...) }Causes a compile error if x is not never; exhaustiveness check.
Assertion functionfunction assert(cond: boolean, msg: string): asserts condTypeScript treats code after the call as having cond be true.
Type-narrowing assertionfunction assertIsString(v: unknown): asserts v is stringNarrows v to string for all code following the call.
function assertNever(x: never): never {
  throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
 
function assertDefined<T>(v: T | null | undefined): asserts v is T {
  if (v == null) throw new Error("Expected defined value");
}
 
// Usage
declare const user: { name: string } | null;
assertDefined(user);
console.log(user.name); // user: { name: string } — TS knows it's defined

Common gotchas

  • typeof null === "object" is a JavaScript legacy quirk. Always check x !== null before trusting an "object" typeof result.
  • Type predicates v is T are not verified by the compiler. A predicate that returns true for the wrong type silently lies to TypeScript. Keep predicates simple and obvious.
  • in narrowing requires the operand to be an object or union of objects. Using "prop" in x where x could be null or undefined throws at runtime. Guard with x != null first.
  • Discriminated unions only work when the discriminant is a literal type, not string or number. type: string provides no narrowing.
  • Assertion functions (asserts cond) are not checked by the compiler for correctness. They narrow downstream types unconditionally, so a buggy assertion can mask type errors.
  • The default: assertNever(s) pattern only catches unhandled variants at compile time. At runtime, the throw is your guard against future discriminant values arriving from external data.