7

Consider the following overloaded function:

function foo(arg1: string, cb: (err: Error|null, res: string) => void): void
function foo(arg1: string, arg2: string, cb: (err: Error|null, res: string) => void): void

I want promisify to work with such functions. But the default implementation returns an invalid type.

As it returns

(arg1: string, arg2: string) => Promise<{}>

I'd expect it to return

{
   (arg1: string): Promise<string>;
   (arg1: string, arg2: string): Promise<string>;
}

Considering this, I'd like to fix the typings. I managed to override that specific prototype using the following :

export type Callback<T> = (err: Error | null, reply: T) => void;
export type Promisify<T> =

    T extends {
        (arg1: infer T1, cb?:  Callback<infer U>): void;
        (arg1: infer P1, arg2: infer P2, cb?:  Callback<infer U2>): void;
    } ? {
        (arg1: T1): Promise<U>;
        (arg1: P1, arg2: P2): Promise<U2>;
    } :

    T extends (cb?:  Callback<infer U>) => void ? () => Promise<U> :
    T extends (arg1: infer T1, cb?:  Callback<infer P>) => void ? (arg1: T1) => Promise<P> :
    T extends (arg1: infer T1, arg2: infer T2, cb?:  Callback<infer U>) => void ? (arg1: T1, arg2: T2) => Promise<U> :
    T extends (arg1: infer T1, arg2: infer T2, arg3: infer T3, cb?:  Callback<infer U>) => void ? (arg1: T1, arg2: T2, arg3: T3) => Promise<U> :
    T extends (arg1: infer T1, arg2: infer T2, arg3: infer T3, arg4: infer T4, cb?:  Callback<infer U>) => void ? (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<U> :
    T;

But it requires me to specifically list all potential method overloads.

It there a way transform all methods overloads at once, similarily to how we can transform object properties?

Kamen Minkov
  • 3,324
  • 1
  • 14
  • 21
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432

1 Answers1

15

We can use the 3.0 feature of Tuples in rest parameters and spread expressions to get a union of the overload parameters but we need to add a case for each number of overloads the function has:

export type GetOverloadArgs<T> = 
    T extends { (...o: infer U) : void, (...o: infer U2) : void, (...o: infer U3) : void   } ? U | U2 | U3:
    T extends { (...o: infer U) : void, (...o: infer U2) : void  } ? U | U2 :
    T extends { (...o: infer U) : void } ? U : never

So for example for foo

type fooParams = GetOverloadArgs<typeof foo> 
// will be 
type fooParams = [string, (err: Error | null, res: string) => void] | [string, string, (err: Error | null, res: string) => void]

From this we can use a type similar to your Promisify to create a function for each parameter set in the union :

export type PromisifyOne<T extends any[]> =
    T extends [Callback<infer U>?] ? () => Promise<U> :
    T extends [infer T1, Callback<infer P>] ? (arg1: T1) => Promise<P> :
    T extends [infer T1, infer T2, Callback<infer U>?] ? (arg1: T1, arg2: T2) => Promise<U> :
    T extends [infer T1, infer T2, infer T3, Callback<infer U>?]? (arg1: T1, arg2: T2, arg3: T3) => Promise<U> :
    T extends [infer T1, infer T2, infer T3, infer T4, Callback<infer U>?] ? (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<U> :
    T;

And using the distributive behavior of conditional types we can create a union of all the overloads:

export type Promisify<T> =PromisifyOne<GetOverloadArgs<T>> 
export type Promisify<T> =PromisifyOne<GetOverloadArgs<T>> 
type fooOverloadUnion = Promisify<typeof foo>
// Same as 
type fooOverloadUnion = ((arg1: string) => Promise<string>) | ((arg1: string, arg2: string) => Promise<string>)

To make this callable again we ca use transform the union to an intersection, using UnionToIntersection, with the final result being:

export type Callback<T> = (err: Error | null, reply: T) => void;
export type PromisifyOne<T extends any[]> =
    T extends [Callback<infer U>?] ? () => Promise<U> :
    T extends [infer T1, Callback<infer P>?] ? (arg1: T1) => Promise<P> :
    T extends [infer T1, infer T2, Callback<infer U>?] ? (arg1: T1, arg2: T2) => Promise<U> :
    T extends [infer T1, infer T2, infer T3, Callback<infer U>?]? (arg1: T1, arg2: T2, arg3: T3) => Promise<U> :
    T extends [infer T1, infer T2, infer T3, infer T4, Callback<infer U>?] ? (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<U> :
    T;

export type GetOverloadArgs<T> = 
    T extends { (...o: infer U) : void, (...o: infer U2) : void, (...o: infer U3) : void   } ? U | U2 | U3:
    T extends { (...o: infer U) : void, (...o: infer U2) : void  } ? U | U2 :
    T extends { (...o: infer U) : void } ? U : never

type UnionToIntersection<U> = (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
export type Promisify<T> =  UnionToIntersection<PromisifyOne<GetOverloadArgs<T>>>

// Sample
declare function foo(arg1: string, cb: (err: Error|null, res: string) => void): void
declare function foo(arg1: string, arg2: string, cb: (err: Error|null, res: string) => void): void

declare const  fooPromise: Promisify<typeof foo>
let r = fooPromise("")
let r2 = fooPromise("", "")
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357