2

From a type like this:

type Def = {
  m1(a1: A1, b1: B1): R1
  m2(a2: A2): R2
  m3(a3: A3, b3: B3, c3: C3): R3
}

I would like to compute a type like this:

type F =
  ((name: 'm1', a1: A1, b1: B1) => R1) &
  ((name: 'm2', a2: A2) => R2) &
  ((name: 'm2', a3: A3, b3: B3, c3: C3) => R3)

that is the overloaded function type for a dispatcher method that would be dynamically generated.

I have managed to add the name parameter, i.e.:

type H = {
  m1: (name: 'm1', a1: A1, b1: B1) => R1,
  m2: (name: 'm2', a2: A2) => R2,
  m3: (name: 'm3', a3: A3, b3: B3, c3: C3) => R3,
}

through simple object mapping and the Parameter and ReturnType helpers, but all my attempt at merging this into a single call signature have failed.

(see playground link)

pqnet
  • 6,070
  • 1
  • 30
  • 51
  • Does [this approach](https://tsplay.dev/WYKQEN) meet your needs? If so I'll write up an answer explaining; if not, what am I missing? – jcalz Aug 23 '23 at 14:37
  • 1
    @jcalz yes sorry I tried to write an abstract example. The method does work, it is basically uses contravariance of function arguments to transform an `|` into an `&` right? I think I tried a similar intersection trick before using some other answer in StackOverflow but it just computed either `any` or `never`. – pqnet Aug 23 '23 at 14:44
  • Yes, that's it exactly. – jcalz Aug 23 '23 at 14:53

1 Answers1

3

Here's one possible approach:

type IntersectMethods<T> = {
    [K in keyof T]: T[K] extends (...args: infer A) => infer R ?
    (x: ((name: K, ...args: A) => R)) => void :
    never
} extends Record<string, (x: infer I) => void> ? I : never

This uses conditional type inference and infers intersections instead of unions because the type to infer has been moved to a contravariant position (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more about variance). It's essentially the same technique as in Transform union type to intersection type with the wrinkle that we are finding only the methods and prepending a name parameter to each one.

Let's test it out:

type Def = {
  m1(a1: A1, b1: B1): R1
  m2(a2: A2): R2
  m3(a3: A3, b3: B3, c3: C3): R3
}

type F = IntersectMethods<Def>
/* type F = 
     ((name: "m1", a1: A1, b1: B1) => R1) & 
     ((name: "m2", a2: A2) => R2) & 
     ((name: "m3", a3: A3, b3: B3, c3: C3) => R3)
*/

Looks good!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360