DEV Community

Neil Charlton
Neil Charlton

Posted on

Type Guards Using Type Predicates in TypeScript

Type predicates are a powerful feature in TypeScript that allow you to create custom type guards to narrow down types in your code. Here's a simple tutorial on how to use them effectively.

What are Type Predicates?

A type predicate is a special return type for a function that tells TypeScript that the function is checking whether a variable is of a specific type. The syntax is:

function isType(param: SomeType | OtherType): param is SomeType {
  // logic to determine if param is SomeType
}
Enter fullscreen mode Exit fullscreen mode

Basic Example

Let's start with a simple example:

// Define two types
interface Fish {
  swim: () => void;
}

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

// Create a type predicate function
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

// Use the type predicate to narrow down types
function moveAnimal(animal: Fish | Bird) {
  if (isFish(animal)) {
    // TypeScript knows animal is Fish here
    animal.swim();
  } else {
    // TypeScript knows animal is Bird here
    animal.fly();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Guide

  1. Define Your Types First, define the types you want to distinguish between:
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}
type Shape = Circle | Square;
Enter fullscreen mode Exit fullscreen mode
  1. Create a Type Predicate Function Create a function that returns a type predicate:
function isCircle(shape: Shape): shape is Circle {
  return shape.kind === "circle";
}
Enter fullscreen mode Exit fullscreen mode

The shape is Circle part is the type predicate. It tells TypeScript that if this function returns true, the parameter is of type Circle.

  1. Use the Type Predicate for Type Narrowing Now you can use your predicate function to narrow types:
function getArea(shape: Shape) {
  if (isCircle(shape)) {
    // TypeScript knows shape is Circle here
    return Math.PI * shape.radius ** 2;
  } else {
    // TypeScript knows shape is Square here
    return shape.sideLength ** 2;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Type Predicates with Arrays

Type predicates are especially useful when filtering arrays:

Copy Code
const shapes: Shape[] = [
  { kind: "circle", radius: 5 },
  { kind: "square", sideLength: 10 },
  { kind: "circle", radius: 3 }
];

// Filter to get only circles
const circles: Circle[] = shapes.filter(isCircle);

// You can also define inline predicates
const squares: Square[] = shapes.filter((shape): shape is Square => shape.kind === "square");
Enter fullscreen mode Exit fullscreen mode

More Advanced Example: Complex Type Predicates

You can create more complex type predicates that check multiple conditions:

interface User {
  name: string;
  email?: string;
  phone?: string;
}

// Check if a user has complete contact information
function hasContactInfo(user: User): user is User & { email: string; phone: string } {
  return typeof user.email === "string" && typeof user.phone === "string";
}

function contactUser(user: User) {
  if (hasContactInfo(user)) {
    // TypeScript knows user has both email and phone here
    sendEmail(user.email);
    sendSMS(user.phone);
  } else {
    // Handle incomplete contact info
  }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Keep predicates simple: Focus on checking one specific aspect of a type.
  • Use descriptive names: Name your predicate functions clearly (e.g., isCircle, hasContactInfo).
  • Avoid side effects: Type predicates should be pure functions without side effects.
  • Consider using discriminated unions: They work well with type predicates.
  • By using type predicates, you can write more type-safe code that's also more readable and maintainable.

Top comments (0)