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, where parameterName 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.