theoolee
theoolee

Reputation: 63

Why doesn't [number | string] extends [number] | [string] in typescript?

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
  1. How does the TypeScript compiler deal with a tuple type in a condition? Does it distribute one tuple into many tuples?
  2. Why is type D false?

Upvotes: 6

Views: 139

Answers (1)

jcalz
jcalz

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.

Playground link to code

Playground link to code v3.9

Playground link to code v3.3

Upvotes: 7

Related Questions