The classification of rejected promises in Typescript

Is it possible to set the type of rejection for a promise? For example:

const start = (): Promise<string> => {
   return new Promise((resolve, reject) => {
      if (someCondition) {
         resolve('correct!');
      } else {
         reject(-1);
      }
   });
}

I want to be able to reject with a number, but ensure that the type is specified. How can I achieve this without risking wrong types being passed to reject?

Additionally, I'd like to receive a compilation error if the rejection response type is handled incorrectly when using this promise.

Answer №1

As detailed in this discussion, the Promise object does not differentiate between fulfilled and rejected promises with distinct types. The reject method accepts any argument without altering the promise's type.

It is currently challenging to provide a more precise typing for the Promise object due to the possibility of rejecting a promise by using throw within then or catch blocks (which is the recommended approach). This behavior cannot be effectively managed by the typing system. Furthermore, TypeScript lacks exception-specific types except for never.

Answer №2

In certain scenarios like Promise or exception throws where setting error types is not possible, handling errors in a Rust-like style can be beneficial:

// Result<T, E> is the type used for returning and propagating errors.
// It is a sum type with two variants:
// Ok<T>, representing success with a value, and 
// Err<E>, representing an error with an error value.
export type Ok<T> = { _tag: "Ok"; ok: T };
export type Err<E> = { _tag: "Err"; err: E };
export type Result<T, E> = Ok<T> | Err<E>;
export const Result = Object.freeze({
  Ok: <T, E>(ok: T): Result<T, E> => ({ _tag: "Ok", ok }),
  Err: <T, E>(err: E): Result<T, E> => ({ _tag: "Err", err }),
});

const start = (): Promise<Result<string, number>> => {
  return new Promise((resolve) => {
    resolve(someCondition ? Result.Ok("correct!") : Result.Err(-1));
  });
};

start().then((r) => {
  switch (r._tag) {
    case "Ok": {
      console.log(`Ok { ${r.ok} }`);
      break;
    }
    case "Err": {
      console.log(`Err { ${r.err} }`);
      break;
    }
  }
});

Answer №3

When it comes to exceptions, the typed any is necessary because we can't always be sure of the correct type of exception during design time. Unfortunately, both TypeScript and JavaScript lack the capability to safeguard exception types at runtime. The most effective solution is to implement type guards in order to perform checks at both design-time and run-time in your code.

source

Answer №4

Here's my interpretation of it:

declare class CustomErrorPromise<TSuccess, TError> extends Promise<TSuccess> {
    constructor(executor: (resolve: (value: TSuccess | PromiseLike<TSuccess>) => void, reject: (reason: TError) => void) => void) {
        super(executor);
        // Object.setPrototypeOf(this, new.target.prototype);  // restore prototype chain
    }
}

export interface CustomErrorPromise<TSuccess, TError = unknown> {
    then<TResult1 = TSuccess, TResult2 = never>(onfulfilled?: ((value: TSuccess) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: TError) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;

    catch<TResult = never>(onrejected?: ((reason: TError) => TResult | PromiseLike<TResult>) | undefined | null): Promise<TSuccess | TResult>;
}

Implement it in your code:

return new CustomErrorPromise<T, ExecError>((resolve, reject) => { ... })

Your text editor should recognize the type of reject:

https://i.sstatic.net/2pjzb.png

https://i.sstatic.net/iSSmo.png

Answer №5

As stated by @EstusFlask in their response, the information provided is accurate.

However, I am interested in exploring an artificial solution to simulate the desired outcome using TypeScript features.

Occasionally, I implement this approach in my codebase 😊:

interface IMyEx{
   errorId:number;
}

class MyEx implements IMyEx{
   errorId:number;
   constructor(errorId:number) {
      this.errorId = errorId;
   }
}
// -------------------------------------------------------
var prom = new Promise(function(resolve, reject) {
     try {
         if(..........)
            resolve('Huuuraaa');         
         else
            reject(new MyEx(100));
     }
     catch (error) {
            reject(new MyEx(101));
     }
});

// -------------------------------------------------------
prom()
.then(success => {
    try {
        }
    catch (error) {
        throw new MyEx(102);
    }
})
.catch(reason=>{
    const myEx = reason as IMyEx;
    if (myEx && myEx.errorId) {
       console.log('known error', myEx)
    }else{
       console.log('unknown error', reason)
    }
})

Answer №6

One way to ensure the argument types of resolve and reject in JavaScript is to use a proxy. In the example below, there is no attempt to simulate the promise constructor, as the focus is on enabling calling .resolve() and .reject() as standalone functions. On the consuming end, the naked promise is utilized with syntax like

await p.promise.then(...).catch(...)
.

export type Promolve<ResT=void,RejT=Error> = {
  promise: Promise<ResT>;
  resolve: (value: ResT | PromiseLike<ResT>) => void;
  reject: (value: RejT) => void
};

export function makePromolve<ResT=void,RejT=Error>(): Promolve<ResT,RejT> {
  let resolve: (value: ResT | PromiseLike<ResT>) => void = (value: ResT | PromiseLike<ResT>) => {}
  let reject: (value: RejT) => void = (value: RejT) => {}
  const promise = new Promise<ResT>((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

While the let statements may seem redundant at runtime, they serve the purpose of preventing compiler errors that are otherwise difficult to resolve.

(async() => {
  const p = makePromolve<number>();
  //p.resolve("0") // compiler error
  p.resolve(0);
  // p.reject(1) // compiler error 
  p.reject(new Error('oops')); 

  // simply use the named promise without enforcing types on the receiving end
  const r = await p.promise.catch(e=>e); 
})()

By following this approach, calls to .resolve and .reject can be dynamically checked for correct types.

In contrast, no effort is made here to impose type checking on the receiving side. While exploring this idea, adding .then and .catch members raised questions about returning values. Should they return a Promise, it would revert back to a standard promise operation. As a result, sticking to using the naked promise for operations like await, .then, and .catch seems most practical.

Answer №7

My approach to the problem is as follows:

To tackle this issue, I propose creating a new type that extends both the original PromiseConstructor and Promise:

interface CustomPromiseConstructor<ResolvedType, RejectedType> extends PromiseConstructor {
  /**
   * Constructs a new Promise.
   * @param executor A function used to initialize the promise. This function takes two parameters:
   * a resolve callback function to fulfill the promise with a value or another promise's result,
   * and a reject callback function to reject the promise with an error or reason.
   */
  new <T = ResolvedType, R = RejectedType>(
    executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: R) => void) => void
  ): CustomPromise<T, R>;
}

interface CustomPromise<ResolvedType, RejectedType> extends Promise<ResolvedType> {
  /**
   * Registers a handler for when the Promise rejects.
   * @param onrejected The function to execute upon rejection of the Promise.
   * @returns A Promise representing the completion of the handler.
   */
  catch<TResult = never>(
    onrejected?: ((reason: RejectedType) => TResult | PromiseLike<TResult>) | undefined | null
  ): CustomPromise<ResolvedType | TResult>;

  /**
   * Attaches callbacks for the resolution and/or rejection of the Promise.
   * @param onfulfilled The callback to execute when the Promise resolves.
   * @param onrejected The callback to execute when the Promise rejects.
   * @returns A Promise representing the completion of either callback.
   */
  then<TResult1 = ResolvedType, TResult2 = never>(
    onfulfilled?: ((value: ResolvedType) => TResult1 | PromiseLike<TResult1>) | undefined | null,
    onrejected?: ((reason: RejectedType) => TResult2 | PromiseLike<TResult2>) | undefined | null
  ): Promise<TResult1 | TResult2>;
}

We can now proceed to transform instances of the Promise type into those of the CustomPromiseConstructor during implementation:

const promise = new (Promise as CustomPromiseConstructor<number, Error>)((resolve, reject) => {
  if (Date.now() % 2 === 0) {
    reject(new Error(`Worker terminated with exit code: 1`));
  } else {
    resolve(0);
  }
});

Answer №8

My approach to solving this (similar to @moontai0724) was as follows:

// typed-promise.d.ts

interface TypedPromise<RESOLVE, REJECT> extends Promise<RESOLVE> {

  then<TResult1 = RESOLVE, TResult2 = never>(
      onfulfilled?: (((value: RESOLVE) => (TResult1 | PromiseLike<TResult1>)) | undefined | null),
      onrejected?: (((reason: (REJECT | any)) => (TResult2 | PromiseLike<TResult2;)) | undefined |null)
  ): TypedPromise<TResult1 | TResult2>;

  catch<TResult = never>(onrejected?: (((reason: (REJECT | any)) => (TResult | PromiseLike<TResult>)) | undefined | null): TypedPromise<RESOLVE | TResult>;

  finally(onfinally?: ((() => void) | undefined | null)): TypedPromise<RESOLVE | REJECT>;

}
// typed-promise-constructor.d.ts

interface TypedPromiseConstructor<RESOLVE, REJECT> extends PromiseConstructor {

  new<RES = RESOLVE, REJ = REJECT>(
      executor: ((resolve: ((value: (RES | PromiseLike<RES>)) => void), reject: ((reason?: REJ) => void)) => void)
  : TypedPromise<RES, REJ>;
}

declare var TypedPromise: TypedPromiseConstructor;

Answer №9

If you stumbled upon this page while searching on google, although not directly related to the original question, I believe it is still worth mentioning.

I recently faced a scenario where I needed TypeScript to recognize that a promise would only reject, particularly when using Promise.race for timeouts. This can be achieved by simply specifying the return type as never. TypeScript will then interpret that the promise will ONLY result in rejection.

Here is an example to illustrate:

const timeout = new Promise<never>((resolve, reject) => {
    setTimeout(() => {
        reject(new Error('Cache timed out after 10 seconds'));
    }, 10000);
});
const result = await Promise.race([
    cache.searchCache(key),
    timeout
]);

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

Issue with jQuery in Internet Explorer causing difficulties loading images

I'm experiencing an issue with my website where all the images load on top of each other when the site is first loaded, creating a chaotic mess. The desired functionality is for the images to remain invisible until specific words are clicked, which is ...

A Vue filtering feature that consistently adds 5 additional elements upon each use

I was wondering, how can I create a computed property filter function that always adds 5 more elements to the current array? Here are more details: Template: <span class="box-content" v-for="item in activeItems" :key="item.id"> <img class=" ...

Idiosyncratic TypeScript behavior: Extending a class with comparable namespace structure

Lately, I've been working on creating a type library for a JavaScript written library. As I was defining all the namespaces, classes, and interfaces, I encountered an error TS2417 with some of the classes. I double-checked for any issues with method o ...

Does Angular 8 development mode implement tree-shaking?

I am curious to know if tree-shaking occurs during Angular 8 development mode. When running the following command: ng build I understand that tree-shaking happens when I use the command below: ng build --optimization=true|false ...

A guide on selecting the best UI container based on API data in React using TypeScript

I have been developing a control panel that showcases various videos and posts sourced from an API. One div displays video posts with thumbnails and text, while another shows text-based posts. Below is the code for both: <div className=""> &l ...

Enhancing Rails functionality with drag-and-drop to modify object attributes

I work with two main models: Collections and Images Collection Model class Collection < ActiveRecord::Base has_many :images, dependent: :destroy accepts_nested_attributes_for :images end Image Model class Image < ActiveRecord::Base belongs_ ...

What is the proper method for triggering an animation using an IF statement with A-Frame animation mixer?

I am currently exploring the capabilities of the animation mixer in A-Frame and trying to trigger a specific action when a particular animation is playing (like Animation 'B' out of A, B, C). Although I'm not well-versed in Javascript, I ha ...

Comparing .innerHTML with createElement() | Exploring setAttribute() versus the Direct method*

Someone mentioned that this approach was not considered "proper", but I didn't pay much attention to it until I encountered a run-time error in IE9. Now, I need help converting the code to utilize object properties instead. What is the reason behind i ...

Nested Angular click events triggering within each other

In my page layout, I have set up the following configuration. https://i.stack.imgur.com/t7Mx4.png When I select the main box of a division, it becomes highlighted, and the related department and teams are updated in the tabs on the right. However, I also ...

Exploring the mechanics of JavaScript and jQuery Ajax: What's the secret to its function

It's common knowledge that browsers are single-threaded, meaning they can only either render the UI or execute Javascript snippets. While web workers offer a way to achieve multi-threading, let's focus on the default behavior. I'm curious a ...

Best practice in TypeScript for handling an Enum with a switch-case to assign a variable

Here's an issue I'm facing: I have a variable called Difficulty that is an Enum. Within a function, I need to set the configuration DifficultyConfig based on the value of Difficulty. The current solution I have in mind seems overly complicated: ...

Platform error: Responses not specified for DIALOGFLOW_CONSOLE

I've been struggling with this issue for almost a day now and I'm at a loss for what else to try. For some reason, Dialogflow Fulfillment in Dialogflow ES is refusing to make any HTTP calls. Every time I attempt, I receive the same error message ...

Tips for loading a unique class name on the initial active UI react component

Is there a way to load a class named "Landingpage" to the body tag or main container div only when the first tab/section (Overview page) is active? The tab sections are located in a child component. Any assistance would be appreciated. Click here for more ...

Modal shows full JSON information instead of just a single item

This is a sample of my JSON data. I am looking to showcase the content of the clicked element in a modal window. [{ "id": 1, "companyName": "test", "image": "https://mmelektronik.com.pl/w ...

VueJs Ellipsis Filter: Enhance Your Texts with

Using Vue.JS, I am dynamically injecting text content into a DOM element. <article>{{ movie.summary }}</article> My goal is to implement an auto-ellipsis filter. Essentially, the code would look like this: <article>{{ movie.summary | e ...

Having difficulty grasping the significance of the data received from the API response

Currently, as I am working on my personal Portfolio for a Web Developer course, I have encountered an issue with correctly implementing my API to retrieve information from the database. Previously, I faced no problem when using a .json file, but now, I am ...

Can you demonstrate how to repeatedly increase a running total using a for loop until the loop ends?

Looking at the for loop. The goal is to have the interest calculated based on the latest value variable. Currently, the script generates the same interest rate and value for each year. What I desire is for it to use the previous value variable to determine ...

Comparing AngularJS and Node JS in serving web pages, which is better?

Currently, I'm in the process of learning how to develop a web using angular and node JS. One aspect that I am struggling with is determining where to acquire the URLs for links. After doing some research on stack overflow and various tutorials relate ...

Updating state in React using the spread operator is not allowed

As a newcomer to React, I've been struggling with using the spread operator to push new elements into an array stored in the state. My aim is to create an array with a sequence of different numbers. Here's the code snippet that I've been wor ...

Issues with executing javascript callback functions within Node.js and express

/* Access home page */ router.get('/home/', function(req, res, next) { var code = req.query.code; req.SC.authorize(code, function(err, accessToken) { if ( err ) { throw err; } else { req.session.oauth_token = accessToken ...