Promise rejection type.
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.
Looks like duplicate of #6283 and other similar. See for more details https://github.com/microsoft/TypeScript/issues/6283#issuecomment-240804072
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.
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
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.)
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).
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.
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).
Could this proposal solve this issue? https://github.com/microsoft/TypeScript/issues/13219
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.
I'm confused, is this implemented or not? Can't find any info in google.
It's not. The rejection type is always unknown
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.
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.
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.
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.
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.
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
astype 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.