[EN/TypeScript] Narrowing and User-Defined Type Guards

Tags
TypeScript
Created
June 1, 2024

This document covers Type Narrowing and User-Defined Type Guards and explains how these features make TypeScript's type system more flexible and precise.

Background

Union Type

A union type in TypeScript combines two or more types into one, indicating that a value can be of any of the specified types. When using a union type, you can only access members that are common to all the possible types of the variable or parameter.

In other words, while union types allow you to handle various types with a single variable, it requires caution to maintain type safety. Let's look at an example:

interface Cat {
    name: string;
    meow: () => void;
}

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

function getAnimal(): Cat | Dog {
    // Returns either a 'Cat' or a 'Dog'
}

let animal = getAnimal();
animal.name(); // This is safe because both 'Cat' and 'Dog' have 'name' method
// animal.meow(); // Error: Property 'meow' does not exist on type 'Dog'

In the code above, the getAnimal function returns a Cat or Dog type. Therefore, the animal variable has a Cat | Dog type, and you can call the name method, which is common to both types, but not the meow method, which only exists on the Cat type.

💬 So, how can you safely use methods that only exist on a specific type? This is the reason for writing this document.

When using union types in TypeScript, to safely access properties or methods unique to a specific type, you need to use type narrowing techniques. Let's explore how type narrowing and user-defined type guards can be effectively utilized.

image

Type Narrowing

Type narrowing is the process of reducing the range of possible types for a variable in TypeScript. TypeScript tries to make type analysis as precise as possible to enhance type safety.

  • typeof operator: Used to narrow down primitive types (e.g., string, number, boolean).
  • instanceof operator: Used to check if an object is an instance of a class and narrow the type accordingly.
  • in operator: Used to check if an object has a certain property and narrow the type based on that.
  • User-defined type guards: Custom functions that narrow types based on specific conditions.

User-Defined Type Guards

Sometimes, we want more control over how types change in our code. This is where user-defined type guards come in. A user-defined type guard is a function that you define to narrow down a type based on certain conditions.

Defining a User-Defined Type Guard

To define a user-defined type guard, you create a function with a return type that is a type predicate.

function isCat(animal: Cat | Dog): animal is Cat {
	return (animal as Cat).meow !== undefined;
}
  • In the function above, animal is Cat is the type predicate. A type predicate has the form parameterName is Type, where parameterName must be the name of a parameter in the current function.
  • Type predicate (animal is Cat): This predicate tells TypeScript that if the function returns true, the animal is of type Cat.

Using Type Guards to Narrow Types

TypeScript infers the type of a variable based on the result of the function call.

let animal = getAnimal();

if (isCat(animal)) {
	animal.meow();
} else {
	animal.bark();
}
  • In this code, TypeScript recognizes that if isCat(animal) returns true, then animal is of type Cat and allows the meow method to be called. Conversely, if it returns false, animal is recognized as a Dog and the bark method can be called.

Using Type Guards with Arrays

You can also use type guards to filter specific types from an array. Here’s how to extract only Cat types from an array of Cat | Dog elements.

const animals: (Cat | Dog)[] = [getAnimal(), getAnimal(), getAnimal()];
const cats: Cat[] = animals.filter(isCat);

In this example, the isCat type guard is used to filter out only Cat type elements from the animals array. The result is a new array of type Cat[].

Summary

  • Type Narrowing is the process of restricting a variable to a more specific type.
  • User-Defined Type Guards are custom functions that use type predicates to narrow types based on conditions.