TypeScript - Beware the user-defined type guards
This post is part of an ongoing series on TypeScript. In it, I try to explain in a detailed way different situations I’ve faced while building relatively big React applications using TypeScript, and moving them from JavaScript. Here’s a list of other posts I’ve written about TypeScript:
The is
keyword in TypeScript is used to specify that a function is a type predicate. Here’s the Wikipedia entry for predicates:
In mathematical logic, a predicate is commonly understood to be a Boolean-valued function
P
:X → {true, false}
, called the predicate onX
. […] Informally, a predicate is a statement that may be true or false depending on the values of its variables.
A type predicate is a function returning a boolean value that can be used to identify the type of an expression. In TypeScript, type predicates can be expressed using the following syntax:
function isCollie(dog: Dog): dog is Collie {
return dog.breed === 'collie' // Must be a Collie
}
Type guards can be implemented using the instanceof
and typeof
JavaScript operators, but can also be implemented with the is
keyword like in the example above. Type guards of this nature are user-defined type guards.
While useful, user-defined type guards are a vector for runtime errors in TypeScript applications. It’s very easy to implement them in a way that makes a piece of code unsafe. Unsuspecting developers that use them from there on may expand the surface of unsafety. Here’s some example code:
interface Animal {}
interface Bird extends Animal {
fly: Function
}
interface Dog extends Animal {
bark: Function
}
function isBird(animal: Animal): animal is Bird {
return true // If you say so...
}
const norbert: Dog = { bark: () => console.log('Wharg!') }
if (isBird(norbert)) {
norbert.fly() // Compiles, error during runtime
}
Granted, that’s a sloppy snippet, but it should serve to illustrate the example. Out in the wild, there are some things you can try to keep in mind in order to avoid this type of problem:
- Implement type guards in a way that leaves out opportunities for runtime failure.
- Make use of TypeScript’s implementations around discriminated unions and the type inference they offer.
- Use them for simpler cases like filtering out
null
andundefined
.
- Read the implementation of type guards you use (but haven’t implemented) critically.
- Remember that after all, your application is still a JavaScript application.
Related documentation and links:
- TypeScript handbook entry on advanced types, section on type guards
- This article, as posted on Hacker News, and its comments