Back to roadmaps typescript Course

Type Guards and Type Narrowing

TypeScript uses static type analysis to inspect your code and predict runtime types. When a variable has a union type, we need to inspect it at runtime to determine its exact type. This process is called Type Narrowing, and the checks we write are called Type Guards.


1. What is Type Narrowing?

Type narrowing occurs when TypeScript analyze control flow statements (like if or switch) and understands that a variable must have a more specific type within a certain block of code.

function printLength(value: string | number) {
  // At this point, value is string | number
  
  if (typeof value === "string") {
    // TypeScript knows value is string here
    console.log(value.length);
  } else {
    // TypeScript knows value must be number here
    console.log(value.toFixed(2));
  }
}

2. Built-in Type Guards

TypeScript recognizes standard JavaScript operators as valid type guards.

A. The typeof Operator

Used to check primitive types (string, number, boolean, symbol, undefined, object, function).

if (typeof data === "string") {
  // data is string
}

B. The instanceof Operator

Used to check if an object is an instance of a specific class.

class FileDownloader {
  download() {}
}

function processTask(worker: FileDownloader | string) {
  if (worker instanceof FileDownloader) {
    worker.download(); // Safe to call
  }
}

C. The in Operator

Used to check if an object contains a specific property.

interface AdminUser {
  adminPrivileges: string[];
}

interface RegularUser {
  accountCreated: Date;
}

function configureAccess(user: AdminUser | RegularUser) {
  if ("adminPrivileges" in user) {
    console.log(user.adminPrivileges); // Safe
  }
}

3. Discriminated Unions

A Discriminated Union is a pattern where every type in a union has a common literal property. TypeScript uses this property to narrow down the union members.

interface NetworkSuccess {
  status: "success";
  payload: string;
}

interface NetworkFailure {
  status: "failed";
  errorMessage: string;
}

type ResponseResult = NetworkSuccess | NetworkFailure;

function handleResponse(res: ResponseResult) {
  switch (res.status) {
    case "success":
      console.log(res.payload); // Success shape
      break;
    case "failed":
      console.log(res.errorMessage); // Failure shape
      break;
  }
}

4. Custom Type Predicates (User-Defined Type Guards)

Sometimes, built-in operators are not enough. You can define a custom function that returns a type predicate: parameterName is Type.

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

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

// Custom type guard
function isBird(animal: Bird | Fish): animal is Bird {
  return (animal as Bird).fly !== undefined;
}

function moveAnimal(animal: Bird | Fish) {
  if (isBird(animal)) {
    animal.fly(); // TypeScript knows it is a Bird
  } else {
    animal.swim(); // TypeScript knows it must be a Fish
  }
}

5. Summary

  • Type narrowing changes a type from a broad union to a specific shape.
  • Use typeof for primitive types and instanceof for classes.
  • Use the in operator to check for the presence of unique properties.
  • Use Discriminated Unions with literal status fields for clean branching logic.
  • Implement user-defined type guards with the is keyword for complex validation.
Published on Last updated: