What is a superior option to converting to a promise?

Imagine I am creating a function like the one below:

async function foo(axe: Axe): Promise<Sword> {
  // ...
}

This function is designed to be utilized in this manner:

async function bar() {
  // acquire an axe somehow ...

  const sword = await foo(axe);

  // utilize the obtained sword ...
}

Everything seems fine so far. The issue arises when I need to invoke a "callback-style async" function in order to implement foo, and I'm unable to modify its signature as it's part of a library/module:

/* The best way to obtain a sword from an axe!
 * Returns null if axe is not sharp enough */
function qux(axe: Axe, callback: (sword: Sword) => void);

To address this, I decided to "promisify" Quux:

async function foo(axe: Axe): Promise<Sword> {
  return new Promise<Sword>((resolve, reject) => {
    qux(axe, (sword) => {
      if (sword !== null) {
        resolve(sword);
      } else {
        reject('Axe is not sharp enough ;(');
      }
    });
  });
}

While this method works, I desired a more straightforward and readable solution. In some programming languages, it's possible to create a promise-like object (referred to as Assure here), and then explicitly set its value elsewhere. It could look something like this:

async function foo(axe: Axe): Promise<Sword> {
  const futureSword = new Assure<Sword>();

  qux((sword) => {
    if (sword !== null) {
      futureSword.provide(sword);
    } else {
      futureSword.fail('Axe is not sharp enough ;(');
    }
  });

  return futureSword.promise;

Is there a native way to achieve this in the language itself, or would I need to rely on a library/module like deferred?

Update (1): additional rationale

Why opt for the second approach over the first? Consider callback chaining.

What if I needed to execute multiple steps within foo, not just invoking qux? In synchronous code, it might resemble this:

function zim(sling: Sling): Rifle {
  const bow = bop(sling);
  const crossbow = wug(bow);
  const rifle = kek(crossbow); 
  return rifle;
}

If these functions were asynchronous, promisifying would result in this structure:

async function zim(sling: Sling): Promise<Rifle> {
  return new Promise<Rifle>((resolve, reject) => {
    bop(sling, (bow) => {
      wug(bow, (crossbow) => {
        kek(crossbow, (rifle) => {
          resolve(rifle);
        });
      });
    });
  );
}

By utilizing an Assure, the implementation could appear as follows:


async function zim(sling: Sling): Promise<Rifle> {
  const futureBow = new Assure<Bow>();
  bop(sling, (bow) => futureBow.provide(bow));

  const futureCrossbow = new Assure<Crossbow>();
  wug(await futureBow, (crossbow) => futureCrossbow.provide(crossbow));

  const futureRifle = new Assure<Rifle>();
  kek(await futureCrossmbow, (rifle) => futureRifle.provide(rifle));
 
  return futureRifle;
}

This method offers better manageability by eliminating the need to track nested scopes and worry about computation order, especially with functions that take multiple arguments.

Reflection

Although the version with nested calls may seem elegant due to fewer temporary variables declarations, the chained callbacks provide a cleaner structure.

Additionally, during the course of composing this inquiry, I considered another approach that aligns with JavaScript principles:

function zim(sling: Sling): Rifle {
  const bow = await new Promise((resolve, reject) => { bop(sling, resolve); });
  const crossbow = await new Promise((resolve, reject) => { wug(bow, resolve); });
  const rifle = await new Promise((resolve, reject) => { kek(crossbow, resolve); }); 
  return rifle;
}

This alternative resembles the usage of util.promisify from Node.js. If only the callbacks adhered to the error-first convention... Nevertheless, it might be worthwhile to develop a rudimentary myPromisify function to encapsulate the callback type handling.

Answer №1

To implement the Assure in TypeScript, you can follow a simple approach:

class Assure<T, U = unknown> {
    public promise: Promise<T>;
    private resolve!: (value: T) => void;
    private reject! : (error: U) => void;

    constructor() {
        this.promise = new Promise((resolve, reject) => {
            this.resolve = resolve;
            this.reject = reject;
        });
    }

    public provide(value: T) {
        this.resolve(value);
    }

    public fail(reason: U) {
        this.reject(reason);
    }
}

Link to Playground with Demo

However, this implementation may not be necessary as it ends up being similar to using regular promises with certain drawbacks:

  1. It deviates from common practices.
  2. No significant advantages over regular promises.

An improved alternative would be defining a function that converts callback functions into promise-returning ones. This process is relatively straightforward and even allows enforcing correct types in TypeScript. Here's an outline for handling various callback styles:

// Utility types
// Function first parameter extraction
type FirstParameter<T extends (...args: any[]) => void> = Head<Parameters<T>>;

// Function last parameter extraction
type LastParameter<T extends (...args: any[]) => void> = Last<Parameters<T>>; 

// Converting async function with result callback to promise
function promisifyResultCallback<T extends (...args: any[]) => void>(fn: T) {
    // Implementation details here...
}

// Error-first style callback conversion to promise
function promisifyErrorFirstCallback<T extends (...args: any[]) => void>(fn: T) {
    // Implementation details here...
}

// Handling asynchronous function with two callbacks
function promisifyTwoCallbacks<T extends (...args: any[]) => void>(fn: T) {
    // Implementation details here...
}

These utility functions enable seamless conversion like this:

// Usage examples
const p_onlyResultCallback = promisifyResultCallback(onlyResultCallback);

const p_errorFirstCallback = promisifyErrorFirstCallback(errorFirstCallback);

const p_twoCallbacks = promisifyTwoCallbacks(twoCallbacks);

Playground Example

The process simplifies function calls and eliminates nesting issues when working with multiple asynchronous functions:

// Before promisifying functions
function zim(sling: Sling): Promise<Rifle> {
  return new Promise<Rifle>((resolve, reject) => {
    bop(sling, (bow) => {
      wug(bow, (crossbow) => {
        kek(crossbow, (rifle) => {
          resolve(rifle);
        });
      });
    });
  });
}

After converting the functions to promises:

const p_bop = promisifyResultCallback(bop);
const p_wug = promisifyResultCallback(wug);
const p_kek = promisifyResultCallback(kek);

You can handle them similarly to regular promises:

function zim(sling: Sling) {
    return p_bop(sling)
        .then(p_wug)
        .then(p_kek)
}

or utilize async/await for a more readable code:

async function zim(sling: Sling) {
    const bow = await p_bop(sling);
    const crossbow = await p_wug(bow);
    const rifle = await p_kek(crossbow);
    return rifle;
}

Try it on Playground

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

Using finally() to correctly construct a Javascript promise

Currently, I am working on an Express API that utilizes the mssql package. If I neglect to execute sql.close(), an error is triggered displaying: Error: Global connection already exists. Call sql.close() first. I aim to keep the endpoints simple and e ...

Encountering issues with displaying images in React Next.js when utilizing dangerouslySetInnerHtml

While working on creating a simple WYSIWYG editor in nextjs, I encountered an issue with displaying uploaded images on the screen. When generating a blob URL for the image and using it as the src attribute of the image tag, it worked fine unless dangerousl ...

Is it possible to effortlessly associate a personalized string with an identifier within an HTML element utilizing Angular2?

Check out this cool plunker import {Component} from 'angular2/core' @Component({ selector: 'my-app', template: ` <div *ngFor="#option of myHashMap"> <input type="radio" name="myRadio" id="{{generateId(option[& ...

Guide on displaying several items from Laravel to Vue through the JavaScript filter method?

I need help transferring multiple objects from laravel to vue and filling my vue objects with information retrieved from the database. This is the data received from laravel: Vue objects to be populated: storeName: {}, storeUrl: {}, appName: {}, ...

"Techniques for extracting both the previous and current selections from a dropdown menu in Angular 2

How can I retrieve the previous value of a dropdown before selection using the OnChange event? <select class="form-control selectpicker selector" name="selectedQuestion1" [ngModel]="selectedQuestion1" (Onchange)="filterSecurityQuestions($event.t ...

AngularJS, the element being referenced by the directive is empty

Currently, I am in the process of transferring a jQuery plugin to AngularJS simply for the enjoyment of it. Previously, when working with jQuery, my focus was on manipulating the DOM using jQuery functions within the plugin loading function. Now that I am ...

Tips on simulating the Q functions during unit testing in node.js using mocha and rewire!

Struggling with an issue while writing unit tests for node.js. The original code in my file is: var Q=require('q') . . . return Q.all(promises).then(function(data) { _.each(data, function(data) { checking.pu ...

Sorry, the server cannot be reached at the moment. Please try again later

Recently delving into Node.js and just getting started on using MongoDB. Currently establishing a connection with my MongoDB Cluster that I have set up. const dbURI = 'mongodb+srv://testuser:<a href="/cdn-cgi/l/email-protection" class="__cf_email_ ...

Obtaining a value from an array using Google App Script

Having some difficulties with the code snippet provided here. It might be a basic question, but I'm unable to figure out what's going wrong. The issue is that I'm trying to perform calculations on individual values in an array generated fro ...

Assuming control value accessor - redirecting attention

import { Component, Input, forwardRef, OnChanges } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'formatted-currency-input', templateUrl: '../v ...

What methods are available for identifying non-operational pointer-events?

According to this resource, Opera 12 does not support pointer-events, causing issues with my website. Interestingly, they do support the property in CSS but don't seem to implement it correctly. Modernizr's feature detection doesn't help in ...

There appears to be an inexplicable issue hindering the functionality of smoothscroll in

Continuing to improve my website: I have implemented a button for scrolling down the page, but I would like to add smooth scrolling functionality for a better user experience. After some research and experimentation, I came across this compact script tha ...

Issue with Ref when used in a distinct HTML template

I have encountered a frustrating issue with my simple Vue project. When I separate the template and code into individual files, the ref stops working and I end up with an undefined value in the HTML template. This scenario works fine: map.component.vue ...

What is the best way to execute AJAX requests in a loop synchronously while ensuring that each request is completed

I am looking to implement an AJAX loop where each call must finish before moving on to the next iteration. for (var i = 1; i < songs.length; i++) { getJson('get_song/' + i).done(function(e) { var song = JSON.parse(e); addSongToPlayl ...

``I am having trouble getting the Bootstrap carousel to start

I am having trouble getting the Bootstrap Carousel to function as expected. Despite including all the necessary code, such as initializing the carousel in jQuery.ready, the images are stacking on top of each other with navigation arrows below them instea ...

Is it possible to modify the content of an element with a click event in Angular/HTML?

How can I implement a feature in my mat-accordion using mat-expansion-panels where the text becomes underlined when the panels are clicked? I want the title to be underlined when the panels are open and for the underline to disappear when they are closed ...

Utilizing Jquery Validation to Remove a Class Upon Form Validation Success

In my current registration process, I have a multipart form where each subsequent form is displayed when the next button is pressed without fading effects. Initially, the button appears faded. Here's a simplified version of how I handle the first form ...

"Refining MongoDB queries with a filter after performing a populate

I want to retrieve all records with populated attributes in a query. Here is the TypeScript code: router.get("/slice", async (req, res) => { if (req.query.first && req.query.rowcount) { const first: number = parseInt(req.qu ...

Uploading PDF files using PHP without the need for traditional input boxes

Currently, I am utilizing a JavaScript code to add annotations to a PDF document within the browser. Once I have completed adding annotations, I save the modified document on my device and then proceed to upload it using another form. However, my goal is ...

Expanding Text Box

My goal is to create a textarea that automatically adjusts its size as the user types or pastes long text into it. This is my current implementation. class AutoResizeTextArea extends InputBase { render() { const { label, placeholder, wrap, ro ...