Today I’m going to share one of my favourite TypeScript patterns: type guards.
Generic Type Guards are pretty handy for narrowing types! — Harry Nicholls (@HarryNicholls) February 27, 2019
Do you regularly work with TypeScript?
You might’ve run into an error similar to this:
A swift slap on the wrist from the TypeScript compiler
If you know that vehicle does have the property turnSteeringWheel, you can quickly solve this issue by casting vehicle as any.
But you could be wrong!
What if the app changes and future-you forgets what you did?! We’ve all been there…
When using any, you’re overriding the compiler and losing the type-safety that TypeScript affords you.
Yes, the compiler is happy. Yes your code runs, and your tests pass.
Hooray 🎉
But don’t celebrate just yet. It’s a fragile approach and you’re inviting errors to the party.
If there’s a child of this component that doesn’t have props, you’ll get a runtime error.
Thankfully, using any is not the only solution.
Let me propose a solution that un-invites runtime errors to your type-safe party.
Stay up to date with latest featured content in our Front End Hub!
A type-safe solution
TypeScript allows you to create something called a type guard.
The TypeScript Handbook describes type guards as:
Some expression that performs a runtime check that guarantees the type in some scope.
The key here is “a runtime check”. This ensures that the variable is the type you’re expecting at the moment your code is executed.
That’s great, but will it satisfy the compiler too?
Yes it will!
Another key characteristic of a type guard is that it must return a type predicate.
A type predicate being something along the lines of vehicle is Car or event is MouseEvent.
When a function returns this, it tells the compiler that the type of the thing passed in will be narrowed. That is, the type will become more specific.
vehicle is Car tells the compiler that we’ve now confirmed that the vehicle passed in to the type guard is in fact a Car.
Say for example, you have a system like this:
interface Vehicle {
move: (distance: number) => void;
}
class Car implements Vehicle {
move = (distance: number) => {
// Move car…
};
turnSteeringWheel = (direction: string) => {
// Turn wheel…
};
}
class VehicleController {
vehicle: Vehicle;
constructor(vehicle: Vehicle) {
this.vehicle = vehicle;
}
}
const myCar = new Car();
const vehicleController = new VehicleController(myCar);
const { vehicle } = vehicleController;
vehicle.turnSteeringWheel('left'); // Error: Property 'turnSteeringWheel' does not exist on type 'Vehicle'
Even though you know vehicle is a Car, VehicleController has stripped it down to a basic Vehicle.
And that’s how the TypeScript compiler interprets it.
Because Vehicle doesn’t have the property turnSteeringWheel, you can’t compile this code because the TS compiler thinks you’ve made a mistake.
Type guards can help you bridge this gap. A simple one looks like this:
This casts vehicle as a Car, and checks if it has a turnSteeringWheelmethod.
The compiler still considers vehicle to be a Vehicle, so you have to cast it as a Car for a second time, but at least you can be confident that turnSteeringWheel is defined.
The downside is this pattern isn’t very reusable, you’d have to write the same block of code every time you want to use it.
Built-in type guards
TypeScript comes with some built-in type guards: typeof and instanceof.
They’re very useful, but have limited scope.
For example, typeof can only be used to check string, number, bigint, function, boolean, symbol, object, and undefined types.
You might be thinking, “What other types are there?”
The catch is that typeof only performs a shallow type-check.
It can determine if a variable is a generic object, but cannot tell you the shape of the object.
const myString: any = 'hello';
if (typeof myString === 'string') {
console.log(myString.toUpperCase()); // myString: string
}
const myObject: any = {
foo: 'bar'
};
if (typeof myObject === 'object') {
console.log(myObject.foo); // myObject: any
}
So typeof is best used for simple types, or, as I most often use it, to check for undefined variables:
interface MyComponentProps {
optionalProp?: string;
id: string;
className: string;
}
const MyComponent = ({
optionalProp,
className,
...props
}: MyComponentProps) => {
let combinedClassNames = className;
if (typeof optionalProp !== 'undefined') {
combinedClassNames = `${className} optionalClass`;
}
return (
<div className={combinedClassNames} {...props}>
Hello, World!
</div>
);
};
Having said this, you could also do away with typeof here by using a ternary: const combinedClassNames = optionalProp ? `${className} optionalClass` : className; .
The second built-in type guard, instanceof, is a bit more interesting.
You can use it to check if a variable is an instance of a given class.
I wonder what’ll happen if we apply that to the Vehicle and Car problem from above?
if (vehicle instanceof Car) {
vehicle.turnSteeringWheel('left'); // No TS errors as 'vehicle: Car'
}
Cool! No more TS errors, and the syntax is a lot less verbose than the casting method.
There’s just one catch here: instanceof only works for classes…
So this doesn’t work:
const anotherCar = {
move: (distance: number) => null,
turnSteeringWheel: (direction: string) => null
}; // 'anotherCar' has the same shape as 'Car', but is not an instance of 'Car'
const anotherVehicleController = new VehicleController(anotherCar);
const { vehicle } = anotherVehicleController;
if (vehicle instanceof Car) {
vehicle.turnSteeringWheel('left'); // No TS errors as 'vehicle: Car'
console.log('Nice car!');
} else {
console.log("Dude, where's my car?!");
}
// console: Dude, where's my car?!
Even though anotherCar has the same shape as a Car, it’s not an instance of the class, so in this case vehicle instanceof Car returns false.
Both typeof and instanceof are useful, but have limited scope in modern, functional programming.
So how can you check the type of any object?
Luckily, you can create custom type guards.
Custom type guards
If you’re into functional programming, you’ll be more accustomed to NOT using classes, so I haven’t really helped you solve any problems yet.
You can still use type guards, you just need to create your own.
Let’s continue to use the vehicle and car example, like so:
const isCar = (variableToCheck: any): variableToCheck is Car =>
(variableToCheck as Car).turnSteeringWheel !== undefined;
You can pass anything into this function, and check if it’s a Car.
One of the most important features of this function is the return type, variableToCheck is Car.
As mentioned above, this is a “type predicate”.
We’re not just checking if the variable has a turnSteeringWheel property, we’re telling the TS compiler if the logical statement returns true, then this variable is a Car.
So now we can refactor this:
const anotherCar = {
move: (distance: number) => null,
turnSteeringWheel: (direction: string) => null
}; // 'anotherCar' has the same shape as 'Car', but is not an instance of 'Car'
const anotherVehicleController = new VehicleController(anotherCar);
const { vehicle } = anotherVehicleController;
if (vehicle instanceof Car) {
vehicle.turnSteeringWheel('left'); // No TS errors as 'vehicle: Car'
console.log('Nice car!');
} else {
console.log("Dude, where's my car?!");
}
// console: Dude, where's my car?!
To this:
const anotherCar = {
move: (distance: number) => null,
turnSteeringWheel: (direction: string) => null
};
const anotherVehicleController = new VehicleController(anotherCar);
const { vehicle } = anotherVehicleController;
if (isCar(vehicle)) {
vehicle.turnSteeringWheel('left'); // No errors, because 'vehicle: Car'
console.log('Nice car!');
} else {
console.log("Dude, where's my car?!");
}
// console: Nice car!
Cool, eh?
You can apply this pattern to any type you can dream up.
What goes into a custom type guard?
The key features of a custom type guard are:
- Returns a type predicate, e.g. variableToCheck is Car
- Contains a logical statement that can accurately determine the type of the given variable, e.g (variableToCheck as Car).turnSteeringWheel !== undefined
Some other examples include:
const isNumber = (variableToCheck: any): variableToCheck is number =>
(variableToCheck as number).toExponential !== undefined;
const isString = (variableToCheck: any): variableToCheck is string =>
(variableToCheck as string).toLowerCase !== undefined;
If you have a lot of types to check though, it could become quite tedious to create and maintain a unique type guard for each type.
That’s where another awesome TypeScript feature comes in: generics.
A generic type guard
If you’re not familiar with generics, check out the TypeScript Handbook generics section.
You might’ve noticed that there’s a common pattern, and a lot of repitition, between the custom type guards above.
They’re not very DRY. This is where generics come in handy.
Here’s a generic type guard, that you can use to narrow types whenever you need to.
export const isOfType = <T>(
varToBeChecked: any,
propertyToCheckFor: keyof T
): varToBeChecked is T =>
(varToBeChecked as T)[propertyToCheckFor] !== undefined;
You can use it like so:
const anotherCar: Car = {
move: (distance: number) => null,
turnSteeringWheel: (direction: string) => null
};
const anotherVehicleController = new VehicleController(anotherCar);
const { vehicle } = anotherVehicleController;
if (isOfType<Car>(vehicle, 'turnSteeringWheel')) {
vehicle.turnSteeringWheel('left'); // No errors, because vehicle: Car
console.log('Nice car!');
} else {
console.log("Dude, where's my car?!");
}
// console: Nice car!
Now you don’t have to write unique type guard functions for every single type you want to check.
Just plug your desired type and unique property into isOfType.
You’ll satisfy the compiler, while being confident that you have type-safety at runtime.
Everybody wins!
A caveat
Uncle Ben knows what’s up
The type guards I’ve shown so far are fairly naïve.
In the Car example, we’re assuming that turnSteeringWheel is unique to Cars.
The type guard asserts that if turnSteeringWheel exists on the given variable, then it’s a Car.
But this might not be true.
You could also have a Bus type:
interface Bus extends Vehicle {
turnSteeringWheel: (direction: string) => null;
isDelayed: boolean;
}
const myBus: Bus = {
move: (distance: number) => null,
turnSteeringWheel: (direction: string) => null,
isDelayed: true
};
const yetAnotherVehicleController = new VehicleController(myBus);
const { vehicle } = yetAnotherVehicleController;
if (isOfType<Car>(vehicle, 'turnSteeringWheel')) {
vehicle.turnSteeringWheel('left'); // The compiler thinks 'yetAnotherVehicle: Car', even though we know its a 'Bus'
console.log('Nice ca... wait a second...');
} else {
console.log("Dude, where's my car?!");
}
// console: Nice ca... wait a second...
In this scenario, there isn’t a problem with the Bus being cast as a Car, because we’re only calling turnSteeringWheel which happens to exist on both types.
However this might be a problem for you in more complex scenarios, so use type guards responsibly.
Recap
Type guards are incredibly useful for narrowing types, satisfying the TS compiler, and helping to ensure runtime type-safety.
The most common scenario in which you’d want to use one is when a type you’re given isn’t as specific as you’d like (Vehicle versus the more specific Car).
There are built-in type guards that you can use (typeof and instanceof) but they have limited use in functional programming.
If you need more flexible type guards you can define your own with a type predicate (vehicle is Car), and a “unique” property of the type you want.
Or you can use the generic isOfType util I outlined above.
Just remember that custom type guards can be a little naïve in complex apps.
Tl;dr: type guards rock 🤘.