6

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?
LeoDog896
  • 3,472
  • 1
  • 15
  • 40
theoolee
  • 63
  • 4
  • that's an interesting observation. Although in practice, arrays of length exactly 1 are not very useful. – Garr Godfrey Nov 14 '22 at 06:19
  • @GarrGodfrey On the contrary, one-element tuples are extremely useful in defining TypeScript types. See e.g. [this answer](https://stackoverflow.com/a/74272675/3558960). – Robby Cornelissen Nov 14 '22 at 06:52
  • Looks like it's not just numbers, `let x!: [object] | [string]; let z!: [object | string]; x = z;`, results in `Type '[string | object]' is not assignable to type '[string] | [object]'.` Maybe the first three worked because `1` only consists of one possibility, and `boolean` only consists of two possibilities? – qrsngky Nov 14 '22 at 08:42

1 Answers1

7

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

jcalz
  • 264,269
  • 27
  • 359
  • 360