Reputation: 1217
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).
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
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