Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Different types for rejected/fulfilled Promise #7588

Closed
OliverJAsh opened this issue Mar 18, 2016 · 20 comments
Closed

Different types for rejected/fulfilled Promise #7588

OliverJAsh opened this issue Mar 18, 2016 · 20 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@OliverJAsh
Copy link
Contributor

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?

@yortus
Copy link
Contributor

yortus commented Mar 19, 2016

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.

@OliverJAsh
Copy link
Contributor Author

Thanks for the reply. For context I am using the built in type definitions for Promise, now provided in TypeScript itself.

In your example, since bar has type Promise, then the final promise in the chain (which is the promise assigned to bar) has to match that type.

I expected the final promise in the chain wouldn't have to match this type if it was a rejected promise.

If you take the type assertion out, it will fail:

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

Also if you throw:

const bar: Promise<number> =
    Promise.resolve(1)
        .then(() => { throw new Error('foo') })
Type 'Promise<void>' is not assignable to type 'Promise<number>'.
  Type 'void' is not assignable to type 'number'.

I can workaround it with the type assertion as you suggested, but it's not ideal.

@yortus
Copy link
Contributor

yortus commented Mar 21, 2016

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.

@mhegazy mhegazy added the Question An issue which isn't directly actionable in code label Mar 28, 2016
@mhegazy mhegazy closed this as completed Mar 28, 2016
@avdd
Copy link

avdd commented Apr 7, 2016

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?

@mindplay-dk
Copy link

@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.

Blizzara added a commit to teekkarispeksi/nappikauppa2 that referenced this issue Sep 1, 2016
- run "npm update"
- clean package.json
- switch deprecated packages to non-deprecated ones
- mainly switch from "tsd" to "typings"
NOTE: node.d.ts and require.d.ts have been manually edited to prevent
Require and NodeRequire from clashing (see e.g.
DefinitelyTyped/DefinitelyTyped#7049 (comment))

- replace "throw err; return null;" with "return Promise.reject(err)",
as typescript and throwing does not work together (see e.g.
microsoft/TypeScript#7588)
- fix some other typing stuff
- fix 'use strict';s to be on top of the files

Please run "rm -r node_modules; npm install" after getting this commit.
Blizzara added a commit to teekkarispeksi/nappikauppa2 that referenced this issue Sep 11, 2016
- run "npm update"
- clean package.json
- switch deprecated packages to non-deprecated ones
- mainly switch from "tsd" to "typings"
NOTE: node.d.ts and require.d.ts have been manually edited to prevent
Require and NodeRequire from clashing (see e.g.
DefinitelyTyped/DefinitelyTyped#7049 (comment))

- replace "throw err; return null;" with "return Promise.reject(err)",
as typescript and throwing does not work together (see e.g.
microsoft/TypeScript#7588)
- fix some other typing stuff
- fix 'use strict';s to be on top of the files

Please run "rm -r node_modules; npm install" after getting this commit.
@paldepind
Copy link

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
Copy link

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

@kitsonk
Copy link
Contributor

kitsonk commented Oct 4, 2016

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` :-(
});

@mindplay-dk
Copy link

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?

@kitsonk
Copy link
Contributor

kitsonk commented Oct 4, 2016

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.

@heyimalex
Copy link

It's unfortunate, and I wish this was possible. If promises didn't trap exceptions things would be different, but unless typescript gets typed exceptions I can't see how it would work.

@bcherny
Copy link

bcherny commented Nov 21, 2016

@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.

@heyimalex
Copy link

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.

@bcherny
Copy link

bcherny commented Nov 21, 2016

That makes sense, thanks for the explanation. So in TS never isn't really the same as a Java throws, since a return type of T | never doesn't really makes sense?

Personally, I like the idea of a more functional way to capture error types, something like a Scala Either or Scalaz/Dotty Effect. I don't think that will ever be widely adopted though, since it would require wrapping existing libs. Though maybe this can be done as a compile-time transform if at some point TSC supports typed errors, error inference, and compiler plugins.

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

I agree.

@craftytrickster
Copy link

craftytrickster commented May 2, 2017

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?

@craftytrickster
Copy link

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.

@heyimalex
Copy link

@craftytrickster The value passed to the reject handler is dynamic. This issue was that it's not possible to do this safely, not that two type arguments are too verbose.

@craftytrickster
Copy link

@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.

@craftytrickster
Copy link

craftytrickster commented Feb 14, 2018

@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.

@mohlsen
Copy link

mohlsen commented Mar 7, 2018

for those stumbling across this, #6283 IMHO gives a better concise answer

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests