Type Guards in TypeScript

In TypeScript, you often deal with variables that could be one of several types this is known as a Union Type (e.g., string | number). However, if you try to access a property that only exists on a string when the variable might still be a number, TypeScript will throw an error to protect you.

Type guards are the solution to this problem. They are expressions that perform a runtime check to narrow down the type of a variable within a specific conditional block. Once a type guard is triggered, TypeScript "remembers" the result and allows you to safely access type-specific properties and methods inside that scope.

Developer Tip: Think of Type Guards as "Type Narrowing." You start with a broad possibility (a Union) and filter it down until you are certain of the specific type you're working with.

 

Key Concepts of Type Guards

  • Type Guard: A logical check (usually inside an if or switch statement) that tells the TypeScript compiler a variable is definitely of a specific type.
  • Custom Type Guards: Specialized functions that return a "type predicate," allowing you to encapsulate complex validation logic that can be reused across your project.
  • Built-in Type Guards: Native JavaScript operators like typeof, instanceof, and in that TypeScript leverages to understand your code's intent automatically.
Best Practice: Always use Type Guards instead of "Type Assertions" (using the as keyword) when possible. Type guards provide runtime safety, whereas assertions just tell the compiler to "trust you," which can lead to hidden bugs if you're wrong.

 

Example Usage of Type Guards

Example 1: Using typeof for Primitive Types

The typeof operator is the simplest way to distinguish between basic JavaScript types like strings, numbers, and booleans. It is perfect for handling raw data from inputs or configuration files.

function formatValue(value: string | number): string {
  if (typeof value === "string") {
    // TypeScript knows 'value' is a string here
    return value.trim().toUpperCase();
  } else {
    // TypeScript knows 'value' must be a number here
    return value.toFixed(2);
  }
}

console.log(formatValue("  hello  "));  // Output: "HELLO"
console.log(formatValue(42.567));       // Output: "42.57"

In this example:

  • The typeof check identifies whether value is a primitive string.
  • Inside the if block, you get full autocompletion for string methods like .trim().
  • Inside the else block, TypeScript is smart enough to know that if it isn't a string, it must be the only other option: a number.
Common Mistake: Be careful with typeof null. In JavaScript, typeof null returns "object". If your union type includes null, a simple typeof check might not be enough to distinguish it from an actual object or array.

Example 2: Using instanceof for Class Types

When working with Object-Oriented Programming (OOP) and custom classes, instanceof is your best friend. It checks if an object was constructed from a specific class.

class FileLogger {
  logToFile(msg: string) { console.log(`Writing to file: ${msg}`); }
}

class ApiLogger {
  sendToCloud(msg: string) { console.log(`Sending to API: ${msg}`); }
}

function executeLog(logger: FileLogger | ApiLogger, message: string): void {
  if (logger instanceof FileLogger) {
    logger.logToFile(message);
  } else {
    logger.sendToCloud(message);
  }
}

Here:

  • instanceof looks at the constructor of the object at runtime.
  • It allows you to safely call methods that exist on FileLogger but not on ApiLogger, and vice versa.
Watch Out: instanceof only works with classes. It will not work with TypeScript Interfaces because interfaces are removed (erased) during compilation and do not exist at runtime.

Example 3: Using in Operator for Object Types

The in operator is highly effective when you are dealing with different object shapes (interfaces) that don't use classes. It checks for the existence of a specific property name.

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

function printDetails(user: Admin | Employee) {
  console.log(`User: ${user.name}`);
  
  if ("privileges" in user) {
    // Narrowed to Admin
    console.log(`Privileges: ${user.privileges.join(", ")}`);
  } else {
    // Narrowed to Employee
    console.log(`Started on: ${user.startDate.toLocaleDateString()}`);
  }
}

Here:

  • The in operator checks if "privileges" is a key within the user object.
  • This is a very common pattern when handling API responses that might return different data structures based on the user's role.

 

Custom Type Guards

Sometimes built-in operators aren't enough, especially for complex validation logic. You can create a function that returns a Type Predicate. A type predicate takes the form parameterName is Type.

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

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

// This is a Custom Type Guard
function isBird(pet: Bird | Fish): pet is Bird {
  return (pet as Bird).fly !== undefined;
}

function move(pet: Bird | Fish) {
  if (isBird(pet)) {
    pet.fly(); // TypeScript is certain this is a Bird
  } else {
    pet.swim(); // TypeScript is certain this is a Fish
  }
}

In this logic:

  • The function returns a boolean, but the return type pet is Bird tells TypeScript: "If this function returns true, treat the variable as a Bird in the calling scope."
  • This makes your code much more readable and modular.
Developer Tip: Custom type guards are excellent for filtering arrays. If you have an array (string | null)[], you can use a custom type guard with .filter() to result in a clean string[] array that TypeScript recognizes.

 

Type Guards with Union Types

Type guards are most frequently used to handle "Discriminated Unions." This is a pattern where every type in a union has a common property (usually called kind or type) with a literal value.

interface SuccessResponse {
  status: 'success';
  data: string;
}

interface ErrorResponse {
  status: 'error';
  message: string;
}

function handleResponse(res: SuccessResponse | ErrorResponse) {
  if (res.status === 'success') {
    console.log("Data:", res.data);
  } else {
    console.log("Error:", res.message);
  }
}

This is often considered the "gold standard" of type narrowing in TypeScript because it is explicit, easy to read, and works perfectly with switch statements.

 

Summary

  • Type Guards: Essential tools for narrowing down Union types to a specific subtype, ensuring your code doesn't crash at runtime.
  • Built-in Type Guards: Use typeof for primitives (string, number, etc.), instanceof for class instances, and in for checking object properties.
  • Custom Type Guards: Use functions with the parameter is Type syntax for reusable, complex type-checking logic.
  • Type Safety: By using guards, you enable TypeScript's powerful static analysis to catch errors before you ever run your code.

Mastering type guards will make your TypeScript code significantly more robust and self-documenting, as the code itself explains the logic of your data structures.