A common pattern that I’ve repeated as a prior JS developer for checking the type of an object is something like:
if (obj.property) { // obj.property exists! }
Sadly, this is not possible in TypeScript.
The problem
This is kind of classic in TypeScript: an object type that we would like to differentiate for different cases. In the following example we have two types,
Fish
and Bird
:type Fish = { name: string, swim: () => void } type Bird = { name: string, fly: () => void } type Pet = Fish | Bird
We can easily print the name of the
Pet
, but if we want to use the methods swim
and fly
, it becomes more complicated.const printFishOrBird = (pet: Pet) => { // Error TS2339: Property 'swim' doesn't exist on type 'Pet' if (pet.swim) { console.log('Fish') } else { console.log('Bird') } }
The example above doesn’t work because
swim
is not defined.The solution: Type guards
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 (
Pet
vs a more specific Fish
).As we can see in the TypeScript docs, for the example above, we simply need to define a function whose return type is a type predicate:
function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined; }
The key here is the return type:
pet is Fish
. TypeScript understands that if we return true
, the argument is Fish
, and we can use this variable.Caveat
The type guards used in the previous example are fairly naive.
In the
Fish
example, we’re assuming that swim
is unique to Fish
.The type guard asserts that if
swim
exists on the given variable, then it’s a Fish
. But this might not be true.We could also have a
Dolphin
type and the assert would not be precise in all the cases.This might be a problem in more complex scenarios, so use type guards responsibly.