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?
TypeScript provides the concept of function overloading. You can have multiple functions with the same name but different parameter types and return type. However, the number of parameters should be the same.
There are mainly two types of overloading, i.e. function overloading and operator overloading. Function overloading improves the code readability, thus keeping the same name for the same action.
Function overloading is a feature of object-oriented programming where two or more functions can have the same name but different parameters. When a function name is overloaded with different jobs it is called Function Overloading.
An overloaded function is really just a set of different functions that happen to have the same name. The determination of which function to use for a particular call is resolved at compile time. In Java, function overloading is also known as compile-time polymorphism and static polymorphism.
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("", "")
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With