Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript filter tuple type by an arbitrary type

How would I go about generating a new tuple type by filtering a provided tuple type by an arbitrary type within the provided tuple?

Example (Playground):

type Journey = ["don't", 'stop', 'believing'];

type ExcludeFromTuple<T extends unknown[], E> = ????;

type DepressingJourney = ExcludeFromTuple<Journey, "don't">; // type should be ['stop', 'believing']

Note that the solution doesn't need to ensure that type E exists in type T before hand, it just need to remove it if it does.

Although the example is simple here, I have a more complicated use case where I want to be able to filter out by an arbitrary type defined by a consumer of the library I am writing.

Although TypeScript natively supports the exclude type, it only works on union types, and I have been unable to find an equivalent for tuples.

A type like ExcludeFromTuple would be extremely useful for generating other utility types.

type RemoveStringsFromTuple<T extends unknown[]> = ExcludeFromTuple<T, string>;
type RemoveNumbersFromTuple<T extends unknown[]> = ExcludeFromTuple<T, number>;
type RemoveNeversFromTuple<T extends unknown[]> = ExcludeFromTuple<T, never>;
type RemoveUndefinedsFromTuple<T extends unknown[]> = ExcludeFromTuple<T, undefined>;

I have a feeling the type would need to leverage a combination of TypeScript 2.8’s conditional types, TypeScript 3.1’s mapped types on tuples, and some type of recursive type magic, but I haven't been able figure it out nor find anyone who has.

like image 681
Reed Hermes Avatar asked Jan 26 '23 14:01

Reed Hermes


1 Answers

Update for TS 4.1+:

With variadic tuple types introduced in TS 4.0, and recursive conditional types introduced in TS4.1, you can now write ExcludeFromTuple more simply as:

type ExcludeFromTuple<T extends readonly any[], E> =
    T extends [infer F, ...infer R] ? [F] extends [E] ? ExcludeFromTuple<R, E> :
    [F, ...ExcludeFromTuple<R, E>] : []

You can verify that this works as desired:

type DepressingJourney = ExcludeFromTuple<Journey, "don't">;
// type should be ['stop', 'believing']

type SlicedPi = ExcludeFromTuple<[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9], 1 | 9>
// type SlicedPi = [3, 4, 5, 2, 6, 5, 3, 5, 8, 7]

Playground link to code


Pre TS-4.1 answer:

Yuck, this is something that really needs recursive conditional types which are not supported in TypeScript yet. If you want to use them you do so at your own risk. Usually I'd rather write a type that should be recursive and then unroll it into a fixed depth. So instead of type F<X> = ...F<X>..., I write type F<X> = ...F0<X>...; type F0<X> = ...F1<X>...;.

To write this I'd want to use basic "list processing" types for tuples, namely Cons<H, T> to prepend a type H onto a tuple T; Head<T> to get the first element of a tuple T, and Tail<T> to get the tuple T with the first element removed. You can define those like this:

type Cons<H, T> = T extends readonly any[] ? ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never : never;
type Tail<T extends readonly any[]> = ((...t: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;
type Head<T extends readonly any[]> = T[0];

Then the recursive type would look something like this:

/* type ExcludeFromTupleRecursive<T extends readonly any[], E> = 
     T["length"] extends 0 ? [] : 
     ExcludeFromTupleRecursive<Tail<T>, E> extends infer X ? 
     Head<T> extends E ? X : Cons<Head<T>, X> : never; */

The idea is: take the tail of the tuple T and perform ExcludeFromTupleRecursive on it. That's the recursion. Then, to the result, you should prepend the head of the tuple if and only if it doesn't match E.

But that's illegally circular, so I unroll it like this:

type ExcludeFromTuple<T extends readonly any[], E> = T["length"] extends 0 ? [] : X0<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X0<T extends readonly any[], E> = T["length"] extends 0 ? [] : X1<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X1<T extends readonly any[], E> = T["length"] extends 0 ? [] : X2<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X2<T extends readonly any[], E> = T["length"] extends 0 ? [] : X3<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X3<T extends readonly any[], E> = T["length"] extends 0 ? [] : X4<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X4<T extends readonly any[], E> = T["length"] extends 0 ? [] : X5<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X5<T extends readonly any[], E> = T["length"] extends 0 ? [] : X6<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X6<T extends readonly any[], E> = T["length"] extends 0 ? [] : X7<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X7<T extends readonly any[], E> = T["length"] extends 0 ? [] : X8<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X8<T extends readonly any[], E> = T["length"] extends 0 ? [] : X9<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type X9<T extends readonly any[], E> = T["length"] extends 0 ? [] : XA<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XA<T extends readonly any[], E> = T["length"] extends 0 ? [] : XB<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XB<T extends readonly any[], E> = T["length"] extends 0 ? [] : XC<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XC<T extends readonly any[], E> = T["length"] extends 0 ? [] : XD<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XD<T extends readonly any[], E> = T["length"] extends 0 ? [] : XE<Tail<T>, E> extends infer X ? Head<T> extends E ? X : Cons<Head<T>, X> : never;
type XE<T extends readonly any[], E> = T; // bail out

Having fun yet? Let's see if it works:

type DepressingJourney = ExcludeFromTuple<Journey, "don't">;
// type should be ['stop', 'believing']

type SlicedPi = ExcludeFromTuple<[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9], 1 | 9>
// type SlicedPi = [3, 4, 5, 2, 6, 5, 3, 5, 8, 7]

Looks good to me.

Link to code

like image 159
jcalz Avatar answered Jan 28 '23 03:01

jcalz