Custom User-Defined Type Guards

Table of Contents

  • Introduction
  • What Are User-Defined Type Guards?
  • Syntax of User-Defined Type Guards
  • Why Use Custom Type Guards?
  • Creating Custom Type Guards
    • Example 1: Checking for Specific Properties in an Object
    • Example 2: Complex Type Guards for Unions
  • Using Type Guards in Conditional Statements
  • When to Use Custom Type Guards
  • Conclusion

Introduction

TypeScript is designed to be both flexible and robust, providing developers with a powerful type system. One of the key features of TypeScript is type guards, which allow you to refine and narrow down the type of a variable within a certain scope. While TypeScript provides built-in type guards like typeof and instanceof, there are times when you might want to create your own custom user-defined type guards.

Custom type guards are particularly useful when you need to check for more complex types or specific conditions that are not easily handled by the built-in type guards. By using user-defined type guards, you can ensure type safety in cases where TypeScript cannot automatically infer the correct type.

In this article, we will explore how to create and use custom user-defined type guards in TypeScript.


What Are User-Defined Type Guards?

A user-defined type guard is a TypeScript function that returns a boolean value and tells TypeScript the specific type of an object or variable. These type guards help TypeScript narrow the type of a variable based on runtime checks, ensuring that the correct type is inferred within a block of code.

A user-defined type guard function follows a specific pattern, where it includes a type predicate in the return type. This type predicate is a special syntax that tells TypeScript what type the function is narrowing to.


Syntax of User-Defined Type Guards

The syntax for a custom user-defined type guard looks like this:

function isSomeType(obj: any): obj is SomeType {
// Check for properties or conditions to determine if the object is of type 'SomeType'
return obj !== null && typeof obj === 'object' && 'propertyName' in obj;
}

Here:

  • obj is SomeType is a type predicate that tells TypeScript that the function will return true only if the obj is of type SomeType. This allows TypeScript to narrow the type of obj within the conditional block where the guard is used.

Why Use Custom Type Guards?

While TypeScript provides built-in type guards like typeof and instanceof, there are cases where these aren’t enough. For example:

  • You may need to check if an object has a specific property or method.
  • You may want to perform complex checks on union types.
  • You might be working with types that are difficult to distinguish without custom logic (e.g., checking whether a variable is an instance of one of several classes).

By creating custom user-defined type guards, you can fine-tune the type checking process, ensuring type safety even in the most complex scenarios.


Creating Custom Type Guards

Example 1: Checking for Specific Properties in an Object

Let’s say you have a union type, and you need to narrow the type based on whether an object has a particular property.

interface Cat {
name: string;
breed: string;
}

interface Dog {
name: string;
barkSound: string;
}

type Animal = Cat | Dog;

// User-defined type guard to check if an animal is a Cat
function isCat(animal: Animal): animal is Cat {
return (animal as Cat).breed !== undefined;
}

// Using the custom type guard
const pet: Animal = { name: "Whiskers", breed: "Siamese" };

if (isCat(pet)) {
console.log(`${pet.name} is a cat of breed ${pet.breed}`);
} else {
console.log(`${pet.name} is a dog that barks like ${pet.barkSound}`);
}

In this example, isCat is a user-defined type guard that checks if the animal object has a breed property. By using this function, TypeScript can narrow the type of pet to Cat inside the if block, allowing safe access to the breed property.

Example 2: Complex Type Guards for Unions

In complex applications, you may have a union type with multiple shapes or interfaces, and you may need to differentiate between them based on several properties.

interface Bird {
fly(): void;
name: string;
}

interface Fish {
swim(): void;
name: string;
}

type Animal = Bird | Fish;

// User-defined type guard to check if an animal can fly
function isBird(animal: Animal): animal is Bird {
return (animal as Bird).fly !== undefined;
}

const petFish: Animal = { name: "Goldy", swim: () => console.log("Swim!") };
const petBird: Animal = { name: "Tweety", fly: () => console.log("Fly!") };

if (isBird(petFish)) {
petFish.fly(); // Error: petFish does not have a fly method
} else {
petFish.swim(); // Correct: petFish has swim method
}

if (isBird(petBird)) {
petBird.fly(); // Correct: petBird can fly
}

In this case, isBird is a user-defined type guard that checks if the animal can fly. TypeScript will narrow the type of pet inside the if block to Bird when the fly method is present.


Using Type Guards in Conditional Statements

Custom user-defined type guards are most useful when working with conditional statements. Once you define a custom type guard, you can use it to safely narrow types within if or switch blocks.

For example:

function processAnimal(animal: Animal) {
if (isBird(animal)) {
animal.fly(); // TypeScript knows animal is of type Bird
} else {
animal.swim(); // TypeScript knows animal is of type Fish
}
}

In this example, the custom type guard isBird is used to narrow down the type of animal and ensure that the correct method is called for either a bird or a fish.


When to Use Custom Type Guards

You should consider using custom type guards when:

  1. You have a complex union type and need to differentiate between types based on runtime properties or conditions.
  2. Built-in type guards are not sufficient (e.g., when you need to check the presence of specific properties or perform logic).
  3. You work with class hierarchies or interfaces and need to differentiate between different classes or object shapes.
  4. You want to improve the safety and maintainability of your code by making type checks more explicit and precise.

Conclusion

Custom user-defined type guards are a powerful feature of TypeScript that allow you to refine and narrow types based on runtime checks. By creating your own type guards, you can improve type safety, especially in scenarios that involve complex types, union types, or class hierarchies.

By defining functions that return a boolean and include a type predicate, TypeScript is able to infer the exact type of a variable in specific conditions, enabling safer, more reliable code. Whether you’re checking for properties in objects, dealing with unions, or working with classes, custom type guards give you full control over how types are checked and refined.