Description
Problem
Before literal types and union types, it made sense that relational comparison operators and equality comparison operators had the same rules.
Overly-permissive
However, with union types, relational comparisons don't always make sense.
declare let x: string;
declare let y: string | number;
if (x < y) {
// ...
}
Here, it's not clear that you'll get the "correct" behavior if x
and y
are not both string
s, but TypeScript permits this. Issue #5156 covers this in more detail.
Overly-strict
While the current "comparable" relationship is the most lax relationship, it is also too strict. For instance, consider the following example from #10119:
function f(onethree: 1 | 3, two: 2) {
return onethree < two;
}
Under the comparable relationship, TypeScript issues an error because the type 2
is not comparable with 1
nor is it comparable with 3
.
Proposal
I propose a new relational comparison relationship, in which for any types S and T:
- If T is a union type, then S is only relationally comparable if S is relationally comparable with each constituent of T.
- Otherwise, if S is a union type, then S is only relationally comparable to T if each constituent of S is relationally comparable to T.
- Otherwise, if S is string-like, then S is relationally comparable if T is string-like or Any.
- Otherwise, if S is number-like, then S is relationally comparable if T is number-like or Any.
- Otherwise, if S is Any, then S is relationally comparable if T is number-like, string-like, or Any.
To note:
- Boolean and other unmentioned types are never relationally comparable to anything. The rationale is that you probably didn't want to compare a Boolean with a less-than operator.
- Any is not relationally comparable to everything - for instance, you can't compare a Boolean with an Any.
- There should not be a case where a mix of string-like and number-like types are ever relationally comparable.
- Type parameters are currently not covered here (see below).
Open Questions
It's not clear how this behaves with type parameters. For instance, you might propose that we just check the type parameter's constraint here, but that's not enough. If you constrain T
to number | string
, then T
will not be relationally comparable to itself because two distinct values of type T
could each have different types at runtime. See #5156 (comment) for a concrete example.