Regrettably, TypeScript is limited in its ability to recognize when certain actions should be restricted. The type checker takes some shortcuts to verify constraints, resulting in situations where errors slip through undetected, as discussed in GitHub issues like microsoft/TypeScript#46076.
This deliberate decision contributes to the intentional unsoundness of TypeScript's type system, maintaining usability by avoiding a trade-off between false negatives and false positives or compromising performance for accuracy in compiler checks.
To explore more about TypeScript's complex relationship with soundness, refer to microsoft/TypeScript#9825, along with discussions on Stack Overflow regarding topics like Why are TypeScript arrays covariant? and TypeScript type narrowing seems to make incorrect assumption.
Here's an elaborated version of your code highlighting why an error might be expected:
type FirstExtendsSecond<X extends Y, Y> =
[X] extends [Y] ? true : false; // ideally always true
In theory, FirstExtendsSecond<X, Y>
with restrictions on X
should only exist if X extends Y
holds true. This results in a non-distributive conditional type check where X extends Y
should unequivocally yield true
, right?
Unfortunately, the reality is different, demonstrated by this example:
declare type Type = {
0: [string, number],
1: [number, string],
}
type Test<T extends 0 | 1> =
FirstExtendsSecond<Type[T][1], Type[T][0]>; // no error
type Test0 = Test<0> // unexpected false 😲
type Test1 = Test<1> // unexpected false 😲
The definition of Test
lacks any errors, yet both Test<0>
and Test<1>
resolve to
false</code! Neither <code>string extends number
nor
number extends string
hold, so why does
Test
's formulation not fail?
This scenario can be understood through a comment in this thread within microsoft/TypeScript#46076. In determining compatibility between types, an intentionally unsound rule is applied as follows:
A type S
is related to a type T[K]
if S
is related to C
, where C
represents the base constraint of T[K]
In the context mentioned above,
The type Type[T][1]
relates to
Type[T][0]</code since the former is connected to the base constraint of the latter, which translates to <code>Type[0 | 1][0]</code, or <code>string | number
.
This lack of error is due to successful type checking when substituting T
with its constraint 0 | 1
:
type Test01 = Test<0 | 1> // evaluates as true
While the rule presents unsoundness concerns we've experienced, it serves practical purposes. As explained in a commentary linked to the relevant issue microsoft/TypeScript#27470:
This approach balances compatibility with older versions and avoids overwhelming complexity from generic types.
Although TypeScript has addressed aspects of soundness over time, pushing for changes based on instances like the one presented may likely get dismissed given how adjustments often risk disrupting existing codebases.
Access Playground link for full code demonstration