Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type inference with overloaded functions

Tags:

typescript

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?

like image 945
Rémi Rousselet Avatar asked Aug 02 '18 10:08

Rémi Rousselet


People also ask

Does TypeScript support function overloading?

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.

What are the different types of function overloading?

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.

What is overloaded function give example?

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.

What happens when you overload functions?

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.


1 Answers

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("", "")
like image 138
Titian Cernicova-Dragomir Avatar answered Nov 03 '22 23:11

Titian Cernicova-Dragomir