beraliv

Filter tuple type in TypeScript

Example of FilterOut use
1type FilterOut<T extends any[], Filter> = any; // implementation
2
3type cases = [
4 Expect<Equal<FilterOut<[1, never, "a"], never>, [1, "a"]>>,
5 Expect<Equal<FilterOut<[1, never, "a"], "a">, [1]>>
6];

Today we discuss TupleFilter

Let's try it out 🚀

Delegate a subtask

Let's start the solution from iteration over the tuple using recursive conditional types.

Iteration over the tuple
1type FilterOut<T extends any[], F> = T extends [infer Head, ...infer Tail]
2 ? [Head, ...FilterOut<Tail, F>]
3 : [];

This way we don't really filter anything. Let's delegate the filtering part to FilterElement.

Delegate filtering to FilterElement
1type FilterElement<Element, F> = [Element];
2
3type FilterOut<T extends any[], F> = T extends [infer Head, ...infer Tail]
4 ? [...FilterElement<Head, F>, ...FilterOut<Tail, F>]
5 : [];

Let's save the progress in Playground – https://tsplay.dev/Wkjx2m

As you see, this way we don't really filter anything yet as we don't use a filter F in FilterElement

Filter the current element

To be able to filter the element, let's use a filter F:

Incorrect way to filter the element
1type FilterElement<Element, F> = Element extends F ? [] : [Element];
2
3type FilterOut<T extends any[], F> = T extends [infer Head, ...infer Tail]
4 ? [...FilterElement<Head, F>, ...FilterOut<Tail, F>]
5 : [];

There's a thought to write it using this conditional type Element extends F but that's incorrect

To be able to test incorrect behaviour, let's have a look at the current implementation in Playground – https://tsplay.dev/W4pLeW

We will see broken cases, let's focus on the first one.

The first broken case with never
1type BrokenFilterOut = FilterOut<[never], never>; // never
2type BrokenFilterElement = FilterElement<never, never>; // never
3type Test2 = [...never]; // never

It happens because we used not just usual conditional type but instead distributed conditional type like T extends unknown or T extends never:

Examples of distributed conditional types
1type DistributedConditionalTypeWithNever<T> = T extends never ? never : [T];
2type DistributedConditionalTypeWithUnknown<T> = T extends unknown ? [T] : never;
3
4type Test1 = DistributedConditionalTypeWithNever<never>; // never
5type Test2 = DistributedConditionalTypeWithUnknown<never>; // never
6type Test3 = DistributedConditionalTypeWithNever<1>; // [1]
7type Test4 = DistributedConditionalTypeWithUnknown<1>; // [1]

For never element with a filter never we get never which is later put into spread which leads to never

To be able to filter it correctly, let's try to check the equality of the types with the expression [T] extends [U]:

Correct way to filter the element
1type FilterElement<Element, F> = [Element] extends [F] ? [] : [Element];
2
3type FilterOut<T extends any[], F> = T extends [infer Head, ...infer Tail]
4 ? [...FilterElement<Head, F>, ...FilterOut<Tail, F>]
5 : [];

First of all it's working for never as [never] extends [never] goes to "then" branch. See:

Identify if we have never or not
1type IsNever<T> = [T] extends [never] ? true : false;
2
3type Check1 = IsNever<never>; // true
4type Check2 = IsNever<1>; // false
5type Check3 = IsNever<false>; // false
6type Check4 = IsNever<"1">; // false

Second, it's working fine if the filter is a union type like 'a' | 'b':

Checking union type filter
1type HasAOrB<T> = [T] extends ["a" | "b"] ? true : false;
2
3type Check1 = HasAOrB<never>; // true
4type Check2 = HasAOrB<"a">; // true
5type Check3 = HasAOrB<"b">; // true
6type Check4 = HasAOrB<"a" | "b">; // true
7type Check5 = HasAOrB<undefined>; // false
8type Check6 = HasAOrB<null>; // false
9type Check7 = HasAOrB<"c">; // false

Solution

So the final solution looks like this

Solution
1type FilterElement<Element, F> = [Element] extends [F] ? [] : [Element];
2
3type FilterOut<T extends any[], F> = T extends [infer Head, ...infer Tail]
4 ? [...FilterElement<Head, F>, ...FilterOut<Tail, F>]
5 : [];

Let's sum up the whole solution:

  1. We iterate over the tuple using conditional type – T extends [infer Head, ...infer Tail]
  2. We delegate filtering the current element to the type FilterElement
  3. We check if the current element is included in the filter with the conditional type [Element] extends [F]
  4. If element is filtered, we return empty tuple and then it's not added into the result

To be able to check it with tests, please have a look at the Playground – https://tsplay.dev/Wy58dW

Thank you for your time and have a wonderful evening 🌇

typescript

Comments

Alexey Berezin profile image

Written by Alexey Berezin who loves London 🏴󠁧󠁢󠁥󠁮󠁧󠁿, players ⏯ and TypeScript 🦺 Follow me on Twitter