2 min read

TypeScript Type Guards and Narrowing

TypeScript's type system allows variables to have multiple possible types (Unions). Narrowing is the process of refining a broad type (like string | number) into a specific type (like string) within a specific block of code.

1. Built-in Type Guards

TypeScript automatically understands standard JavaScript checks.

typeof

Used for primitives (string, number, boolean, symbol).

function printId(id: number | string) {
  if (typeof id === "string") {
    // In this block, TypeScript knows 'id' is a string
    console.log(id.toUpperCase());
  } else {
    // Here, 'id' must be a number
    console.log(id.toFixed(2));
  }
}

instanceof

Used for classes and objects constructed with new.

class Dog {
  bark() { console.log("Woof!"); }
}

class Cat {
  meow() { console.log("Meow!"); }
}

function interact(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark(); // OK
  } else {
    pet.meow(); // OK
  }
}

in operator

Checks if a property exists on an object.

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim();
  } else {
    animal.fly();
  }
}

2. User-Defined Type Guards (Type Predicates)

Sometimes TypeScript can't figure out the type automatically. You can write a function that returns a type predicate.

The return type syntax is parameterName is Type.

interface Fish { swim: () => void; }
interface Bird { fly: () => void; }

// Custom Type Guard
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // TypeScript knows pet is Fish here
  } else {
    pet.fly();  // TypeScript knows pet is Bird here
  }
}

3. Discriminated Unions

This is a powerful pattern where every type in a union has a common literal property (the "discriminant"), usually called kind or type.

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      // TypeScript knows this is a Circle
      return Math.PI * shape.radius ** 2;
    case "square":
      // TypeScript knows this is a Square
      return shape.sideLength ** 2;
  }
}

programming/javascript/typescript/typescript