3

this is not about object {} === {},

found this issues don't know if this is the same

The set of types - AUnion is not an empty set. Two more types (L and R) are extending it. My understanding that these L, R are at least as wide as AUnion, and, therefore I expect that elements in L and R have no empty intersection - namely AUnion.

What part of TypeScript type system I'm missing?

given the TypeScript code

type AUnion = 'a'|'b';
type Proc = <L extends AUnion, R extends AUnion>(l:L, r:R)=>0|1;
declare const proc:Proc;

One can call the proc with two equal arguments

const arg:'a' = 'a';
proc(arg,arg); // this is fine (type-wise) 'a' does extend AUnion

As far as I understand 'a' is equal to 'a'. Their types are the one a and a extend the union type AUnion.

Let's implement the type.


const proc:Proc = (l, r)=>{
  if(l===r){
    return 0;
  }
  return 1;
}

Yet the TypeScript complains

This condition will always return 'false' since the types 'L' and 'R' have no overlap.

Playground Link

What is it I'm missing?

Mx.Wolf
  • 578
  • 5
  • 13
  • Does this answer your question? [Why are two identical objects not equal to each other?](https://stackoverflow.com/questions/11704971/why-are-two-identical-objects-not-equal-to-each-other) – Luca Kiebel Jan 09 '22 at 13:02
  • IMHO - no. compiler is talking about types not values. BTW one can provide the function with two identical values 'a' and JavaScript - do return 0. the question is about typing. I don't understand why compiler consider that they (types of arguments) **never** ever overlap. – Mx.Wolf Jan 09 '22 at 13:17
  • thanks @Dai, does it explain why compiler accepts the call statement proc(arg,arg) as OK. and does not consider the two types extending the same base as possibly having at least some thing in common? – Mx.Wolf Jan 09 '22 at 14:16
  • Also, your generic type parameters probably need to be on the left-hand-side of a `type` expression in order to allow consumers to supply their own type arguments, e.g. `type Proc< L extends AUnion, R extends AUnion> = ( left: L, right: R ) => 0 | 1;` – Dai Jan 09 '22 at 14:20
  • [Playground Link](https://www.typescriptlang.org/play?ssl=16&ssc=1&pln=1&pc=1#code/C4TwDgpgBAggqgOwJYHsFQLxQOQENsA+2ARtgNwBQokUACgE4oDGAPADJQQAewECAJgGdYiVAgA0UAEqcefISORoAfJigAKADYAuNpPrapASgzKADAQCMlJmkHAoYRk0vaGzFnmzivqrFvF6E2UAbwooKCQAMy0MOKCwiIj6CGAAV3p0M0oIgF9wqBT0zKhrClzKClsEe0dnACY3Z098H1I-DU1A4MTImM04jASC5NSMrJyofNHi9DL8oA) shows two out of four implementations required. Then question is - why first implementation is not flagged by TSC, More over - example exactly shows that there is overlap – Mx.Wolf Jan 09 '22 at 15:46
  • @Dai the terms "wide"/"narrow" are most definitely used in TypeScript; "wide" corresponds to supertypes and "narrow" corresponds to subtypes. If `A extends B` (and not `B extends A`) then `A` is narrower than `B` and `B` is wider than `A`. You can find the term "widening"/"wider" in the TS docs all over the place ([example](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#unit-types)). I'm not sure why you think otherwise. – jcalz Jan 09 '22 at 21:44
  • @jcalz You are correct, I was wrong - cheerfully withdrawn :) – Dai Jan 10 '22 at 03:02

1 Answers1

4

According to various GitHub issues, the existence of an error is intended, but the exact wording of the error isn't always appropriate. See microsoft/TypeScript#25642, microsoft/TypeScript#27910, microsoft/TypeScript#41402.

If you have a value x of type X and a value y of type Y, then the rule for whether x === y is allowed is that X and Y must be "comparable". This, roughly, means that either X extends Y or Y extends X are known to be true. If neither of those are known to be true, then x === y will be disallowed.

The fact that both X extends Z and Y extends Z might be known to be true for some third type Z doesn't change that. In fact, the unknown type is the top type in TypeScript, which means that X extends unknown and Y extends unknown will be true for every X and Y. And so the fact that X and Y are constrained to some common supertype really does not imply much about X and Y's suitability to be compared to each other.

In your case, L extends AUnion and R extends AUnion, but neither L extends R nor R extends L are known to be true. So the compiler disallows the comparison.


As for the specific error message wording, it's bad. It is provably incorrect that x === y "will always return false" when X and Y are not "comparable". And when the compiler says "X and Y has no overlap", it certainly sounds like it's saying that "X & Y is never", but of course this isn't necessarily true. There are cases like this which will always return false (e.g., if X is string and Y is number, or if X is {z: string} and Y is {z: number}).

But the emptiness of X & Y really isn't the issue. The compiler will prevent comparison of {x: string} and {y: number} even though the intersection, {x: string; y: number}, is most definitely not empty. So the comparison isn't really prevented because it's impossible for the two values to be the same; it's prevented because comparing two not-directly-related types is often indicative of an error.


If you want to compare two values whose types are considered "incomparable" by the compiler, you can always widen one or both of them to comparable types. In your case you know that both L and R are comparable to AUnion, so you can do something like

const proc: Proc = (l, r) => {
  const _l: AUnion = l;
  const _r: AUnion = r;
  if (_l === _r) { // okay
    return 0;
  }
  return 1;
}

or

const proc: Proc = (l, r) => {
  if (l as AUnion === r) { // okay
    return 0;
  }
  return 1;
}

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • In the context of a type-constraint, how does `X extends Y` work w.r.t. cases where `X` isn't entirely substitutable for `Y` or vice-versa, such as with contravariant collections? (e.g. `Ford[] extends Car[]` is only true for covariant use-cases... so I'm confused). TypeScript does give us `readonly T[]`, but I feel that complicates things even further. – Dai Jan 10 '22 at 03:15
  • Hi, @jcalz. The example with two local variables of type AUnion is helpful. Thanks. You cited the rule for comparison to be allowed. Can you point me to that rule please. – Mx.Wolf Jan 10 '22 at 08:03
  • 1
    @Mx.Wolf Since TS doesn't have an official spec document I can only point to various GitHub comments, user documentation, or possibly lines of the type checker source code. I linked to some of these comments in the answer already (see top paragraph). "This is similar to the check that determines if a type assertion is legal, with some extra special cases.", where the [type assertion](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) rule is (again, only roughly) "upcast or downcast", i.e., "widening or narrowing", i.e., "supertype or subtype". – jcalz Jan 10 '22 at 14:04
  • @Dai TS has bivariant method parameters so `Ford[] extends Car[]` is always true, even though it's unsound. TS will let you "widen" a `Ford[]` to a `Car[]` and then `push` a `Toyota` onto it. TS has unsoundness all over the place; see ["a note on soundness"](https://www.typescriptlang.org/docs/handbook/type-compatibility.html#a-note-on-soundness) and [ms/TS#9825](https://github.com/microsoft/TypeScript/issues/9825). So what exactly is and is not considered assignable is fuzzy and inconsistent, unfortunately. – jcalz Jan 10 '22 at 14:08