Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript spread operator for type [duplicate]

I'm trying to define a type which gets a function type as a generic parameter and returns a function type which is the same as the input function type except it has one more argument at the end:

  type AugmentParam<F extends (...args: any[]) => any, ExtraParam> = F extends (
    ...args: infer Args
  ) => infer R
    ? (
        ...args: [
          ...Args,
          ExtraParam
        ]
      ) => R
    : never

An example of the usage:

type F = (x: number) => boolean
type F2 = AugmentParam<F, string> // (x: number, arg2: string) => boolean

...Args seems not working, however if I change it to something like this it works:

  type AugmentParam<F extends (...args: any[]) => any, ExtraParam> = F extends (
    ...args: infer Args
  ) => infer R
    ? (
        ...args: [
          Args[0],
          Args[1] /* Spread doesn't work here, so it doesn't work for arbitrary number of arguments :( */,
          ExtraParam
        ]
      ) => R
    : never

But it only works for a specific number of arguments and I need to define one such type for each n-ary function.

like image 202
Alireza Mirian Avatar asked Nov 05 '19 14:11

Alireza Mirian


1 Answers

TypeScript can fairly easily represent prepending a type onto a tuple type, called Cons<H, T> like this:

type Cons<H, T extends readonly any[]> =
    ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never

type ConsTest = Cons<1, [2, 3, 4]>;
// type ConsTest = [1, 2, 3, 4]

You can use this definition along with conditional mapped tuple types to produce a Push<T, V> to append a type onto the end of a tuple:

type Push<T extends readonly any[], V> = Cons<any, T> extends infer A ?
    { [K in keyof A]: K extends keyof T ? T[K] : V } : never

type PushTest = Push<[1, 2, 3], 4>;
// type PushTest = [1, 2, 3, 4]

but this definition of Push is fragile. If the T tuple has optional elements, or if it comes from the parameter list of a function, you'll notice that the compiler "shifts" the optional markers and parameter names one element to the right:

type Hmm = (...args: Push<Parameters<(optStrParam?: string) => void>, number>) => void;
// type Hmm = (h: string | undefined, optStrParam?: number) => void

Parameter names are not really part of the type so while it's annoying it's not affecting the actual type. Appending an argument after an optional one is... strange, so I'm not sure what the right behavior there. Not sure if these are dealbreakers for you, but be warned.

Anyway your AugmentParam would look like:

type AugmentParam<F extends (...args: any[]) => any, ExtraParam> =
    (...args: Extract<Push<Parameters<F>, ExtraParam>, readonly any[]>)
        => ReturnType<F>

and it works (with the earlier caveats):

type F = (x: number) => boolean

type F2 = AugmentParam<F, string>
// type F2 = (h: number, x: string) => boolean

type F3 = AugmentParam<F2, boolean>
// type F3 = (h: number, h: string, x: boolean) => boolean

Okay, hope that helps. Good luck!

Link to code

like image 114
jcalz Avatar answered Oct 07 '22 05:10

jcalz