Search code examples
typescripttypescript-typingsmapped-types

How to promisify all callback-style methods in an object


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?


Solution

  • 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>);
    }