Typescript: Remove entries from tuple type
Asked Answered
N

4

27

not sure if this is possible, but I would like to be able to define a type that converts tuples like: [number, string, undefined, number] to [number, string, number] (ie filter out undefined).

I thought about something like this:

type FilterUndefined<T extends any[]> = {
    [i in keyof T]: T[i] extends undefined ? /* nothing? */ : T[i];
}

Sadly I am am pretty sure that there is no way to achieve this.

Northing answered 9/2, 2019 at 14:55 Comment(3)
Tuples are immutable. So you would have to replace one with another. #16297143Buine
t = (1, "a", {"key":"value"}) l = list(t) l.remove("a") t2 = tuple(l) print("t", t, "\nt2",t2) yields t (1, 'a', {'key': 'value'}) t2 (1, {'key': 'value'})Buine
@Buine I think you are writing about Python and values, while the question is about Typescript and typesRabin
D
27

TS 4.1

Filter operations on tuples are now officially possible:

type FilterUndefined<T extends unknown[]> = T extends [] ? [] :
    T extends [infer H, ...infer R] ?
    H extends undefined ? FilterUndefined<R> : [H, ...FilterUndefined<R>] : T
Let's do some tests to check, that it is working as intended:
type T1 = FilterUndefined<[number, string, undefined, number]> 
// [number, string, number]
type T2 = FilterUndefined<[1, undefined, 2]> // [1, 2]
type T3 = FilterUndefined<[undefined, 2]> // [2]
type T4 = FilterUndefined<[2, undefined]> // [2]
type T5 = FilterUndefined<[undefined, undefined, 2]> // [2]
type T6 = FilterUndefined<[undefined]> // []
type T7 = FilterUndefined<[]> // []

More infos


Playground

Dilative answered 23/9, 2020 at 19:1 Comment(5)
Nice job! Why do you use "T extends any[]" instead of "T extends unknown[]"?Daglock
It was shorter :-). any used as generic constraint doesn't really matter here compared to unknown (top type) - you won't lose type safety. The important part is, that T is limited to array types (readonly any[] would be even a tad better). Though your hint is still useful: If ESLint/linters complain about any usage, it can be safely replaced by unknown for FilterUndefined.Dilative
PS: updated answer to use unknown. Thanks again @FlavioVilante for the hint.Dilative
Is there a way to preserve the tuple labels after filtering?Antipyretic
IIRC, not possible, @NandinBorjigin. While there are cases where named tuple members are preserved (such as when concatenating tuple types), this is not the case here. The recursive conditional type here infers the members and "reconstructs" the tuple, thus losing the information about names.Cassondracassoulet
T
29

Got it! But it need a lot of recursive magic:

type PrependTuple<A, T extends Array<any>> =
  A extends undefined ? T : 
  (((a: A, ...b: T) => void) extends (...a: infer I) => void ? I : [])

type RemoveFirstFromTuple<T extends any[]> = 
  T['length'] extends 0 ? undefined :
  (((...b: T) => void) extends (a, ...b: infer I) => void ? I : [])

type FirstFromTuple<T extends any[]> =
  T['length'] extends 0 ? undefined : T[0]

type NumberToTuple<N extends number, L extends Array<any> = []> = {
  true: L;
  false: NumberToTuple<N, PrependTuple<1, L>>;
}[L['length'] extends N ? "true" : "false"];

type Decrease<I extends number> = RemoveFirstFromTuple<NumberToTuple<I>>['length']
type H = Decrease<4>

type Iter<N extends number, Items extends any[], L extends Array<any> = []> = {
  true: L;
  false: Iter<FirstFromTuple<Items> extends undefined ? Decrease<N> : N, RemoveFirstFromTuple<Items>, PrependTuple<FirstFromTuple<Items>, L>>;
}[L["length"] extends N ? "true" : "false"];

type FilterUndefined<T extends any[]> = Iter<T['length'], T>
type I = [number, string, undefined, number];
type R = FilterUndefined<I>


Playground

How it works:

PrependToTuple is util that takes item A and list T and add it on first place when A is not undefined. PrependToTuple<undefined, []> => [], PrependToTuple<undefined, [number]> => [number]

RemoveFirstFromTuple works pretty mach i the same way

NumberToTuple is recursively check if length of final Tuple is N, if not he add 1 to recursive call. This util is needed to create Decrease util.

And the most important z Iter works like recursive loop, when length of final tuple is N (size of Input) its return Output, but PrependToTuple is not increasing length when we try do add undefined, so when Iter<FirstFromTuple<Items> extends undefined we have to decrease N.

Tritheism answered 9/2, 2019 at 15:43 Comment(5)
Good job! If I recall correctly NumberToTuple is a recursive type (from jcalz?) and has a max number of 23 after that it will give weird results. However tuples are rarely that long.Rabin
Yes for long tuples will throw error 'false' is referenced directly or indirectly in its own type annotation.Tritheism
Where did you take all these types from? If it's not a secret :) I doubt you came up with them yourself?Rabin
No it's not secret. Trick with recursive tuple I got from react days 2018 in Verona. From lighting talk made by @MattiaManzati. I using this tricks in some extraordinary TS problems. Types like Iter was inspired by itTritheism
Yeah, this black magic does work. But sadly I can't use it to achieve the types I want to have, with such generic types the compiler just gives up and puts implicit anys everywhere interesting,Argillaceous
D
27

TS 4.1

Filter operations on tuples are now officially possible:

type FilterUndefined<T extends unknown[]> = T extends [] ? [] :
    T extends [infer H, ...infer R] ?
    H extends undefined ? FilterUndefined<R> : [H, ...FilterUndefined<R>] : T
Let's do some tests to check, that it is working as intended:
type T1 = FilterUndefined<[number, string, undefined, number]> 
// [number, string, number]
type T2 = FilterUndefined<[1, undefined, 2]> // [1, 2]
type T3 = FilterUndefined<[undefined, 2]> // [2]
type T4 = FilterUndefined<[2, undefined]> // [2]
type T5 = FilterUndefined<[undefined, undefined, 2]> // [2]
type T6 = FilterUndefined<[undefined]> // []
type T7 = FilterUndefined<[]> // []

More infos


Playground

Dilative answered 23/9, 2020 at 19:1 Comment(5)
Nice job! Why do you use "T extends any[]" instead of "T extends unknown[]"?Daglock
It was shorter :-). any used as generic constraint doesn't really matter here compared to unknown (top type) - you won't lose type safety. The important part is, that T is limited to array types (readonly any[] would be even a tad better). Though your hint is still useful: If ESLint/linters complain about any usage, it can be safely replaced by unknown for FilterUndefined.Dilative
PS: updated answer to use unknown. Thanks again @FlavioVilante for the hint.Dilative
Is there a way to preserve the tuple labels after filtering?Antipyretic
IIRC, not possible, @NandinBorjigin. While there are cases where named tuple members are preserved (such as when concatenating tuple types), this is not the case here. The recursive conditional type here infers the members and "reconstructs" the tuple, thus losing the information about names.Cassondracassoulet
C
9

Supplementary answer:

An extension of the approach in ford04's answer allows us to create a "splicer" utility type that can remove values at arbitrary indices (in case someone finds this in search of a solution to type-safe splice).

This involves creating a utility type that will generate the tuple with undefined from a given tuple and index:

type UndefIndex<T extends any[], I extends number> = {
    [ P in keyof T ] : P extends Exclude<keyof T, keyof any[]> ? P extends `${I}` ? undefined : T[P] : T[P]
}

Then this is just a matter of composing the UndefIndex and FilterUndefined types:

type FilterUndefined<T extends any[]> = T extends [] ? [] :
    T extends [infer H, ...infer R] ?
    H extends undefined ? FilterUndefined<R> : [H, ...FilterUndefined<R>] : T;

type SpliceTuple<T extends any[], I extends number> = FilterUndefined<UndefIndex<T, I>>;

type a = SpliceTuple<[1,2,3], 0>; //[2,3]
type b = SpliceTuple<[1,2,3], 1>; //[1,3]
type c = SpliceTuple<[1,2,3], 2>; //[1,2]
type d = SpliceTuple<[1,2,3], 3>; //[1,2,3]

Playground

Cassondracassoulet answered 10/2, 2021 at 23:40 Comment(0)
H
0

// remove from tuple another tuple bu recursive

export type RemoveFromTuple<T extends unknown[], U extends unknown[]> = T extends [infer A, ...infer B] ? ( A extends U[number] ? RemoveFromTuple<B, U> : [A, ...RemoveFromTuple<B, U>]) : [];
Hangbird answered 22/8, 2023 at 11:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.