User Defined Type Guards in TypeScript

Created: Nov 13th 2022 - Updated : Nov 13th 2022

A Type Guard is a technique used to test if a value is of a specific type depending on the boolean returned by a regular function that contains (usually) a conditional block. This technique is used to make sure that the tested value is of that specific type we are checking for.

Type Guards are pretty helpful as you can imagine. One example could be that we retrieve data from an API but we don't know if that returned value is of the Type we are looking for. To do so we can create a "User Defined Type Guard"

Let's see how it is done.

retrieve-pancake.ts

let couldBePancake: unknown  // Api response retrieved

// Custom typeguard function
type Pancake = {
	name: string;
    variant: 'salty' | 'sweet';
    toppings: number;
    totalPrice: number;
 }
 
 	console.log(couldBePancake.name) // Object is of type unknown
 

in the example above, we cannot use 'couldBePancake.name' because we don't know yet if it is of type 'Pancake'. We need to implement the following :

retrieve-pancake.ts

let couldBePancake: unknown  // Api response retrieved

// Custom typeguard function
type Pancake = {
	name: string;
    variant: 'salty' | 'sweet';
    toppings: number;
    totalPrice: number;
 }
 
 // Exhaustively checking if obj contains all attributes of Pancake
 function isPancake(obj: any): obj is Pancake {
 	return obj.name && obj.variant && obj.toppings && totalPrice
 }
 
 // calling the guard type
 if (isPancake(couldBePancake)) {
 	console.log(couldBePancake.name) // no errors here
 }
 

Since we narrowed the type down to make sure it is a Pancake, we can use couldBePancake.name.

In short our logic goes like "If this is indeed of Type Pancake, then we can go ahead and use couldBePancake.name ." And all our TS errors are gone.

We have to take note that TypeScript doesn't assume type guards remain active in callback as making this assumption is dangerous.

retrieve-pancake.ts

let couldBePancake: unknown  // Api response retrieved
let basePrice: number; // comes from configuration

// Custom typeguard function
type Pancake = {
	name: string;
    variant: 'salty' | 'sweet';
    toppings: number;
 }
 
 // Exhaustively checking if obj contains all attributes of Pancake
 function isPancake(obj: any): obj is Pancake {
 	return obj.name && obj.variant && obj.toppings && totalPrice
 }
 
 // calling the guard type
 if (isPancake(couldBePancake)) {
 	console.log(couldBePancake.name)
    // BUT this will not work
    calculateTotal(()=> {
    	console.log(couldBePancake.toppings * 1.1 + basePrice) // TS Error: Object is possibly 'undefined'
    }
 }
 

if we want to fix this 'issue' we can simply store the inferred safe value in a local variable. That way TypeScript can easily understand that it doesn't get changed externally.

retrieve-pancake.ts

let couldBePancake: unknown  // Api response retrieved
let basePrice: number; // comes from configuration

// Custom typeguard function
type Pancake = {
	name: string;
    variant: 'salty' | 'sweet';
    toppings: number;
 }
 
 // Exhaustively checking if obj contains all attributes of Pancake
 function isPancake(obj: any): obj is Pancake {
 	return obj.name && obj.variant && obj.toppings && totalPrice
 }
 
 // calling the guard type
 if (isPancake(couldBePancake)) {
 	console.log(couldBePancake.name)
    const numOfToppings = couldBePancake.toppings
    calculateTotal(()=> {
    	console.log(numOfToppings * 1.1 + basePrice) // TS understands that numOfToppings inferred type is "number".
    }
 }