Igor Soloydenko
Igor Soloydenko

Reputation: 11835

Typescript type inference in a generalized "promisify" function

Context

Recently I was working on "promisification" of a third-party library. Basically, the library is full of NodeJS async style functions (that use callback as a last argument). I.e. the functions which have signatures similar to this one:

function foo(arg1: string, arg2: number, ..., callback: (error, result) => void): void

I tried to write a function would reduce the code for wrapping the original functions and make them into Promise<T> returning ones:

function cb<TResult>(
  resolve: (res: TResult) => void,
  reject: (err: any) => void
): (actualError, actualResult) => void {

  return (error, result) => error ? reject(error) : resolve(result);
}

Then to promisify the methods, I'd write code like that:

patchUserMetadata(userId: string, userMetadata: any): Promise<a0.Auth0UserProfile> {
  return new Promise((resolve, reject) =>
    this.wrapped.patchUserMetadata(userId, userMetadata, cb(resolve, reject)));
}

linkUser(userId: string, secondaryUserToken: string): Promise<any> {
  return new Promise((resolve, reject) =>
    this.wrapped.linkUser(userId, secondaryUserToken, cb(resolve, reject)));
}

// ... and so on, and on, and on...

As you can easily see, I'm still not very familiar with TypeScript and basically was trying to reinvent a wheel. My wheel ended up being a hexagon and I kept writing too much wrapping code by hand...

Someone who reviewed my code pointed out that I can use js-promisify to achieve similar result at lower cost. The library defines a helper that does the job:

module.exports = function (fun, args, self) {
  return new Promise(function (resolve, reject) {
    args.push(function (err, data) {
      err && reject(err);
      resolve(data);
    })
    fun.apply(self, args);
  });
};

Since I'm dealing with TypeScript rather than JavaScript, I went further and did a bit of research. This is how I ended up picking typed-promisify and the code now looked like this:

patchUserMetadata = promisify(this.wrapped.patchUserMetadata);

linkUser = promisify(this.wrapped.linkUser);

MUCH neater, huh?

Getting closer

I was wondering how exactly does this promisify function work? I looked at the source code and found a solution which works similar to js-promisify's one:

export function promisify<T>(f: (cb: (err: any, res: T) => void) => void, thisContext?: any): () => Promise<T>;
export function promisify<A, T>(f: (arg: A, cb: (err: any, res: T) => void) => void, thisContext?: any): (arg: A) => Promise<T>;
export function promisify<A, A2, T>(f: (arg: A, arg2: A2, cb: (err: any, res: T) => void) => void, thisContext?: any): (arg: A, arg2: A2) => Promise<T>;
// ...more overloads

export function promisify(f: any, thisContext?: any) {
  return function () {
    let args = Array.prototype.slice.call(arguments);
    return new Promise((resolve, reject) => {
      args.push((err: any, result: any) => err !== null ? reject(err) : resolve(result));
      f.apply(thisContext, args);
    });
  }
}

Question

If you look at the promisify closely, you can see that this solution is not really generalized. Meaning, if I needed to promisify a function with 10+ parameters, there wouldn't be a matching overload for it. The implementation would still work fine, however the type information would get lost in this case.

Is there a way in TypeScript to infer the precise function type (or signature, or count and types of parameters) without defining all those nasty overloads upfront?

I'm looking for something like this [obviously, pseudocode]:

export function promisify<...[TArgs], T>(
  f: (...allArgsButLastTwo: [TArgs],
  cb: (err: any, res: T) => void) => void,
  thisContext?: any
): (...[TArgs]) => Promise<T>;

export function promisify(
  ...allArgsButLastTwo: any[],
  f: any,
  thisContext?: any
) {
  return function () {
    let args = Array.prototype.slice.call(arguments);
    return new Promise((resolve, reject) => {
      args.push((err: any, result: any) => err !== null ? reject(err) : resolve(result));
      f.apply(thisContext, args);
    });
  }
}

I have a feeling that what I'm looking for is not achievable and that's why long overload list was a last resort/compromise solution the author had to use.

Upvotes: 3

Views: 3078

Answers (2)

Acy
Acy

Reputation: 659

Since variadic types are now supported (TS 4.0+), we can write

type callback<P, Q> = (error: P, response: Q) => void;
const promisify =
  <T extends readonly unknown[], U, V>(
    f: (...fargs: [...T, callback<U, V>]) => void
  ) =>
  (...args: T) =>
    new Promise((res, rej) => {
      const callback: callback<U, V> = (err, response) => {
        if (err) rej(err);
        res(response);
      };
      f(...args, callback);
    });

Now, promisify won't always work when applied to overloaded functions. Example: The following gives an Expected 1 arguments, but got 2. error at 'utf8' .

const pmifyRead = promisify(fs.readFile); 
pmifyRead('/Users/joe/test.txt', 'utf8').then(console.log).catch(console.error)

But, fs.readFile can take in two arguments before its callback argument! Unfortunately, this function has 4 overloads, and the one chosen when binding to promisify is the bottommost overload: The overload that only takes in path and callback. If I can delay the type inference until the promisifed function is invoked, then everything should work. But, type is already inferred when promisified. Grateful for anyone who has a solution.

Upvotes: 2

fs_
fs_

Reputation: 196

As of version 2.5, there's currently no way of doing this in TypeScript until this gets resolved: https://github.com/Microsoft/TypeScript/issues/5453

It has been on the roadmap for a while, under Variadic Types.

Upvotes: 4

Related Questions