UPDATE: TypeScript 4.0 will feature variadic tuple types, which will allow more flexible built-in tuple manipulation. Push<T, V>
will be simply implemented as [...T, V]
.
Pre-TS4.0 answer:
Ugh, why?! Ahem, I mean, I can maybe do this, but the more type-juggling involved, the less I'd recommend doing this for anything important. There's a library called ts-toolbelt which is close to being "officially supported" by TypeScript (although it doesn't work in the Playground, at least not yet, so I'm not about to make an Stack Overflow answer that requires it) where you can probably build something that works.
How I'd approach this is to convert tuples with optional elements to unions of tuples without them. Unfortunately, I'm missing a built-in way to take a number type like 6
and get a tuple of that length. So I'm making a hardcoded list of tuples-of-various-lengths that I can map over. You can extend it if you need this to work on longer tuples. Here we go:
type Cons<H, T extends readonly any[]> =
((head: H, ...tail: T) => void) extends ((...cons: infer R) => void) ? R : never;
That's just the standard Cons<1, [2,3,4]>
becomes [1,2,3,4]
.
type Tup = [[], [0], [0, 0], [0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]; // make as long as you need
That's the big list of tuples. So Tup[4]
is [0,0,0,0]
, etc.
type TruncateTuple<T extends readonly any[], N extends number> = Extract<
Tup[N] extends infer R ? { [K in keyof R]: K extends keyof T ? T[K] : never }
: never, readonly any[]>;
That type takes a tuple T
and a length N
and truncates T
to length N
. So TruncateTuple<[1,2,3,4], 2>
should be [1,2]
. It works by getting a tuple of length N
from Tup
, and maps over it with properties from T
.
type OptTupleToUnion<T extends readonly any[]> =
TruncateTuple<Required<T>, T['length']>;
Here's the main event... OptTupleToUnion
takes a tuple T
and produces a union from it of non-optional tuples. It works by truncating Required<T>
(that is, T
with optional elements turned into required ones) to length T['length']
which is the union of possible lengths of T
. So OptTupleToUnion<[1,2,3?,4?]>
should become [1,2] | [1,2,3] | [1,2,3,4]
.
Then I'll rename my old Push
out of the way to _Push
:
type _Push<T extends readonly any[], V>
= T extends any ? Cons<void, T> extends infer U ?
{ [K in keyof U]: K extends keyof T ? T[K] : V } : never : never;
and make Push<T, V>
act on OptTupleToUnion<T>
instead of T
:
type Push<T extends readonly any[], V> = T extends any ?
_Push<OptTupleToUnion<T>, V> : never;
(with the same T extends any ? ..T.. : never
to make sure unions get distributed)
Let's see if it works:
type A = Push<[1, 2, 3], 4>; // [1, 2, 3, 4]
type B = Push<[1, 2, 3?], 4>; // [1, 2, 3, 4] | [1, 2, 4]
Yay, looks good. 😅 I might have to give up if you start asking for more functionality here... maybe someone else has more stamina!
Okay, hope that helps; good luck!
Link to code