I am relatively new to typescript. I would like to create a generic wrapper/utility that would take an object with callback-style methods (unknown in advance) and promisify them. The specific use case is to promisify auto-generated node grpc clients.
Example type for a client using callback-style methods:
type AutogeneratedClient = {
autogeneratedMethod(request: MethodSpecificRequestType, callback: (error: ServiceError, response: MethodSpecificResponseType) => void): ClientUnaryCall
}
I want to be able to have a generic "promisify" utility that would return an object as described by the following type:
type PromisifiedAutogeneratedClient = {
autogeneratedMethod(request: MethodSpecificRequestType): Promise<MethodSpecificResponseType>
}
I was thinking of implementing this as a Proxy that uses the "get" trap to return a wrapper function that promisifies the original callback-based method. Problem is, I don't know how to do it with Typescript. I started looking into Mapped types, but I am still unable to make this work.
How would you solve this problem in a type-safe way?
I am thinking something along the lines of the following:
First, let's define some types. I do not know how gRPC generically defines its clients, so I will assume something like the one you have shown above:
type GrpcClientFn<REQ, RES> =
(req: REQ, cb: (e: ServiceError, res: RES) => void) => ClientUnaryCall;
Which naturally leads us to the corresponding promisifeid type:
type PromisifiedGrpcClientFn<REQ, RES> = (req: REQ) => Promise<RES>;
Now the type for a single client function "promisifier", not exactly what you want, but a stepping stone to it, and an implementation:
type Promisify<REQ, RES, F extends Function> =
F extends GrpcClientFn<REQ, RES> ? PromisifiedGrpcClientFn<REQ, RES> : never;
function promisify<REQ, RES, FIN extends GrpcClientFn<REQ,RES>>(fin: FIN): PromisifiedGrpcClientFn<REQ, RES> {
return function(req: REQ) {
return new Promise((resolve, reject) => {
fin(req, (error, outcome) => {
if (error) {
reject(error);
} else {
resolve(outcome);
}
});
});
}
}
This takes a gRPC-style function and promisifies it. (Stylistically, I choose to use the old-style function(a,b,c) { ... }
syntax over the modern (a,b,c) => { ... }
syntax in some places, to make the return type explicit.)
Ok, the hard stuff gone: now define an entire object whose values are gRPC client functions:
type GrpcClientObj = {
[key: string]: GrpcClientFn<any, any>;
}
I wish I could do something better about the <any,any>
, but I can't think of something!
Before defining the "promisified" object type, I need two helpers, to extract the request and response parameter types:
// obtain type of request parameter
type PREQ<F extends Function> =
F extends (req: infer REQ, cb: (e: ServiceError, res: any) => void) => ClientUnaryCall ? REQ : never;
// obtain type of response parameter
type PRES<F extends Function> =
F extends (req: any, cb: (e: ServiceError, res: infer RES) => void) => ClientUnaryCall ? RES : never;
At last, the type of the "promisified" object is:
type PromisifiedGrpcClientObj<T extends GrpcClientObj> = {
[P in keyof T]: Promisify<PREQ<T[P]>, PRES<T[P]>, T[P]>;
}
And the implementation couldn't come easier:
function promisifyObj(o: GrpcClientObj): PromisifiedGrpcClientObj<GrpcClientObj> {
return Object.keys(o).reduce((aggr, cur) => ({
...aggr,
[cur]: promisify(o[cur])
}), {} as PromisifiedGrpcClientObj<GrpcClientObj>);
}
And a Typescript playground link.
EDIT: Maybe, the following would be more appropriate - the difference is how we define the output type in relation to the input type:
function promisifyObj<GRPCO extends GrpcClientObj>(o: GRPCO): PromisifiedGrpcClientObj<GRPCO> {
return Object.keys(o).reduce((aggr, cur) => ({
...aggr,
[cur]: promisify(o[cur])
}), {} as PromisifiedGrpcClientObj<GRPCO>);
}