1

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

4
  • What exactly is the point of this code? There's a lot of stuff going on there with that conditional type and it's formatted in a confusing way (and the word Registery with that second e is distracting). Commented Apr 20, 2024 at 3:19
  • 1
    I think, if I understand your code, that there's no problem at all with any of the code you're identifying as a problem (e.g., "a typescript generic that handles T | T[] | "*" or "[key: U, ...rest: Reg[U]]"`. Instead it's the lack of microsoft/TypeScript#46680 that's the problem. If you give up on destructuring the union of tuples like this playground link shows then it works. It's ugly, but it works. Does that fully address the question? If so I'll write up an answer explaining; if not, what am I missing? Commented Apr 20, 2024 at 3:31
  • you did bring useful information, I thought it's possible. so thanks so much it was the answer I'm looking for. sorry about the extra "e" Commented Apr 20, 2024 at 13:17
  • I’ll write up an answer when I get a chance. Commented Apr 20, 2024 at 13:40

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.