TypeScript Type Narrowing

A developer reference for narrowing union types using typeof, instanceof, in, and discriminated unions.

TL;DR

  1. Narrow primitive unions with JavaScript typeof conditional checks.
  2. Distinguish class instances safely using the instanceof operator.
  3. Build custom predicate functions using the value is Type syntax.

Typeof and Truthiness

    typeof primitive guard

    Narrows unions containing primitive strings, numbers, or booleans.

    function pad(val: string | number) {
      if (typeof val === 'string') {
        return val.padStart(4, '0');
      }
      return val.toFixed(2);
    }
    truthiness check

    Filters out null, undefined, false, and empty strings.

    function printAll(strs: string[] | null) {
      if (strs && strs.length > 0) {
        for (const s of strs) console.log(s);
      }
    }
    equality narrowing

    Uses strict equality checks to eliminate candidate types.

    function compare(
      x: string | number, y: string | boolean
    ) {
      if (x === y) {
        console.log(x.toUpperCase()); // x is string
      }
    }

Instanceof and In Operator

    instanceof check

    Verifies prototype inheritance against known class constructors.

    function handleError(err: unknown) {
      if (err instanceof Error) {
        console.error(err.stack);
      }
    }
    in operator guard

    Tests whether an object possesses a specific property key.

    type Fish = { swim: () => void };
    type Bird = { fly: () => void };
    function move(animal: Fish | Bird) {
      if ('swim' in animal) return animal.swim();
      animal.fly();
    }
    Array.isArray guard

    Distinguishes array collections from single element items.

    function ensureList(
      input: string | string[]
    ): string[] {
      if (Array.isArray(input)) return input;
      return [input];
    }

Discriminated Unions

    discriminant property

    Shares a common literal tag to distinguish object variants.

    type Circle = { kind: 'circle'; radius: number };
    type Square = { kind: 'square'; size: number };
    type Shape = Circle | Square;
    switch narrowing

    Evaluates the shared discriminant across case branches.

    function area(s: Shape): number {
      switch (s.kind) {
        case 'circle': return Math.PI * s.radius ** 2;
        case 'square': return s.size * s.size;
      }
    }
    exhaustive check

    Uses never to catch unhandled union members at compile time.

    function assertNever(x: never): never {
      throw new Error(`Unhandled variant: ${x}`);
    }

Custom Type Predicates

    is type predicate

    Returns a boolean that narrows the parameter in calling scopes.

    interface Cat { meow: () => void; }
    function isCat(pet: unknown): pet is Cat {
      return (pet as Cat)?.meow !== undefined;
    }
    filter predicate

    Removes null and undefined from array sequences cleanly.

    function isNonNullable<T>(
      val: T
    ): val is NonNullable<T> {
      return val !== null && val !== undefined;
    }
    const clean = [1, null, 2].filter(isNonNullable);
    assertion function

    Guarantees a type or throws an explicit runtime exception.

    function assertIsString(
      val: unknown
    ): asserts val is string {
      if (typeof val !== 'string') {
        throw new TypeError('Expected string');
      }
    }

Tips

  1. Leverage discriminated unions with literal kind tags to achieve exhaustive compile-time checking in switch statements.
  2. Use user-defined type guard predicates when filtering nullable items out of arrays with the filter() method.

Warnings

  1. Remember that typeof null evaluates to object, so always check for null before inspecting object properties.
  2. Custom assertion functions using asserts condition throw at runtime, so never pass unvalidated external data directly.

In Practice

FAQ