RxJS: How to Use Type Guards with Observables
September 11, 2017 • 4 minute read
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.