How to properly type Array.filter in TypeScript
Published on Saturday, December 30, 2023
This is a quick tip on how to make sure your arrays are properly typed after performing a .filter
operation.
Say you have an array of potential numbers that looks something like this:
const maybeNumbers = [1, 2, '3', undefined, 4, '5']
The type of the maybeNumbers
array is (string | number | undefined)[]
. Now, you want to filter the array to only contain numbers, so you do a standard .filter
operation like so:
const maybeNumbers = [1, 2, '3', undefined, 4, '5']const numbers = maybeNumbers.filter((maybeNumber) => { return Number.isInteger(maybeNumber)})
This code definitely does what we want, and the non-integer values will be removed from the new numbers
, array. The problem, however, is that the numbers
array still has the type (string | number | undefined)[]
. This is because each item in the array has the union type string | number | undefined
, and TypeScript cannot infer that a given type in the union would be removed by the filter operation.
To help TypeScript out, we can use a type guard on the filter function to modify the type of each item:
const maybeNumbers = [1, 2, '3', undefined, 4, '5']const numbers = maybeNumbers.filter((maybeNumber): maybeNumber is number => { return Number.isInteger(maybeNumber)})
Because this function runs against every item in the array, the end result is that the type of the array is modified to only contain the types that pass the type guard. In this case, that means the type of numbers
is now number[]
.
Taking it further
That was a fairly simple example where you declare what the resulting type will be, but what if you want to filter out a specific type without knowing what the types will be ahead of time?
Let's invert our example above so that we keep everything except numbers:
const maybeNumbers = [1, 2, '3', undefined, 4, '5']const notNumbers = maybeNumbers.filter((maybeNumber) => { return !Number.isInteger(maybeNumber)})
Now, you might be tempted to use maybeNumber is string | undefined
as a type guard, but that becomes difficult to maintain if the types in the array can change. Instead, you can use a generic function to make the type guard more flexible.
const maybeNumbers = [1, 2, '3', undefined, 4, '5']const notNumbers = maybeNumbers.filter(<T>(maybeNumber: T | number): maybeNumber is T => { return !Number.isInteger(maybeNumber)})
In this type signature, we are inferring the type of maybeNumber
as T | number
. This means that any types that are not number
will be represented by T
. This allows us to use the type guard maybeNumber is T
to remove the number
type from the array while keeping the other types intact.
Thanks for reading, and happy coding. ✌️