Technology · TypeScript

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.

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.

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

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

TypeScript Type AliasesTypeScript Utility Types