Search code examples
typescriptgenericsoverloadingspread-syntax

is there a typescript generic that handles T | T[] | "*"


I'm new to generics in typescript, it's confusing

is there a way to make the spread operator ... work in the snippet below

the issue

the line [ key: U, ...rest: Reg[U] ], doesn't work as I expect

the question

what am I doing wrong ?

I tried to make this work but couldn't

possible solutions i didnt try yet

function overloading


type Registery = {
  "item.1": [name: string]
  "item.2": [id: number, ready: "Y" | "N"]
  "item.3": [ok: boolean]
}
type ReK = keyof Registery
const send = <
  T extends ReK | ReK[] | "*" | never = never
>(
  key: T,
  callback: (
    ...args: 
        T extends Array<infer U>
          ? (
            U extends ReK 
              ? [ key: U, ...rest: Registery[U] ]
              : never 
          )
          : T extends string 
            ? (
              T extends ReK 
                ? Registery[T]
                : T extends "*" 
                  ? Registery[ReK]
                  : never 
            )
            : never 
  ) => any
) => { /**  */ }
send("item.2", (...args) => {
  const [ 
    arg1,
    //^?const arg1: number 
    arg2,
    //^?const arg2: "Y" | "N" 
  ] = args 
})

send(["item.1", "item.2"], (key, ...args) => {
  //                                ^?
  const k = key 
  //    ^?const k: "item.1" | "item.2"

  if (key == "item.1") {
    const [ 
      arg1,
      //^?const arg1: string | number 
      arg2, 
      //^?const arg1: string | number 
    ] = args
  }
  if (key == "item.2") {
    const [ 
      arg1,
      //^?const arg1: string | number 
      arg2,
      //^?const arg2: string | number 
    ] = args 
  }
})

here's a link to ts playground https://tsplay.dev/mxEQ7W


Solution

  • Your types are fine. The problem is that TypeScript doesn't support destructured discriminated unions when destructuring with a rest property. There is an open feature request at microsoft/TypeScript#46680 to support this, but until and unless that's implemented you have to work around it.


    What you've done is equivalent to

    type ParamUnion =
      [key: "item.1", name: string] |
      [key: "item.2", id: number, ready: "Y" | "N"];
      
    const f: (...args: ParamUnion) => any =
      (key, ...rest) => {
        if (key === "item.1") {
          rest[0].toUpperCase(); // error!
        } else {
          rest[0].toFixed(); // error!
        }
      };
    

    where ParamUnion is a discriminated union of tuple types where the first element is the discriminant.

    That's the exact problem you have, with no mention of generics or T | T[] | "*" or [ key: U, ...rest: Reg[U] ]. That stuff is interesting but is essentially a red herring.


    One way to work around this is not to use a rest element when doing your destructuring assignment of the ParamUnion type into parameters:

    const g: (...args: ParamUnion) => any =
      (key, nameOrId, ready?) => {
        if (key === "item.1") {
          nameOrId.toUpperCase(); // okay
        } else {
          nameOrId.toFixed(); // okay
        }
      };
    

    Another way to work around this is not to destructure the rest element at all, and instead just use a rest parameter:

    const h: (...args: ParamUnion) => any =
      (...args) => {
        if (args[0] === "item.1") {
          args[1].toUpperCase(); // okay
        } else {
          args[1].toFixed(); // okay
        }
      };
    

    Both of these behave as desired.

    Playground link to code