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

Cross domain request in a simple HTML document

I'm currently working on an app that is strictly in plain HTML files without a server. I'm facing difficulties with cross domain requests from JavaScript. Whenever I try to make a request, the browser displays this error message: XMLHttpRequest ...

Discovering Typescript: Inferring the type of a union containing specific strings

I am working with a specific type called IPermissionAction which includes options like 'update', 'delete', 'create', and 'read'. type IPermissionAction = 'update' | 'delete' | 'create' | ...

Guide to automating the versioning of static assets (css, js, images) in a Java-based web application

To optimize the efficiency of browser cache usage for static files, I am seeking a way to always utilize cached content unless there has been a change in the file, in which case fetching the new content is necessary. My goal is to append an md5 hash of th ...

Steps to deactivate a JavaScript function once the page has undergone a Page.IsPostBack event

My current setup involves a simple div with the display set to none. Upon page load, I use $("#MyDiv").show(); to display the div after a delay, allowing users to enter information into the form and submit it using an asp.net button. After submitting the ...

What is the method for combining two box geometries together?

I am looking to connect two Box Geometries together (shown in the image below) so that they can be dragged and rotated as one object. The code provided is for a drag-rotatable boxgeometry (var geometry1). What additional code do I need to include to join t ...

Executing a PHP script with form parameter onSubmit for sending an email

I am in the process of creating a form that will trigger a script and send data to a PHP page before redirecting away from the site upon submission. The main purpose behind this setup is to ensure that a confirmation email is sent to the individual fillin ...

Console is displaying an error message stating that the $http.post function is returning

Just diving into angular and I've set up a controller to fetch data from a factory that's loaded with an $http.get method connecting to a RESTful API: videoModule.factory('myFactory', function($http){ var factory = {}; facto ...

Seeking guidance for the Angular Alert Service

I'm relatively new to using Angular and I'm struggling to determine the correct placement for my AlertService and module imports. Currently, I have it imported in my core module, which is then imported in my app module. The AlertService functions ...

How can I align the Socket.io path on the client with the directory structure of my Node.js server?

Currently, I am utilizing socket.io with node.js along with Expressjs. As I serve my HTML page, I have the socket.io.js file linked directly in a script tag: <script src="/socket.io/socket.io.js"></script> I'm facing some difficulty match ...

Recommendation: 3 options for radio buttons on the registration form

My form includes a section where users need to choose if they want to sign up for a session that occurs 3 times daily. The catch is, only 5 applicants can enroll in each session (AM, Mid-day, PM). It's a competition to secure a spot. Here is the form ...

"Incorporate multiple data entries into a table with the help of Jquery

How can I store multiple values in a table row using jQuery or JavaScript when the values come from a database via Ajax? <html> <head> <style> table { font-family: arial, sans-serif; border-collapse: collapse; width: 100%; } td, th ...

Issues with basic emit and listener in socket.io

I recently inherited an App that already has socket IO functioning for various events. The App is a game where pieces are moved on a board and moves are recorded using notation. My task is to work on the notation feature. However, I am facing issues while ...

Cascading MVC 2 Dropdown menus

I am trying to bind a dropdown based on the change of another dropdown, but I keep getting an "Undefined" error. Here is my code snippet: <select id="BreakOutValue" class="input1_drop" onchange="onChange()" ></select> <%:Html.DropDownList( ...

Exploring the power of "this" in Vue.js 2.0

Seeking help with a VueJS issue. I am trying to create a voice search button that records my voice and prints it out in an input form. <input type="text" name="inputSearch" id="inputSearch" v-model="inputSearch" class="form-control" x-webkit-speech> ...

When used, the jQuery selector returns the PreviousObject rather than the object that was selected initially

I'm currently attempting to add a second menu to my website alongside the top menu. I have multiple menus set up in two bootstrap columns (col-8 and col-4). Initially, when I used jQuery selectors outside of a function, everything worked smoothly. How ...

WebDriverError: The preference value for network.http.phishy-userpass-length in Firefox is not valid: exceeds maximum allowed length

Attempting to initiate a new test case using gecko driver (v0.15) with a specific Firefox profile in Protractor 5.1.1. I created the profile based on this guidance: Set firefox profile protractor Upon starting the execution through the protractor configur ...

Enhancing React Flow to provide updated selection and hover functionality

After diving into react flow, I found it to be quite user-friendly. However, I've hit a roadblock while attempting to update the styles of a selected node. My current workaround involves using useState to modify the style object for a specific Id. Is ...

An External Force is Disrupting My Jquery

Something seems off because everything was working perfectly fine until the FallingLeavesChristmas.min.js file stopped activating on this specific page. I initially thought it was an issue with the JavaScript itself, but that doesn't seem to be the ca ...

Incorporate an external JS file (File A) that is dependent on another JS file (File B) into a TypeScript file within the context of Angular 4

Working on an Angular 4 project, I recently installed two external JS libraries using npm. They are now in the node_modules folder and usable in another TS file within my project. The issue arises because import B requires import A, preventing me from effe ...

Tips for using the identical function on matched elements

I am working on a code where I want each textbox input to change its corresponding image. The function is the same for all partners in the list (txt & img). I have looked around and found some similar posts, but I am struggling to make the function wor ...