Working with Type Predicates in TypeScript

Updated: January 7, 2024 By: Guest Contributor Post a comment

Overview

TypeScript’s type system allows for sophisticated types and type guards using type predicates. This feature is particularly useful for user-defined type guards that can check if a variable belongs to a specific type. In this guide, we’ll explore how to define and use type predicates, stepwise with code examples.

Introduction to Type Predicates

In TypeScript, type predicates are a way to give a type-checker hints about the type of variable. A type predicate takes the form parameterName is Type, where parameterName must be a parameter of the current function. Here’s a simple example to show how it’s used:

function isString(test: any): test is string {
    return typeof test === 'string';
}

let item: any = 'Hello, TypeScript!';

if (isString(item)) {
    // Item is now known to be a string
    console.log(item.toUpperCase()); // OK
}

With a type predicate, we ensure that item is treated as a string inside the if-block, allowing us to safely call string-specific methods like toUpperCase.

Using Type Predicates for Custom Types

Type predicates become even more handy when dealing with custom types and interfaces. Let’s define an interface Animal and use a type predicate to check our types.

interface Cat {
    purr: () => void;
}

interface Dog {
    bark: () => void;
}

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal).purr !== undefined;
}

const whiskers = { purr: () => console.log('Purr!') };
const fido = { bark: () => console.log('Woof!') };

if (isCat(whiskers)) {
    whiskers.purr();
} else {
    fido.bark();
}

Here, we defined custom types for Cat and Dog. The type predicate isCat lets us differentiate between them within the function scope.

Refining Type Predicates

For more nuanced situations, we might want to refine our type predicates further. Consider a scenario where we have several types inheriting from a common base type:

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

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

class Sparrow implements Bird {
    fly() { console.log('Flitter-flutter!'); }
}

class Salmon implements Fish {
    swim() { console.log('Swish-swash!'); }
}

function isFish(pet: Bird | Fish): pet is Fish {
    return (pet).swim !== undefined;
}

let myPet = new Sparrow();

if (isFish(myPet)) {
    myPet.swim();
} else {
    myPet.fly();
}
// In this example, the predicate helps us ensure that we are calling the right method for the given pet instance.

Type narrowing also works exceptionally well with union types and type predicates.

Advanced Techniques with Type Predicates

Type predicates can be combined with other TypeScript features for more complex scenarios. For instance, let’s integrate type predicates with generics:

function isInstanceOf<T>(obj: any, className: new (...args: any[]) => T): obj is T {
    return obj instanceof className;
}

class User {
    constructor(public name: string) {}
}

let object: any = new User('Alice');

if (isInstanceOf<User>(object, User)) {
    console.log(`User's name is ${object.name}`);
}
// The function isInstanceOf is a generic function that checks an object against a type and can be reused across different types.

We have also seen TypeScript’s usability shoots up when combining type predicates with conditional types, mapped types, and utility types.

Handling Errors and Edge Cases

While creating type predicates, handle potential errors and edge cases to prevent type issues at runtime. Be mindful of null and undefined values and use the strict mode to catch such errors early. Additionally, ensure that your predicate functions do not have side effects; they should only perform type checking.

Conclusion

Type predicates in TypeScript offer powerful capabilities for guarding the types of variables and enhancing code correctness and developer experience. As we’ve explored from basic examples to more complex scenarios, understanding and using type predicates can significantly improve type safety and provide clarity to anyone reviewing your code base.