Unique Context
Not long ago, I found myself delving into the world of "promisification" while working on a third-party library. This library was packed with NodeJS async functions that followed the callback pattern. These functions had signatures similar to:
function foo(arg1: string, arg2: number, ..., callback: (error, result) => void): void
I attempted to simplify the process by creating a generic function to wrap these async functions and convert them into Promise-returning ones like so:
function cb<TResult>(
resolve: (res: TResult) => void,
reject: (err: any) => void
): (actualError, actualResult) => void {
return (error, result) => error ? reject(error) : resolve(result);
}
To promisify the methods, my code looked something like this:
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 forth...
It became evident that my TypeScript skills were still developing, as I realized I was reinventing the wheel in a rather convoluted manner. My solution resembled a hexagon instead of a wheel, requiring me to write excessive wrapping code...
A helpful reviewer suggested js-promisify
as a simpler alternative. The library provided a handy helper function:
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);
});
};
Given that I was dealing with TypeScript, I did some research and opted for typed-promisify
, which streamlined my code to:
patchUserMetadata = promisify(this.wrapped.patchUserMetadata);
linkUser = promisify(this.wrapped.linkUser);
Much cleaner, right?
Closer Look
I started to ponder how exactly the promisify
function operated. Upon inspecting the source code, I discovered a solution reminiscent of js-promisify
:
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);
});
}
}
The Question Persists
Upon closer examination of the promisify
function, it became apparent that its generality was limited. Promisifying a function with numerous parameters could lead to loss of type information due to the lack of specific overloads.
Is there a way in TypeScript to deduce the precise function type without explicitly defining multiple overloads?
My ideal scenario would involve inferring the function signature dynamically rather than preemptively listing all possible combinations upfront.