TypeScript Type Narrowing

Master typeof, instanceof, in, and discriminated unions to safely work with union types.

TL;DR

  1. 01Narrow union types using typeof, instanceof, and in checks.
  2. 02Use discriminated unions for safer object type narrowing.
  3. 03Build custom type guards with the is keyword.

Tips

  1. 01Reach for discriminated unions when modeling state, since they pair perfectly with switch statements and exhaustive narrowing.

Warnings

  1. 01A failing assertion function throws at runtime, so always wrap calls in try/catch when the input is untrusted.

Typeof Checks

  • Use typeof to narrow primitive types like string, number, and boolean.
    function format(value: string | number) {
      if (typeof value === "string") {
        return value.trim();
      }
      return value.toFixed(2);
    }
  • TypeScript automatically narrows the type inside each branch.
  • Works for string, number, boolean, bigint, symbol, undefined, and function.
  • This is the simplest form of narrowing and works with most union types.
  • Use it whenever a union mixes primitive types together.

Instanceof and Truthiness

  • Use instanceof to narrow object types created from classes.
    if (error instanceof Error) {
      console.log(error.message);
    }
  • Check for null or undefined to narrow nullable union types.
    function greet(name: string | null) {
      if (name) return `Hi ${name}`;
      return "Hi friend";
    }
  • Truthiness checks narrow out falsy values like null, undefined, and empty string.
  • Use optional chaining ?. alongside narrowing for safer property access.
  • Combine with nullish coalescing ?? to provide default values.

The In Operator

  • Use in to check if a property exists on an object.
    type Dog = { bark(): void };
    type Cat = { meow(): void };
    function speak(pet: Dog | Cat) {
      if ("bark" in pet) pet.bark();
      else pet.meow();
    }
  • TypeScript narrows the type based on which property is present.
  • Useful when union members have different shapes without a common tag.
  • Works for both own properties and inherited ones on the object.
  • Pair with discriminated unions for even cleaner narrowing.

Discriminated Unions

  • Add a shared literal property to union members for safe narrowing.
    type Shape =
      | { kind: "circle"; radius: number }
      | { kind: "square"; side: number };
    
    function area(shape: Shape) {
      if (shape.kind === "circle") {
        return Math.PI * shape.radius ** 2;
      }
      return shape.side ** 2;
    }
  • The shared kind property acts as a discriminator for narrowing.
  • TypeScript automatically narrows to the matching member in each branch.
  • Pair with a switch statement for clean handling of every case.
  • This is the most reliable pattern for narrowing complex unions.

Custom Type Guards

  • Write a function that returns a type predicate using the is keyword.
    function isString(value: unknown): value is string {
      return typeof value === "string";
    }
  • The compiler uses the predicate to narrow types in calling code.
    if (isString(value)) {
      console.log(value.toUpperCase());
    }
  • Use type guards for complex checks that built-in operators cannot express.
  • Use asserts value is Type for guards that throw on failure.
    function assertString(v: unknown): asserts v is string {
      if (typeof v !== "string") throw new Error("Not a string");
    }
  • Custom guards keep narrowing logic reusable across your codebase.

FAQ