WaeCo
WaeCo

Reputation: 1217

Promisified function type

I am writing a RFC library. Basically I have an interface that is implemented on the server and wrapped by a Proxy in the client. The Proxy then does http calls in the background to call the method on the server.

This works fine with functions that already return a Promise. However on the client functions will always return a Promise through the Proxy wrapper, but the type system does not know this.

So with the following code I am creating a mapped type to change the return types of the functions to Promise.

// Generic Function definition
type AnyFunction = (...args: any[]) => any;
// Extracts the type if wrapped by a Promise
type Unpacked<T> = T extends Promise<infer U> ? U : T;

type PromisifiedFunction<T extends AnyFunction> =
    T extends () => infer U ? () => Promise<Unpacked<U>> :
    T extends (a1: infer A1) => infer U ? (a1: A1) => Promise<Unpacked<U>> :
    T extends (a1: infer A1, a2: infer A2) => infer U ? (a1: A1, a2: A2) => Promise<Unpacked<U>> :
    T extends (a1: infer A1, a2: infer A2, a3: infer A3) => infer U ? (a1: A1, a2: A2, a3: A3) => Promise<Unpacked<U>> :
    // ...
    T extends (...args: any[]) => infer U ? (...args: any[]) => Promise<Unpacked<U>> : T;

type Promisified<T> = {
    [K in keyof T]: T[K] extends AnyFunction ? PromisifiedFunction<T[K]> : never
}

Example:

interface HelloService {
    /**
    * Greets the given name
    * @param name 
    */
    greet(name: string): string;
}

function createRemoteService<T>(): Promisified<T> { /*...*/ }

const hello = createRemoteService<HelloService>();
// typeof hello = Promisified<HelloService>
hello.greet("world").then(str => { /*...*/ }) // all fine here
// typeof hello.greet = (a1: string) => Promise<string>

Well everything works, so whats the problem?

What I do not like about this implementation is, that argument names and documentation gets lost (at least in vs code).

enter image description here

So for the person that just wants to consume a service it is not a nice development experience to look up the service definition somewhere external.

The other thing I do not like is that I have to write a definition for every amount of arguments. But I guess there is no other way until Typescript supports Variadic Types.

EDIT: While continue working with this I discovered a bigger problem: Having an overloaded function in the interface the mapped interface is not correctly inferred.

interface HelloService {
    greet(name: string): string;
    greet(id: number): string;
}

Depending on the order of the functions the mapped type is either
typeof hello.greet = (a1: string) => Promise<string> or
typeof hello.greet = (a1: number) => Promise<string>

But it is supposed to be:
typeof hello.greet = (a1: string|number) => Promise<string>

So any suggestions on how to improve this?

Upvotes: 1

Views: 179

Answers (1)

WaeCo
WaeCo

Reputation: 1217

With new Typescript 3.0 one can use generic rest parameters.

This allows us to specify the types like this:

type UnpackPromise<T> = T extends Promise<infer U> ? U : T;

type AnyFunction<U extends any[], V> = (...args: U) => V;
type Promisified<T> = {
    [K in keyof T]: T[K] extends AnyFunction<infer U, infer V> ? (...args: U) => Promise<UnpackPromise<V>> : never;
}

Upvotes: 2

Related Questions