The problem

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?

Oranges only please

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:

1
2
3
4
5
6
7
8
9
10
// Let's pretend this function is a juice squeezer
const squeezeJuice = (fruits: Array<'orange'>) => 'orange-juice';

const basket = ['apple', 'orange', 'apple', 'orange'] as const;

// Get oranges from the basket
const oranges = basket.filter((fruit) => fruit === 'orange');

// Make some orange juice
const juice = squeezeJuice(oranges);

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:

1
2
3
const juice = squeezeJuice(oranges);
// Argument of type '("orange" | "apple")[]' is not assignable
// to parameter of type '"orange"[]'.

Why? basket.filter filtered 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.

1
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?

Refining an array

The built-in 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const refineArray = <A, B extends A>(
items: readonly A[],
refinement: (item: A) => item is B // type guard function
): B[] => {
const results: B[] = [];

for (const item of items) {
if (refinement(item)) {
results.push(item);
}
}

return results;

// Note that this is implementation equivalent to a filter with a typecast:
// return items.filter(refinement) as B[];
};

// After substituting with apples and oranges refineArray type would look roughly like this:
type RefineFruitBasket = (items: Array<'orange' | 'apple'>, refinement: (item: 'orange' | 'apple') => item is 'orange') => Array<'orange'>;

// Using it
const oranges = refineArray(basket, (fruit): fruit is 'orange' => fruit === 'orange');
const juice = squeezeJuice(oranges); // no errors!

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 true that means that type of x is a number. Please read TypeScript documentation for further details.

Our refineArray function seems to work, however, note the typeguard

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

One step back, two steps forward

Let’s try a different approach. Maybe a sibling of filter function, 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:

1
2
const oranges = basket.map((fruit) => fruit === 'orange' ? fruit : undefined)
// Array<'orange' | 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.

1
2
const oranges = basket.map((fruit) => fruit === 'orange' ? [fruit] : [])
// Array<Array<'orange'>>;

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:

1
2
3
const oranges = basket.map((fruit) => fruit === 'orange' ? [fruit] : []).flat();

const juice = squeezeJuice(oranges); // yay, no errors!

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:

1
2
3
const oranges = basket.flatMap((fruit) => fruit === 'orange' ? [fruit] : []);

const juice = squeezeJuice(oranges); // still type checks

Summary

You can use .flatMap for type safe filtering.

Pros:

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

Cons:

  • It’s a bit less readable than plain .filter.

Discussion

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.