Cloud Engineer living in Perth, Western Australia

Type guards with Typescript and why you should use them

Posted on August 30, 2022
3 minute read

Small wooden cutout squares with letters and numbers on them used in the boardgame Scrabble.

A pattern I’ve come across often in web development is to make some kind of API call, do something with it (or do nothing!) and return it to the user. Many developers use this pattern with tools like fetch from the native JavaScript library or third parties like Axios. This is mostly fine, until something goes wrong, and then your application behaves in a way that you might not expect. This is a great example of where a typeguard can really shine, showing you not only better ways to handle your expected results, but also provide a better experience for your users.

Let’s start with an example. Lets say you are building a React application and you make an API call to some service, like a weather api and you need to show the results of that call to your users. You might have a call like this:

const getWeather = async (): Promise<WeatherResponse> => {
  return fetch(url).then(response => {
    if (!response.ok) {
      throw new Error(response.statusText);
    }
    return response.json() as Promise<WeatherResponse>;
  });
};

In this example we’re returning the response which is a promise which resolves to a JavaScript object and telling the Typescript compiler that we know this will be of type Promise<WeatherResponse>. We know that, and we’re telling Typescript that. But what if the API changes and starts to return something which doesn’t match that object shape? Or what if the API returns nothing, but is still a successful call?

This is where type guards can help us.

Consider this code:

type WeatherApiResult = { temperature: number }
const isWeatherData = (data: unknown): data is WeatherApiResult => {
  return typeof data === "object" && data !== null && "temperature" in data;
}

There’s few things going on here.

  1. We’re declaring a type for the expected response from the API that we’re going to need to use in our app.
  2. We’ve created a function called isWeatherData which take a data argument of type unknown and returns us a boolean.
  3. We’re checking in that function that the type of data is an object, we’re checking it’s not a null value, and finally we check that temperature is a key in the object.

How do we then use this in our API call? Let’s review the updated code:

const getWeather = async (): Promise<unknown> => {
  return fetch(url).then(response => {
    if (!response.ok) {
      throw new Error(response.statusText);
    }
    return response.json() as Promise<unknown>;
  });
};

Note that now, our return object is unknown. In our code where we want to use this API response, we can check it and handle it as necessary. For example:

try {
  // This will be a resolved promise of type unknown.
  const apiResponse = await getWeather();

  if (isWeatherData(apiResponse)) {
    // Here the type guard has confirmed to us that API response is a type we expect.
    // In this case: WeatherApiResult
    return apiResponse
    } else {
    // This is where you would handle data which didn't contain weather data.
    return "We got something back we weren't expecting."
  }
} catch (error) {
  // The catch will go here if the getWeather() promise rejects.
  return "An error occured calling the API"
}

I hope this sample has demonstrated the value of a type guard and how you can build a better experience for your users by handling cases at run time where if you get results you don’t expect that your users will have a helpful message or alert saying something didn’t go right, rather than it just not working or worse with errors all over the page because something blew up.