Utilizing Typescript's type inference within a universal "promisify" function

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.

Answer №1

Currently, in TypeScript version 2.5, there is no direct way to achieve this functionality until the issue is resolved. You can track the progress at: https://github.com/Microsoft/TypeScript/issues/5453

This feature has been on the development roadmap for some time now, categorized under Variadic Types.

Answer №2

With the introduction of variadic types now being accepted (TS 4.0+), we have the ability to construct like this:

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

However, applying promisify does not always work smoothly with overloaded functions. For instance, using it with fs.readFile prompts an error saying

'Expected 1 arguments, but got 2.'
specifically at 'utf8'.

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

In this case, even though fs.readFile can accept two arguments before its callback parameter, due to having 4 different overloads, the variant that only takes in path and callback is selected when binding to promisify. One possible solution could be deferring the type inference until the moment the promisified function is called. Unfortunately, the type inference occurs during the promisification itself. Any help or solutions would be greatly appreciated.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Assign a value to a FormControl in Angular 6

I have 60 properties linked to 60 controls through the mat-tab in a form. When it comes to editing mode, I need to assign values to all the controls. One approach is as follows: this.form.controls['dept'].setValue(selected.id); This involves l ...

Is there a way to delegate properties in Angular 2+ similar to React?

When working with React, I have found it convenient to pass props down dynamically using the spread operator: function SomeComponent(props) { const {takeOutProp, ...restOfProps} = props; return <div {...restOfProps}/>; } Now, I am curious how I ...

Swap out the default URL in components with the global constant

Could anyone offer some assistance with this task I have at hand: Let's imagine we have a global constant 'env' that I need to use to replace template URLs in components during build time. Each component has a default template URL, but for ...

Using Typescript to implement a conditional return type and ensuring that the value types are consistent together

I am working with a useSelectedToggle hook that helps in connecting the UI state to the open/closed status of a dialog where it will be displayed. The toggle defines the value as (T) when it is open, and null when it is closed. How can I enforce stricter ...

Resolving Angular Issue: Error code (5, 12) TS2314 - Solving the requirement for 1 type argument in the 'Array<T>' generic type

Encountered an issue in the JSON data within my typescript file. I'm working on creating an Angular API that performs a git-search operation. Initially, I had the JSON data set up correctly but then I modified all data values to their respective data ...

Verify if the keys are present within the object and also confirm if they contain a value

How can we verify keys and compare them to the data object? If one or more keys from the keys array do not exist in the object data, or if a key exists but its value is empty, null, or undefined, then return false; otherwise, return true. For example, if ...

Zero-length in Nightmare.js screenshot buffer: an eerie sight

I'm currently working on a nightmare.js script that aims to capture screenshots of multiple elements on a given web page. The initial element is successfully captured, but any subsequent elements below the visible viewport are being captured with a l ...

Oops! The program encountered an issue on the production environment, but it's running smoothly

When I execute Webpack using the command node node_modules/webpack/bin/webpack. js --env. prod An error message is displayed below. However, when running in --env. dev mode, the command executes without any issues. Can't resolve './../$$_gen ...

How can you update the property values of a CSS class that already exists in an Angular2+ controller?

In my styles.css file, I have a CSS class called '.users' with a property of color. Now, I am looking to dynamically change the color property value of the 'users' class based on certain conditions in my controller. For instance, I want ...

Triggering blur event manually in Ionic 3

Is there a way to manually trigger the blur event on an ion-input element? The ideal scenario would be with an ionic-native method, but any javascript-based workaround will suffice. My current configuration: Ionic: ionic (Ionic CLI) : 4.0.1 (/User ...

What is the process for exporting a class and declaring middleware in TypeScript?

After creating the user class where only the get method is defined, I encountered an issue when using it in middleware. There were no errors during the call to the class, but upon running the code, a "server not found" message appeared. Surprisingly, delet ...

Generate md-card components in real-time using data fetched from an API

I have a scenario where I make an API call to fetch user profiles, and I want to generate Angular Material Design md-cards dynamically for each profile. The number of profiles retrieved can vary, hence the need for dynamic card creation. Below is the comp ...

Adding dynamic metadata to a specific page in a next.js app using the router

I was unable to find the necessary information in the documentation, so I decided to seek help here. My goal is to include metadata for my blog posts, but I am struggling to figure out how to do that. Below is a shortened version of my articles/[slug]/page ...

problem with arranging sequences in angular highcharts

I am facing an issue with sorting points in different series using highcharts. To illustrate my problem, consider the following example series: [ {name: 'series one', value: 5 values}, {name: 'series two', value: 10 values} ] When usin ...

Steps to make ng-packagr detect a Typescript type definition

Ever since the upgrade to Typescript 4.4.2 (which was necessary for supporting Angular 13), it appears that the require syntax is no longer compatible. Now, it seems like I have to use this alternative syntax instead: import * as d3ContextMenu from ' ...

prolonging inner interface created by supabase

Below is the Typescript interface that has been generated by Supabase export interface definitions { Users: { /** Format: uuid */ id: string; /** * Format: timestamp with time zone * @default now() */ created_at?: string; ...

What is the reason for my algorithm's inability to work with this specific number?

I'm currently working on creating an algorithm to compute the sum of prime numbers that are less than or equal to a specified number. Below is my attempt: function calculatePrimeSum(num) { // initialize an array with numbers up to the given num let ...

Exploring NextJS with Typescript to utilize the getStaticProps method

I'm currently enrolled in a NextJS course and I am interested in using Typescript. While browsing through a GitHub discussion forum, I came across an issue that I don't quite understand. The first function provided below seems to be throwing an e ...

What are the benefits of pairing Observables with async/await for asynchronous operations?

Utilizing Angular 2 common HTTP that returns an Observable presents a challenge with nested Observable calls causing code complexity: this.serviceA.get().subscribe((res1: any) => { this.serviceB.get(res1).subscribe((res2: any) => { this.se ...

Inquiry regarding the return value of 'async-lock' in nodejs

I am utilizing the async-lock module in my typescript project to handle concurrency. However, I am encountering difficulties with returning the result within lock.acquire(...) {...}. Any guidance on how to resolve this issue would be greatly appreciated. ...