

Discover more from JK’s Blog
Type guards with Typescript and why you should use them
Type guards in typescript can help improve the user experience and the developer experience of writing less buggy code and better handling of edge cases.
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.
We’re declaring a type for the expected response from the API that we’re going to need to use in our app.
We’ve created a function called
isWeatherData
which take a data argument of typeunknown
and returns us a boolean.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 thattemperature
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.