Reputation: 63
These are true:
type A = [boolean | string] extends [boolean] | [string] ? true : false // true
type B = [number | boolean] extends [number] | [boolean] ? true : false // true
type C = [1 | string] extends [1] | [string] ? true : false // true
But this is false:
type D = [number | string] extends [number] | [string] ? true : false // false
false
?Upvotes: 6
Views: 139
Reputation: 329418
Before TypeScript 4.0, all of those conditional types evaluated to false
. The issue here has to do with how the compiler treats object types with union-typed properties differently when the union in question is or is not considered the type of a discriminant of a discriminated union.
Since TypeScript 3.2 introduced support for non-unit types as discriminants, a union can be a discriminated union as long as some common member of the union has a property of a literal type (or a union of such types).
The types boolean | string
and number | boolean
are acceptable discriminant properties, since boolean
is shorthand for the union true | false
, each of which is a literal type. 1 | string
is also an acceptable discriminant, since 1
is a literal type. On the other hand, number | string
is not an acceptable discriminant type, since neither number
nor string
is a literal type.
In general, TypeScript does not consider an object type with a union typed property assignable to a union of object types. That is, unions do not in general propagate upward from properties to top-level objects:
const x: { a: RegExp | Date } = { a: Math.random() < 0.5 ? /abc/ : new Date() };
const y: { a: RegExp } | { a: Date } = x; // error!
This behavior can be annoying, especially in situations where the type you're trying to assign to is a discriminated union. So TypeScript 3.5 introduced support for "smarter" union type checking via microsoft/TypeScript#30779 where you can do such assignments as long as the target type is a discriminated union. So the following assignment works as of TypeScript 3.5:
const v: { a: RegExp | "abc" } = { a: Math.random() < 0.5 ? /abc/ : "abc" };
const w: { a: RegExp } | { a: "abc" } = v; // error in TS3.4-, okay in TS3.5+
Since "abc"
is a literal type, then the type of w
is a discriminated union, and therefore the assignment is accepted.
From TypeScript 3.5 through TypeScript 3.9, those conditional types still all evaluated to false
, because this change in assignability was not fully reflected in the type system, such as when you write conditional types. The fix to this, microsoft/TypeScript#39393, was released with TypeScript 4.0. Now the type system also sees that objects-with-union-properties are assignable to appropriate discriminated unions:
type A = [boolean | string] extends [boolean] | [string] ? true : false
// false in TS3.9-, true in TS4.0+
type B = [number | boolean] extends [number] | [boolean] ? true : false
// false in TS3.9-, true in TS4.0+
type C = [1 | string] extends [1] | [string] ? true : false
// false in TS3.9-, true in TS4.0+
type D = [number | string] extends [number] | [string] ? true : false
// false
One-tuples like [X]
are similar to object types like {0: X}
, so [X] | [Y]
is a discriminated union if {0: X} | {0: Y}
is. And since boolean | string
and number | boolean
and 1 | string
are discriminant types, the assignment succeeds. And since number | string
is not a discriminant type, the assignment fails.
Upvotes: 7