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.
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.