In our code base, we have a utility function isArray
to check whether an input is an Array.
function isArray<T>(obj: any): boolean {
return Array.isArray(obj);
}
Its goal is to return true if given input is an array, false if not. Other code references this utility function as:
function doStuff(thing: number | number[]): void {
if (isArray(thing)) {
thing.forEach(item => {
console.log("A thing:", item);
})
} else {
console.log("A thing:", thing);
}
}
You would see that there's error message on the line thing.forEach()
:
[ts] Property 'forEach' does not exist on type number.
But why does it think thing
is a number since it's within the isArray
condition block? Wouldn't only when that condition pass, it comes to the block inside?
It's because TypeScript at compile time does NOT know what type isArray
returns. It is possible that users have wrong implementation of isArray
or to an extreme, just return true
all the time. When you try to call forEach
on a non array type, it'd get an exception. TypeScript guards this kind of error by having a type guard.
A type guard is some expression that performs a runtime check that guarantees the type in some scope. To define a type guard, we simply need to define a function whose return type is a type predicate:
function isArray<T>(obj: any): obj is T[] {
return Array.isArray(obj);
}
In this case, obj is T[]
is our type predicate. When isArray
called, TypeScript knows that variable to that specific type.
A predicate takes the form
parameterName is Type
, whereparameterName
must be the name of a parameter from the current function signature.
Now if you run that doStuff
function again, TypeScript knows that thing
in the if branch is definitely an array; it also knows that in the else branch, it's not an array, so it must be a number.