26

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.

Jan van Brügge
  • 742
  • 8
  • 13
  • Tuples are immutable. So you would have to replace one with another. https://stackoverflow.com/questions/16296643/convert-tuple-to-list-and-back – JGFMK Feb 09 '19 at 15:28
  • 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'}) – JGFMK Feb 09 '19 at 15:36
  • 2
    @JGFMK I think you are writing about Python and values, while the question is about Typescript and *types* – Nurbol Alpysbayev Feb 09 '19 at 15:56

4 Answers4

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.

Przemyslaw Jan Beigert
  • 2,351
  • 3
  • 14
  • 19
  • Good job! If I recall correctly `NumberToTuple` is a recursive type (from jcalz?) and has a max number of [23](https://stackoverflow.com/a/54246826/9259778) after that it will give weird results. However tuples are rarely that long. – Nurbol Alpysbayev Feb 09 '19 at 15:54
  • Yes for long tuples will throw error `'false' is referenced directly or indirectly in its own type annotation.` – Przemyslaw Jan Beigert Feb 09 '19 at 15:58
  • Where did you take all these types from? If it's not a secret :) I doubt you came up with them yourself? – Nurbol Alpysbayev Feb 09 '19 at 15:59
  • 2
    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 it – Przemyslaw Jan Beigert Feb 09 '19 at 16:05
  • 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, – Jan van Brügge Feb 17 '19 at 11:46
25

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

ford04
  • 66,267
  • 20
  • 199
  • 171
  • 1
    Nice job! Why do you use "T extends any[]" instead of "T extends unknown[]"? – Flavio Vilante Oct 30 '20 at 11:36
  • 1
    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`. – ford04 Oct 30 '20 at 11:53
  • 1
    PS: updated answer to use `unknown`. Thanks again @FlavioVilante for the hint. – ford04 Mar 02 '21 at 10:57
  • Is there a way to preserve the tuple labels after filtering? – Nandin Borjigin Jul 13 '22 at 12:02
  • 1
    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 `infer`s the members and "reconstructs" the tuple, thus losing the information about names. – Oleg Valter is with Ukraine Jul 13 '22 at 14:10
8

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

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>]) : [];
jon
  • 1,494
  • 3
  • 16
  • 29