Define Custom Type Guard Functions in TypeScript

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 3 years ago

One aspect of control flow based type analysis is that the TypeScript compiler narrows the type of a variable within a type guard.

This lesson explores how you can define functions and type predicates to create your own type guards similar to the Array.isArray() method.

Take a look at this numbers array whose elements are numbers or nested arrays of numbers. Let's now assume we want to write a flatten function, a function that takes such arrays and returns a flat version of them.

We'll say function flatten, and we'll define a single parameter called array. That array will hold numbers or number arrays themselves. We'll also add a type annotation for our return type which will be a flat number array.

First, we're going to create a variable called flattened and initialize it with an empty array. Next, we're going to iterate over the array parameter using a for of loop. For each element, we're going to call the array.is array method to check whether this element is an array itself.

If it is, we're going to push all of its elements into the flattened array using the spread syntax. Otherwise, we simply add the element itself. Finally, we are going to return the flattened array from the function.

All right. Let's see whether our flatten function works as intended. I'm going to add a console.log statement and log the result of calling flatten on the numbers array. If we now compile the program and run the generated JavaScript file through Node, we get the expected output. Perfect.

Let's now take a look at the types in our program. We've added a type annotation that says that our array parameter is an array of numbers or a number array. This is why element is either a number or a number array. That makes sense so far.

The interesting part happens within the if block. Within here, element is typed to be a number array. It can no longer be just a plain number. This is because we've called the array.is array method. Let's see how that is defined.

We can see here that the return type of the is array method is arg is array of any. This is what's called a type predicate. It's basically a Boolean, so you have to return true or false from this function, but it gives an additional hint to the type system, saying that arg is an array of any.

Notice that arg here was specified as any, so it could have been anything. If the is array method returns true, we now know that arg is an array of something. This is how the type checker understands the effect of the is array method. It can now narrow the type of the element variable to number array.

Before the if statement, element was typed as number or number array. But within the if block, it's now typed to be just number array. Therefore, in the else block, element can only be number because if it had been an array, we would have entered the if block. This understanding of type guards is just another capability of control flow based type analysis.

Now our flatten function is overly specific in that it's restricted to the number type. Let's go ahead and make a generic. We're going to add a generic type T, and we're going to replace every occurrence of the number type by this T type.

As we can see, everything type checks again. Within our for loop, element is typed to be T or T array. Within the if block, we get the T array while within the else block, we simply get the plain T.

Notice that down here in the call to the flatten function the type argument number is used for the type parameter T. This shows that the TypeScript compiler was able to automatically infer all the required types here.

Let's finish up this lesson by writing our type guard function, for example, the is flat function that takes a mixed array and tells us whether or not it's flat. Just like before, this function can operate on any generic type T. It accepts a single parameter called array, and it's return type is the type predicate array is T array. Our array is considered flat if it doesn't contain an element that is an array itself.

We can quickly check that using the sum method to find on the array prototype and the is array method defined on array. We can now use the is flat function as a type guard and check whether or not our numbers array is flat.

Let's check the type of the numbers variable within this if block. Within the if block, TypeScript infers numbers to be a number array while outside, it is types as an array of numbers and nested number arrays. There you go, your own user-defined type guard function.

o-t-w
o-t-w
~ 3 years ago

I don't understand the syntax here:

function flatten(array: (number | number[])[]):number[] {
Marius Schulz
Marius Schulzinstructor
~ 3 years ago

@o-t-w: The array parameter is typed to be an array whose elements must be numbers or number arrays. You could also write the types like this:

function flatten(array: Array<number | Array<number>>): Array<number> {
  // ...
}
Arsenii Shelestiuk
Arsenii Shelestiuk
~ 2 years ago

How would you implement the opposite to isFlat function? Call it "containsArrays" for instance

Marius Schulz
Marius Schulzinstructor
~ 2 years ago

@Arseniy: You could implement an isArrayOfArrays function like this:

function isArrayOfArrays(array: unknown[]): array is unknown[][] {
  return array.every(element => Array.isArray(element));
}
Joël
Joël
~ 2 years ago

Hi @marius,

I am not sure about the aim of the keyword is , it's not really well documented inside TS type predicate doc, they spoke about runtime check ... could you explain the aime here ? Regards

Marius Schulz
Marius Schulzinstructor
~ 2 years ago

@Alexandre: A type guard lets TypeScript narrow the type of a variable. For example, if you have a variable x of type string | number, you can check its type using the typeof operator:

if (typeof x === "string") {
  // In here, `x` has type `string`
}

TypeScript has a built-in understanding for type guards using e.g. the typeof or instanceof operators. However, in some cases you might need a more complex check. That's when you'd use a custom type guard function.

A custom type guard must return a boolean, but its return type isn't simply boolean — it has the shape paramName is someType where paramName is the name of a parameter of that function and someType is an arbitrary TypeScript type. You can implement the function in any way you like, as long as it returns a boolean.

When you use your custom type guard function, TypeScript will then narrow the type of the variable that paramName in paramName is someType to someType. For example:

function isArrayOfArrays(array: unknown[]): array is unknown[][] {
  return array.every(element => Array.isArray(element));
}
Dean
Dean
~ 2 years ago

I would of liked a more involved usage and meaning of the "is" usage. Like, why not just do: Why the need for array is T[], rather than just T[]? I have the same lack of understanding for the "as" usage too..

function isFlat<T>(array: (T | T[])[]): T[] {
    console.log(!array.some(Array.isArray));
}
Jessica
Jessica
~ a year ago
function isFlat<T>(array: (T | T[]): array is T[] {
	return !array.some(Array.isArray);
}

Wouldn't this throw an error if array was T as opposed to T[]?

Marius Schulz
Marius Schulzinstructor
~ a year ago

@Jessica: Have a closer look at the exact function signature:

function isFlat<T>(array: (T | T[])[]): array is T[] {
    // ...
}

The array parameter is of type (T | T[])[], not (T | T[]). This means it's always an array.