Strange type Promise <string|number> in error message
How did the typescript compiler come up with the Type Promise <string|number> ...
in the error message below.
Type 'Promise<string | number>' is not assignable to type 'Promise<number>'. Type 'string | number' is not assignable to type 'number'. Type 'string' is not assignable to type 'number'.
The code that generates the error message follows below:
const promise: Promise<number> = Promise.resolve(100).then(n => n.toString());
What I thought would happen
I expected Promise.resolve(100).then(n => n.toString())
to return a Promise<string>
.
When assigning this to const promise: Promise<number>
, I expect typescript to show an error message complaining of a mismatch between Promise<string>
and Promise<number>
Instead, typescript somehow adds a string|number
union and complains that Promise<string|number>
is not assignable to type Promise<number>
CodePudding user response:
The TypeScript call signature for the then()
method of Promise
objects is equivalent to:
interface Promise<T> {
then<R = T, E = never>(
onfulfilled?: ((value: T) => R | PromiseLike<R>) | undefined | null,
onrejected?: ((reason: any) => E | PromiseLike<E>) | undefined | null
): Promise<R | E>;
}
and has two generic type parameters that the compiler needs to infer when you call it. The first type parameter R
corresponds to the "success" return value for the onfulfilled
callback, while the second type parameter E
corresponds to the "error" return value for the onrejected
callback. And then
returns a promise with a payload of the union type R | E
.
Normally when you write something like
const promise = Promise.resolve(100).then(n => n.toString());
without the onrejected
callback, there is no inference site from which to infer E
, and the compiler falls back to the default type argument of the impossible never
type, and since never
is eagerly absorbed by unions, you'd get R | never
, or just Promise<R>
, corresponding to the type returned from the onfulfilled
callback. So in the above call, R
is inferred as string
, and E
fails to infer and falls back to never
, and you get Promise<string>
out, as expected.
But by annotating the type of promise
as Promise<number>
in:
const promise: Promise<number> = Promise.resolve(100).then(n => n.toString());
you are getting different behavior. You are telling the compiler that you expect a Promise<R | E>
to be Promise<number>
, and therefore that E
can be contextually typed. So E
no longer falls back to never
; instead, the compiler tries to choose E
so that Promise<R | E>
matches Promise<number>
... so it chooses number
(which you can verify if you ask IntelliSense for the quick info about the then()
call):
/* (method) Promise<number>.then<string, number>(
onfulfilled?: ((value: number) => string | PromiseLike<string>) | null | undefined,
onrejected?: ((reason: any) => number | PromiseLike<number>) | null | undefined
): Promise<string | number> */
Of course that doesn't work; R
is still inferred as string
from the onfullfilled
callback, and with E
of number
, the return type is Promise<string | number>
which is not assignable to Promise<number>
, and you get the error
// Type 'Promise<string | number>' is not assignable to type 'Promise<number>'.
So that's the explanation.
Backing up: to some extent it's an implementation detail exactly which error message you get when inference fails... in the face of an incompatible type annotation, you know something is going to break, but not necessarily the precise nature of the failure. You didn't expect the compiler to try to use number
as context for the inference, and so were surprised to see Promise<string | number>
instead of Promise<string>
. But since neither of those types would have worked, it's somewhat academic that the error message mentioned one instead of the other.