Type-safe filters in TypeScript
The built in
Array.filter method cannot be used to filter array items in a type safe way. To exclude some items on the type level we would need a typecast. Are there any better alternatives?
To demonstrate this, let’s say we have a basket of apples and oranges. We want to make some fresh orange juice. And of course all that using TypeScript.
Our first approach might look like this:
// Let's pretend this function is a juice squeezer
Note that this example uses type literals extensively to make it more concise.
Looks great, however, TypeScript will not allow to use
squeezeJuice like that. Instead, we will get a type error:
const juice = squeezeJuice(oranges);
apple values just fine, however this method always returns an array of the same type as its input. So even that apples are filtered on the value level, on the type level exactly the same array type is returned.
A little typecast can “solve” the situation.
const oranges = basket.filter((fruit) => fruit === 'orange') as Array<'orange'>;
However, typecasts are bad for your code. Eg. in this case, if we used invalid filtering function we wouldn’t known until the program runs. An apple would got through to the squeezer and break the device. No more fresh juice!
How can we improve this situation?
filter didn’t suit our needs, so let’s try to implement a type-safe filter ourselves.
Also, let’s use type guards because we want to feel smart today.
const refineArray = <A, B extends A>(
Wait, but what are type guards?
In short, type guard is a function that can assert if its input argument is of specific type. For example,
(x: unknown): x is number => typeof x === "number"is a super simple example of a type guard. It returns a boolean (
typeof x === "number"is true or false) and its return type signature containes extra type level information (
x is number) which can be used by the compiler after the function call. In this case if it returns
truethat means that type of
xis a number. Please read TypeScript documentation for further details.
refineArray function seems to work, however, note the typeguard
(fruit): fruit is 'orange' => fruit === 'orange'
is a bit explicit. It requires us to provide the exact type we would like the array to have, without any helpful type inference. So uncivilized.
refineArray is usable in this simple scenario, however it can get cumbersome to declare its type guard signature with complex or generic types. Also that’s another area for possible human error.
Let’s try a different approach. Maybe a sibling of
map could help us?
map maps returned array on a type level as well, seems promising. So what if instead of filtering we could map values that we don’t want to kind of a no value?
Our first approach could be to use
undefined as a no value:
const oranges = basket.map((fruit) => fruit === 'orange' ? fruit : undefined)
That leaves us with a bit simpler array but still a heterogeneous one since we’ve introduced a new type
undefined. We could filter this array and typecast the result but this is yet another step back.
So can we use
map without introducing any other types that input arguments?
What about using an empty array to represent no value? To stay consistent a valid result could return an array with a single item.
const oranges = basket.map((fruit) => fruit === 'orange' ? [fruit] : )
Almost there. Result is not exactly what we wanted but looks promising. Now we only need a function that would turn an
Array<Array<T>> into an
Array<T>. Meet flat:
const oranges = basket.map((fruit) => fruit === 'orange' ? [fruit] : ).flat();
It turns out, this use case of mapping each item into an array and joining results is so common that it has its own built-in method - flatMap. Let’s try it:
const oranges = basket.flatMap((fruit) => fruit === 'orange' ? [fruit] : );
You can use
.flatMap for type safe filtering.
- Works great with type inference - no need for typecasts or explicit typings.
- It’s built in, no need for custom code or libraries.
- No space for human error on type level as compared to
- It’s a bit less readable than plain
If TypeScript was able to infer type of type guard functions,
refineArray would be equally or even more user friendly than
flatMap for the sake of filtering. What is more, then the built-in
Array.filter method could use the same typings as we declared in
refineArray so that we would get a best of all worlds - a built-in, type safe, readable filtering function.
TL;DR: You can use
flatMap as a built-in type-safe method to filter heterogeneous arrays.