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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With