Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Push type to the end of the tuple

I can add element to the begining of the tuple, or remove it from there

type ShiftTuple<T extends any[]> = ((...t: T) => void) extends ((x: infer X, ...r: infer R) => void) ? R : never;
type UnshiftTuple<X, T extends any[]> = ((x: X, ...t: T) => void) extends ((...r: infer R) => void) ? R : never;

But I have difficulties with doing the same things with the last element instead of the first.

Even if I write the function

function f<X, Z extends any[]>(x: X, ...args: Z) {
  return [...args, x]
}

typescript says it returns any[].

Is there a way to push new type into the end of the tuple like (insupported)

export type PushTuple<T extends any[], X> = [...T, X];
like image 356
Qwertiy Avatar asked Oct 24 '19 17:10

Qwertiy


1 Answers

UPDATE FOR TS4.0+

TypeScript 4.0 introduced variadic tuple types, which supports Pushing to the end of a tuple in a much more straightforward way... like this:

type Push<T extends readonly any[], V> = [...T, V];

OLD ANSWER FOR TS3.9-:

I usually define Push in terms of what you're calling Unshift (but which I call Cons), like this:

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;

The way Push<T, V> works is to make a tuple one element longer than T (which it does with Cons) and then it maps over it. It fills the initial elements of the output with the corresponding elements from T, and then anything left over it fills in V. The only element index in a mapped tuple of length n+1 that is not an index of a tuple of length n is the index n itself, so that means the last element of the new tuple is V. (I made sure to distribute over T in case it's a union.)

Note that this only works for non-edge cases... don't expect readonly, optional, and rest tuple elements or non-tuple arrays to behave well; you'd have to work around those restrictions yourself if you want to define it.


RESUME REGULAR ANSWER:

So then you can define f() like this:

function f<X, Z extends any[]>(x: X, ...args: Z): Push<Z, X>;
function f(x: any, ...args: any[]): any[] {
    return [...args, x]
}

(note that I use an overload to free myself from worrying about the compiler trying and failing to understand that the implementation complies with Push<Z, X>:

And it works as you expect:

const s = f(4, "hey", false);
// const s: [string, boolean, number]
console.log(s); // ["hey", false, 4]

Okay, hope that helps; good luck!

Link to code

like image 108
jcalz Avatar answered Sep 18 '22 12:09

jcalz