Type predicates: Filtering arrays in Typescript
Typescript has made developing in javascript more predictable. It provides warnings about any unhandled types in our code, that might result in an exception at runtime. Most common example being null checks that we miss often.
Amazing as it maybe, typescript does have features that are a little harder to get around. Filtering arrays for a specific type is one of them.
Filtering elements of an array in javascript is uncomplicated . The build in function filter handles everything, with our only task being passing a function that validates the elements.
const names = ["John", "Ray", null ,"Nancy"]
// returns false if the value of name is null, giving us a list of strings
const filteredNames = names.filter((name) => !name)
Taking the same approach in typescript lands us with a red squiggly line.
Let us assume a simple function accepting an array of strings. Given a array that contain null
, we need to filter it before passing it to the function.
const printNames = (names: string[]) => {
console.log(names);
}
const names: (string | null)[]= ["John", "Ray", null ,"Nancy"]
const filteredNames = names.filter((name) => !name)
// TypeError: (string | null)[] is not assignable to string[]
printNames(filterednNames)
The workings of the code is absolutely correct, it does filter out all the null values. But, typescript still indicates an error.
Although true, we have filtered out the null values, there is no way for typescript to know that. The filter function accepts a validation function. It doesn’t understand its content nor, filter operations that are performed. Hovering over to check the type of filteredNames
in VS code, will give its type as (string | null)[]
.
Using Type predicate
Defining a function that returns a type predicate helps us get around the error.
const filteredNames = names.filter((name): name is string => !name)
The name is string
portion of the function is the type predicate. It takes the form of parameterName is Type
. where parameterName
must be the name of a parameter from the current function signature.
Functions that are declared as type predicate, must return a boolean. It provides the typescript compiler information.
When the return value is true
, TypeScript assumes that the return type is the one that's declared in the type predicate. This is true for custom types too.
type Circle = { radius: number}
type Square = { length: number}
type Shape = Circle | Square
const isCircle = (shape: Shape): shape is Circle => {
// Any condition we want for filtering
return 'radius' in shape;
};
const circles = shapes.filter(isCircle);
If this function returns true, TypeScript assumes that the provided argument is of type Circle
.