简体   繁体   中英

TypeScript union of strings not assignable to union of tuples in function

The behaviour of these two examples should be identical, yet the second one errors. Why?

// Example 1:
const a: 'x' | 'y' = 'x'; 
const b: ['x'] | ['y'] = [a]; // ok

// Example 2:
function fn(a: 'x' | 'y') {
  const b: ['x'] | ['y'] = [a];
  //    ^
  // Type '["x" | "y"]' is not assignable to type '["x"] | ["y"]'.
  //   Type '["x" | "y"]' is not assignable to type '["x"]'.
  //     Type '"x" | "y"' is not assignable to type '"x"'.
  //       Type '"y"' is not assignable to type '"x"'.
}

You can try it on the playground .

UPDATE: 2019-05-30 the release of TypeScript 3.5 introduces smarter union type checking which fixes this for object types (like {a: "x"} | {a: "y"} , but doesn't seem to do anything to tuple types (like ["x"] | ["y"] ). Not sure if that's intentional or not.


In "Example 1", the fact that a is initialized to "x" makes a big difference. The control flow analysis narrows the type of a down to just "x" despite your annotation as "x" | "y" "x" | "y" :

let a: "x" | "y" = "x";
console.log(a === "y"); // error!
// This condition will always return 'false' 
// since the types '"x"' and '"y"' have no overlap.

So then of course in this case [a] will match ["x"] | ["y"] ["x"] | ["y"] , since [a] is known by the compiler to be of type ["x"] .


Therefore, Example 1 only succeeds coincidentally. In general, this fails. The compiler does not generally see [A] | [B] [A] | [B] as equivalent to [A | B] [A | B] . The former is seen as a strictly narrower type than the latter.

type Extends<T, U extends T> = true;
type OkayTup = Extends<[string | number], [string] | [number]>; 
type NotOkayTup = Extends<[string] | [number], [string | number]>; // error!

This may be surprising, since in fact every value of type [A | B] [A | B] should be assignable to type [A] | [B] [A] | [B] . This same surprise happens when you look at the analogous property-bag version:

type OkayObj = Extends<{a: string | number}, {a: string} | {a: number}>;
type NotOkayObj = Extends<{a: string} | {a: number}, {a: string | number}>; // error!

Again, {a: A} | {a: B} {a: A} | {a: B} is seen to be a strictly narrower type than {a: A | B} {a: A | B} , despite the fact that you'd be hard pressed to come up with a value of the latter type that wasn't assignable to the former.

So, what's going on here? Well, it seems that this is either intentional or a design limitation of TypeScript. The Word of Language Architect says:

For your example to type check with no errors we would have to consider types of the form { x: "foo" | "bar" } { x: "foo" | "bar" } to be equivalent to { x: "foo" } | { x: "bar" } { x: "foo" } | { x: "bar" } . But this sort of equivalence only holds for types with a single property and isn't true in the general case. For example, it wouldn't be correct to consider { x: "foo" | "bar", y: string | number } { x: "foo" | "bar", y: string | number } { x: "foo" | "bar", y: string | number } to be equivalent to { x: "foo", y: string } | { x: "bar", y: number } { x: "foo", y: string } | { x: "bar", y: number } because the first form allows all four combinations whereas the second form only allows two specific ones.

(Note: the equivalence holds in slightly more cases than mentioned above... it only applies where the properties that are different in each union constituent take on all possible values of the union in the single-property case. So, {x: string | number, y: boolean, z: string} is equivalent to {x: string, y: true, z: string} | {x: string, y: false, z: string} | {x: number, y: true, z: string} | {x: number, y: false, z: string} )

I'd say this is a design limitation... detecting the relatively rare cases where property unions can be collapsed/expanded would be very expensive, and it's just not worth it to implement.


In practice, if you find yourself faced with a union property merge thingy that the compiler doesn't verify but that you know is safe, demonstrate your superior intellect and assert your way out of it:

function fn(a: 'x' | 'y') {
  const b = [a] as ['x'] | ['y'] // I'm smarter than the compiler 🤓 
}

Okay, hope that helps; good luck!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM