type A = Promise<string> | Promise<number>
type B = Promise<string | number>
const a: A = Math.random() > 0.5 ? Promise.resolve('string') : Promise.resolve(1)
const b: B = Promise.resolve(Math.random() > 0.5 ? 'string' : 1)
class C {
fa(a: A) {
console.log(a)
}
fb(b: B) {
console.log(b)
}
}
const c = new C()
c.fa(a)
c.fb(b)
c.fa(b) /* <-- This produces an error:
Argument of type 'B' is not assignable to parameter of type 'A'.
Type 'Promise<string | number>' is not assignable to type 'Promise<string>'.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.ts(2345)
*/
c.fb(a) // This works
Why does this code produce an error?
P.S. Typescript 4.9.4
CodePudding user response:
As per the error message:
Type 'string | number' is not assignable to type 'string'
It's easier if you think about it in terms of set unions.
A
is the union of (1) the set of all promises that returnstring
and (2) the set of ones that returnnumber
.B
is (3) the set of all promises that might return eitherstring
ornumber
.
One is a subset of the other, they're not the same set.
It's akin to:
- All neurosurgeons in the world all rocket engineers.
- All people that are neurosurgeons and rocket engineers at the same time.
Now if you only care about the job they can do (brain surgery being performed or engineering launchable rockets) you're correct that the possible outputted work of the two sets is the same.
Which is also the case here:
a.then(x => ...) // (parameter) x: string | number
b.then(y => ...) // (parameter) y: string | number
CodePudding user response:
In general it isn't true that F<T> | F<U>
is equivalent to F<T | U>
; it would require that F
is covariant in its type argument and vice versa (see
Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for description of variance). And the compiler doesn't necessarily aggressively reduce one form to the other even when they are equivalent.
In particular, however, Promise<T> | Promise<U>
and Promise<T | U>
can be interchanged safely. So the question is: why doesn't this happen in TypeScript?
It looks like the authoritative answer can be found in microsoft/TypeScript#51293. According to the TS dev team lead:
The proposed reduction rule [equivalence of
Promise<T> | Promise<U>
withPromise<T | U>
] is valid today, but I'm not sure what to make of it.There's really nothing stopping TC39 from, tomorrow, deciding that Promises should have a
.thenAgain
method that runs a fresh invocation of the callback, and at that point the rule thatPromise<T | U> === Promise<T> | Promise<U>
stops being true. Then we'd be in quite a pickle.This particular rule isn't something we've heard other feedback on despite obviously ~everyone interacting with Promises in one way or another, and there are no type checkers which don't reject at least some valid programs, so on net I don't see an action item here.
So the answer seems to be "low demand combined with potential future-proofing problems".
CodePudding user response:
In addition to what has been said, polymorphic callbacks don't work well with Promise<string> | Promise<number>
, so you have a good reason to prefer Promise<string | number>
:
type A = Promise<string> | Promise<number>
type B = Promise<string | number>
declare const a: A;
declare const b:B;
const id = <T>(x: T): T => x;
const ok:B = b.then(id)
const sortOfOk:B = a.then(x => x)
const notOk:B = a.then(id);
// ~~~~~
// Type 'Promise<unknown>' is not assignable to type 'B'.
// Type 'unknown' is not assignable to type 'string | number'.
Despite the return type being Promise<unknown>
, type information wasn't erased, it's just not propagated correctly:
a.then<boolean>(id)
// ~~
// Argument of type '<T>(x: T) => T' is not assignable to
// parameter of type '((value: string) => boolean | PromiseLike<boolean>) &
// ((value: number) => boolean | PromiseLike<boolean>)'.