TypeScript: Different types for rejected/fulfilled Promise

TypeScript 1.8.9

I’m trying to understand how to correctly type a rejected promise. I expect the following code to compile.

const bar: Promise<number> =
    Promise.resolve(1)
        .then(() => Promise.reject<string>(new Error('foo')))

Error:

main.ts(7,7): error TS2322: Type 'Promise<string>' is not assignable to type 'Promise<number>'.
  Type 'string' is not assignable to type 'number'.

Is it possible to have different types for a Promise when it is rejected and fulfilled?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 20 (5 by maintainers)

Commits related to this issue

Most upvoted comments

@mhegazy why was this issue closed?

I was expecting to find a generic type with two type arguments, e.g. Promise<TResolved,TRejected>.

My use-case is a simple wrapper around an XMLHttpRequest, which should resolve as a JSON object on success - on error, it should reject with the actual XMLHttpRequest instance, so the consumer can obtain the status, statusText, any returned headers, and whatever else you might need in order to handle the error.

I agree with @mindplay-dk that Promises are better represented with two type arguments. There is potential for more type safety. When using then the resulting promise error type could be a union of the possible errors.

// if
fn1: (a: A) => Promise<B, Error1>
fn2: (b: B) => Promise<C, Error2>
// then
fn1(a).then(fn2): Promise<C, Error1 | Error2>

And a promise that never rejects could have the type Promise<A, never>.

@heyimalex I do understand that the errors are very dynamic and hard to predict. I still think that defaulting to void and giving the user at least the chance to structure their errors is still valuable (as hard/tedious as it may be to catch + rethrow a specific type). But if I am in the minority here I understand your concerns.

@heyimalex I still really think that there could be some value in having this. Doing Promise<T, U = any> would state that U is dynamic and possibly anything, but if the user is determined enough, they can use a U of their own choice. I’m sure this issue will remain closed, but just wanted to comment again in case you’ve had a change of heart.

Can you guarantee that a promise adheres to that signature? What if there’s an unexpected exception inside your promise handler?

Doesn’t the same apply to any function?

Doesn’t the same apply to any function?

Yes exactly, but you haven’t asserted what types of errors that a function might throw, unlike a Promise. There is no external contract, unlike return type, to determine what might or might not be thrown by a function. If a function only has a code path that throws, it TypeScript will infer never as the return type, but makes no contract about the type of error. My comment just above yours highlights where it all goes pete-tong.

You can chain the promises, where the error type cannot be inferred while the resolution type can be:

function foo(result: any): number {
    if (typeof result !== 'bar') throw new TypeError('Not bar');
    return 1;
}

const result = 'foo';

const p = new Promise<string, Error>((resolve, reject) => {
    if (result === 'foo') {
        resolve(result);
    }
    else {
        reject(new Error('I wanted foo'));
    }
)
.then(foo) // return type of `foo` changes this to Promise<number, Error>
.catch((err) => {
    err; // runtime `TypeError` design time `Error` :-(
});

The Promise<void> type comes from the type definition for Promise.reject:

reject(reason: any): Promise<void>;
reject<T>(reason: any): Promise<T>;

Your original example is using the first overload. Since there is no information available to infer the fulfillment type, it uses void. The second overload allows you to explicitly state the promise type.

Perhaps what you are wanting is for TypeScript to be able to work out that the last promise in the chain is always rejected and therefore the fulfilment value is irrelevant?

It could effectively do this if the Promise.reject type definition was changed to:

reject(reason: any): Promise<any>;

Which basically says " This will never be fulfiled so it’s effectively compatible with any fulfilment value type since such a value will never be produced."

That might be a specific suggestion worth making to the team. I’m not sure if void was chosen over any for some reason overlooked here.

@kitsonk @HeyImAlex I’m not sure I understand. It seems to me that adding a second param is strictly better than having a second param that is an any. The problem of typing unknown runtime exceptions is a general problem, and is distinct from the problem of typing known runtime exceptions.

With default type arguments in the 2.3 Release, couldn’t it be argued that the reject type can be defaulted to its current void, which keeps backward compatibility, and then allow the consumers to specify a second reject type argument?

Can you guarantee that a promise adheres to that signature? What if there’s an unexpected exception inside your promise handler?

I just came across this. How should I learn the correct typings for such APIs? Just read the declaration files, or is there are more reader-friendly format published somewhere?

Can this be reopened? I think that disallowing the user to specify the error value is very counterproductive, and with the new release, there should be no breaking changes.

The whole point of TypeScript is to allow static typing, and this is forcing 50% of the promise’s type information to be implicit/dynamic. Considering how widespread Promises are, I think this is worth considering.

The only times that exceptions are typed is in catch blocks and rejected promise handlers, so it’s not really a general problem. Even if you know the type of a rejected promise is T, because of the possibility of an unknown exception it will always actually be T | any, which is really just any. Unless you can statically tell whether an exception can occur, you can’t tell what the value passed to the rejected promise handler will be and so typing it with T would not be type safe. There’s no way around it.

Admittedly that’s a pretty academic argument, and having typed rejections would be awesome in terms of self documenting promise-based apis.

The T in Promise<T> refers to the type of the fulfilled value. There is no generic type for the rejection reason. It’s type is hardcoded to any in the type definitions I’ve seen. E.g. here’s a snippet from 'es6-promise.d.ts':

declare class Promise<T> implements Thenable<T> {
    ...
    then<U>(onFulfilled?: (value: T) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Promise<U>;
    ...

So both T and U refer to fulfillment value types, and the error reason is typed as any.

In your example, since bar has type Promise<number>, then the final promise in the chain (which is the promise assigned to bar) has to match that type. So just change the last bit to Promise.reject<number>(new Error('foo'))) and it compiles.