Push type to the end of the tuple with skipping optional
Asked Answered
A

1

1

I've found out how to push type to the end of the tuple:

type Cons<H, T extends readonly any[]> =
    ((head: H, ...tail: T) => void) extends ((...cons: infer R) => void) ? R : never;

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 in general it works:

type A = Push<[1, 2, 3], 4>;  // [1, 2, 3, 4]
type B = Push<[1, 2, 3?], 4>; // [1, 2, 3 | undefined, 4?]

But I actually want it not to contain undefined if some parameters are optional:

A = [1, 2, 3, 4]
B = [1, 2, 3, 4] | [1, 2, 4] // How can I get this?
B = [1, 2, 3 | undefined, 4] | [1, 2, 4] // or this?

Demo


Actually I'm trying to do this:

function doSmth<F extends (...args: any) => number>(f: F, ...args: Push<Parameters<F>, string>) {
  var x: string = args.pop();
  return f(...args) + x;
}

function f1(x: number, y: number) {
  return x + y;
}

function f2(x: number, y?: number) {
  return x + (y || 0);
}

doSmth(f1, 1, 2, "A");
doSmth(f2, 1, 2, "B");
doSmth(f2, 1, "C"); // Problem here: should compile, but it doen't
doSmth(f2, 1, 2);   // And here: should not compile as last is not a string, but it does
doSmth(f2, 1, undefined, "D"); // Possible, but actually I do not care
Anthropomorphosis answered 25/10, 2019 at 14:44 Comment(0)
R
3

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

Routh answered 25/10, 2019 at 15:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.