@ncjamieson

RxJS: How to Use Type Guards with Observables

September 11, 2017 • 4 minute read

Chain-link fence
Photo by Tanner Van Dera on Unsplash

Since version 1.6, TypeScript has supported user-defined type guards.

When composing an observable, a type guard can be used to ensure that the correct type is inferred at compile time and that the received value is validated at run time. With run-time validation, problems can be caught early and descriptive errors can be thrown from locations close to where values are received — these are easier to diagnose than cannot-read-property-of-undefined errors that are thrown from seemingly unrelated locations.

What is a type guard?

A user-defined type guard is a function that performs a run-time check to evaluate its returned type predicate.

Let’s look at an example that uses the following interface:

interface Person {
  name: string;
  age: number;
}

A basic type guard that can be used to determine whether or not a value is compatible with the Person interface looks something like this:

function isPerson(value: any): value is Person {
  return (
    value && typeof value.name === "string" && typeof value.age === "number"
  );
}

Of particular interest is the function signature’s return type: value is Person. This is the type predicate and it’s what makes the function a type guard.

The guard’s signature tells TypeScript that the function will return a value that represents the evaluation of the predicate, so our user-defined type guard can be used like this:

function toString(value: Person | string): string {
  if (isPerson(value)) {
    return `name = ${value.name}; age = ${value.age}`;
  } else {
    return value;
  }
}

Recognising isPerson as a type guard, TypeScript knows that if it returns a truthy result, value must be a Person and the name and age properties can therefore be used within the if block statement.

TypeScript also knows that if isPerson returns a falsy result, value must be a string — as that is the only alternative.

How do we use a type guard with an observable?

With RxJS, there is often more than one way of implementing behaviour and that’s the case with our type guard. Let’s look at what it is we are trying to do.

A type guard evaluates a type predicate for a specified value and returns a boolean result. We want to use a type guard so that the correct type is inferred at compile time. Although we are not changing the value in any way, this is conceptually similar to the map operator: before using the guard, the observable will have a general type; and after using the guard, it will have a more specific type.

Let’s create an guard operator that applies a type guard to a source observable’s emitted values. We can leverage the map operator in its implementation, like this:

import { Observable, OperatorFunction } from "rxjs";
import { map } from "rxjs/operators";

export function guard<T, R extends T>(
  guard: (value: T) => value is R,
  message?: string
): OperatorFunction<T, R> {
  return (source) =>
    source.pipe(
      map((value) => {
        if (guard(value)) {
          return value;
        }
        throw new Error(message || "Guard rejection.");
      })
    );
}

The guard operator takes a type guard — r — and an optional rejection message. The ‘projection’ function that it passes to map doesn’t project values; it just returns values that pass the type guard and throws errors for values that do not.

When calling the guard operator, its type parameters — T and R — can be inferred from the type guard, so they don’t need to be specified explicitly.

Let’s see how our guard operator could be used with Angular’s HttpClient. When the client’s get method is called like this:

const person = http.get(`/people/${id}`);

The type of person will be inferred as Observable<Object> — the return type of the get method. Object is not a particularly useful type, so the recommended way of calling get involves specifying a type parameter, like this:

const person = http.get<Person>(`/people/${id}`);

When called this way, the type of person will be inferred as Observable<Person>. This is a compile-time type assertion. It informs TypeScript that the response’s content will have a shape compatible with the Person interface. However, at run time, the response’s content could be anything.

When the guard operator is used, like this:

const person = http.get(`/people/${id}`).pipe(guard(isPerson));

The type of person will be inferred as Observable<Person> and a run-time check will be performed on the response’s content, effecting an easy-to-diagnose error if the content fails the type guard.

RxJS is nothing if not flexible and there are other ways of performing the compile-time type assertions and the run-time validations that we’ve looked at in this article. However, if you are already using interfaces and type guards, a guard operator makes it easy to add assertions and validations to observables that receive content from external sources. It’s an approach that I’ve found to be effective when using Angular’s HttpClient, AngularFire2’s list and object observables, and RxJS’s ajax observable.


Nicholas Jamieson’s personal blog.
Mostly articles about RxJS, TypeScript and React.
MastodonGitHubSponsor

© 2022 Nicholas Jamieson All Rights ReservedRSS