Runtime type safety with type guards

Typescript is a typed programming language that offers developers the ability to write code in a type safe way. But Typescript doesn't help with making sure data you fetch from an api adheres to the type you expect it, unless you take the necessary steps to ensure that it does. Type guards is a way to validate that a piece of data has the expected types. If the type guard fails, you can then deal with it appropriately. Lets take an example.

interface Movie {
  name: string;
  releaseDate: string;
  categories: string[];
  actors: string[];
}

function getMovies(actor: string) {
  const res = await fetch(`https://movieapi.com/movies?actor=${actor}`);

  const movies = await res.json();

  return movies as Movies[];
}

The problem with the above code is that we haven't validated that the response is an actual list of movies. It could be something else.

A naive type guard of the response:

const isMovie = (movie: unknown): movie is Movie =>
  typeof movie.name === 'string' &&
  typeof move.releaseDate === 'string' &&
  Array.isArray(movie.categories) &&
  Array.isArray(movie.actors);

This is a very bad example of a type guard. Not only is it very hard to read, and it doesn't check the type of the arrays properly. Lets construct re-usable helper functions that we use to compose type guards in a cleaner way.

const isString = (value: unknow): value is string => typeof value === 'string';
const isNumber = (value: unknown): value is number => typeof value === 'number';

These give us a good starting point. The following examples are inspired by Type Guard Composition which is a great article on this topic.

Object Type Guard

Now lets create an object type guard, as this is the most commonly used when interacting with APIs.

type Guard<T = unknown> = (x: unknown) => x is T;

type GuardReturnType<T extends Guard> = T extends Guard<infer U> ? U : never;

type Key = string | number | symbol;

type GuardRecord = Record<Key, Guard>;

const isObject =
  <T extends GuardRecord>(guards: T) =>
  (x: unknown): x is { [key in keyof T]: GuardReturnType<T[key]> } =>
    typeof x === 'object' &&
    x !== null &&
    Object.entries(x).every(([key, value]) => guards[key](value));

Lets start from top. The Guard is a helper function that takes a generic and returns wheter or not it is of that generic type. The GuardReturnType type extends the Guard and infers the U type and returns wheter or not its that value. And finaly the GuardRecord would represent a key value pair of any generic object, with the gaurd set to its value and key is of any valid type of a generic object key in typescript.

Here is an example usage of the above object type guard.

const isPerson = isObject({
  name: isString,
  age: isNumber,
});

To check the movie object, we also need to be able to handle an array. So lets do that.

Array type guard

type Guard<T = unknown> = (x: unknown) => x is T;

const isArray =
  <T extends Guard>(guard: T) =>
  (x: unknown): x is T[] =>
    Array.isArray(x) && x.every((y) => guard(y));

const isArrayOfStrings = isArray(isString);

Here we iterate over each item in the array and run the guard function. In the isArrayOfStrings example we pass in the isString function to check that its a string.

Finally we can construct our type checking of the movie type.

const isMovie = isObject({
  name: isString,
  releaseDate: isString,
  categories: isArray(isString),
  actors: isArray(isString),
});

This is way better than what we started with.

To see more examples such as of other types such as tuples, record, optionality and recursive types, checkout this article: Type Guard Composition

Useful libraries

Doing these type guard is a good way of making sure that your code is type safe during the run-time. Sometimes its easier to just use existing libraries than re-inventing the wheel. Here are some useful libraries that one can start using:

← Back to home