TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Promise rejection type.

Open RebeccaStevens opened this issue 5 years ago • 11 comments

Search Terms

Promise Reject Type

Suggestion

Add ability to type Promise rejections.

// Current Promise Constructor Implementation:
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;

// Proposed Change:
new <T, E = any>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: E) => void) => void): Promise<T, E>;

Use Cases

When handling promise rejections, the type any isn't very useful. I would be useful to have the rejection have an actual type without the need to cast or type guard.

Examples

Promise.reject<never, string>('hello world')
  .catch(reason => {
    console.error(reason.length); // `reason` is of type string.
  });
class MyError extends Error {
  // ...
}

Promise.reject<never, MyError>(new MyError(/* ... */))
  .catch(reason => {
    // `reason` is of type MyError.
    const info = reason.getMoreInfo();
    // ...
  });

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.

RebeccaStevens avatar Jul 21 '20 00:07 RebeccaStevens

Looks like duplicate of #6283 and other similar. See for more details https://github.com/microsoft/TypeScript/issues/6283#issuecomment-240804072

IllusionMH avatar Jul 21 '20 03:07 IllusionMH

I don't feel like they rejection type always has to be of type any/unknown.

It is obviously possible to detect what the rejection type is when Promise.reject is used. The hard bit would be detecting rejections caused by a throw statement inside an async function. But if we approach it from the perspective of a throw statement just being an alternative type of return statement, it seems possible to me (but maybe quite difficult).

I've been having a bit of a play with this to see what it would be like add support. This is what I've got so far just by playing with type definitions.

RebeccaStevens avatar Jul 28 '20 04:07 RebeccaStevens

p2                                         // p2 is of type Promise<number, string>
  .then((v2) =>  void cosnole.log(v2))     // v2 is of type number
  .catch((e2) => void console.error(e2));  // e2 is actually type string | ReferenceError, not string

IllusionMH avatar Jul 28 '20 09:07 IllusionMH

Why would the type be string | ReferenceError? Isn't TypeScript already be reporting ReferenceError when they happen (2304)? (Or is that a strict mode only thing? If so then this proposal could just be for strict mode.)

playground

RebeccaStevens avatar Jul 28 '20 22:07 RebeccaStevens

Yes, TS will report 2304 in this obvious case however emitted JS code will still have a change to receive both string and ReferenceError. Idea is to show that if there is something between resolve and catch - there are no guarantees that code between them wont throw something else (e.g. failed network request, incorrect normalize function etc.)

Looks like string will be only true for case p2.catch(...) but only if you believe that there are no other errors before resolve/reject (which might not be always true).

IllusionMH avatar Jul 29 '20 00:07 IllusionMH

Could you give a small example?

If myPromise.then(...).catch(...) can't be handled due to this but myPromise.catch(...) could be, imo that's still worth pursuing.

RebeccaStevens avatar Jul 29 '20 01:07 RebeccaStevens

In unsound cases or unknown external code (e.g. computedBoolean) there is always a chance to have behavior that will throw

declare const computedBoolean: (params: any) => boolean; // but in general case might throw
const list = [{id: 111}, {id: 222}];

function firstThreeIds(data: Array<{id: number}>): number[] {
    return [
      data[0].id,
      data[1].id,
      data[2].id // no compile time error, but will throw
    ];
}

const p = new Promise<number[], string>((resolve, reject) => {
  if (computedBoolean(list)) { // can throw before resolve
    resolve(firstThreeIds(list)); // wil throw before resolve
  } else {
    reject("foo");
  }
});


p
  .catch((e) => { /* ... */}); // actually string | TypeError

So it looks like closest thing e can be typed is E | Error in definition (string | Error in current example). And again if computedBoolean won't throw anything else (not sure which case will be encountered more often).

IllusionMH avatar Jul 30 '20 10:07 IllusionMH

Could this proposal solve this issue? https://github.com/microsoft/TypeScript/issues/13219

RebeccaStevens avatar Jul 31 '20 06:07 RebeccaStevens

I just opened #45869. I think maybe the best-case outcome would be implementing #13219 (throws-clause) and this issue, in which case it would become possible to document Promises that have a rejects type of never, i.e. rejection is not possible. (Think return Promise.resolve(1);, etc.)

It would be a lot of work for most projects to migrate to throws-clause compliance but once done, the type checker would be able to reason about exception / rejection paths -- I would love to automate that reasoning, rather than getting bitten by orphaned async-function calls and having to hunt them down to fix on my own.

thw0rted avatar Sep 15 '21 08:09 thw0rted

I'm confused, is this implemented or not? Can't find any info in google.

Bec-k avatar Nov 28 '22 12:11 Bec-k

It's not. The rejection type is always unknown

RebeccaStevens avatar Nov 28 '22 12:11 RebeccaStevens

Just encountered working with reason as an any type. I understand why it doesn't have a specific type, but I guess I'm wondering why it's not instead typed as unknown? There's a similar situation for the error property of ErrorEvent as it's completely non-standard.

In both situations, it seems that the appropriate type should be unknown, which by definition forces the developer into safe type guards. Having it typed as any just takes TS out of the picture altogether.

steverep avatar Aug 08 '23 23:08 steverep

Since this change isn't breaking you could always monkey patch the appropriate .d.ts file with the proposed change. I think it would be nice if this made it into the official typings. There still other changes that would need to be made to the compiler to get a good developer experience. For instance, if you throw inside of a then() block, it would be nice if TS could infer that update the rejection type appropriately. There was a ticket about adding support for tracking the exception types thrown by functions, but it was closed earlier this year, see https://github.com/microsoft/TypeScript/issues/13219#issuecomment-1515037604. I'm guessing this proposal is dead-in-the-water for similar reasons.

kevinbarabash avatar Aug 11 '23 00:08 kevinbarabash

I don't understand the argument that the rejection type can't be guaranteed. Nothing can be guaranteed by typescript at runtime. Don't see why rejection type is different. If I'm telling typescript rejection type is a certain type is because I'm supposed to know what I'm doing. Typescript code is usually full of castings or assuming some stuff is defined, etc.. Don't see why this should be any different.

And yes maybe in the try-catch might be always any, but at least it should be typed when using Promise.catch.

M-jerez avatar Aug 14 '23 01:08 M-jerez

If you read the issues I linked from my previous post (2 years ago!), you'll see some of the reasoning. I think one of the most compelling arguments against allowing manual declaration of a throws or rejects type is that a novice who reads your code is unlikely to realize that you're making an unsafe assertion, along the lines of a typecast.

I'd still really like to see a way to annotate Promises that cannot reject, though. I actually write a lot of async functions with top level try/catch - it's a good pattern to follow when implementing an Express router callback, since the server library will not handle a rejection for you.

thw0rted avatar Aug 14 '23 01:08 thw0rted

I think one of the most compelling arguments against allowing manual declaration of a throws or rejects type is that a novice who reads your code is unlikely to realize that you're making an unsafe assertion, along the lines of a typecast.

In this case is the developer that set the return type the one that needs to guarantee the thrown exception is the correct type. I'm telling typescript I will only reject with that kind of type.

In typescript is perfectly valid to write

function sum(a: number, b: number): number {
  return 'hello world' as any as number;
}

This is totally valid TS although would fail at runtime, don't see why the developer could not explicitly tells typescript he will make sure the rejected value is a certain type. I'm not asking typescript to check thrown types etc. just so the consumers of the promises know the rejected type.

M-jerez avatar Aug 14 '23 14:08 M-jerez

Sorry I wasn't more specific, but I wrote the previous comment from a phone. I was referring to https://github.com/microsoft/TypeScript/issues/45869#issuecomment-920252246 :

it’s really not obvious that there is an assertion. Experienced TS developers are attuned to look out for unsoundness when they see as type assertions; this proposal makes the unsoundness very easy to overlook.

The point was that, if rejection-types were implemented, I could write

async function lookup(key: string): Promise<string, never> {
  try {
    return maybeGetValueEventually(key);
  } catch {
    logThatMightThrow(key);
    return "fallback";
  }
}

but without comprehensive checked exceptions (a feature that has already been rejected) the type-checker couldn't prove that the function never rejects.

In this example, the first generic argument in the returned-value Promise type (string) is type-checked -- if I wrote return 0 in the catch block instead of return "fallback", it would get flagged as an error -- but the second generic argument (never) is an unsafe assertion that I'm making. There's no way for the typechecker to determine if the Promise never actually rejects, but there's no cast or as keyword to act as a red flag. You'd need a pretty deep expertise in TypeScript to actually understand that difference.

I do think this could be handled by a linter rule ("all explicit Promise rejection types require a comment"), and I would still argue that some construct for asserting at its source that a Promise cannot reject is useful enough to merit a certain amount of novice-developer hazard, but I wanted to point out that there is a totally reasonable counterargument here.

thw0rted avatar Aug 14 '23 14:08 thw0rted