Home > Software design >  Typescript: Why is Promise<A | B> not assignable to Promise<A> | Promise<B>
Typescript: Why is Promise<A | B> not assignable to Promise<A> | Promise<B>

Time:01-21

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 return string and (2) the set of ones that return number.
  • B is (3) the set of all promises that might return either string or number.

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> with Promise<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 that Promise<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>)'.

playground

  • Related