2

Let's say we have this example:

class Base<T extends Base<T>> {}
class ClassA extends Base<ClassA> {}
class ClassB extends Base<ClassB> {}
type Condition = ClassA extends ClassB ? true : false;

The base class has one generic argument which basically says, anything derivind from it should template it with it's own type.

Then we have 2 classes that derive from said base.

Lastly I've created a conditional type that checks whether the derived classes extend each other.

To my surprise typescript is telling me that they do, but I think that this condition should be false. ClassA isn't extending ClassB and vice versa. Only ClassA extends Base<ClassA> should return true.

Is this an issue in typescript's conditional types or am I missing something? I came across this issue while building more complex conditional type, that as well was returning unexpected results.

Edit: The generic arguments aren't needed as well. Even this example returns wrong result:

class Base {}
class ClassA extends Base {}
class ClassB extends Base {}
type Condition = ClassA extends ClassB ? true : false;
Maroš Beťko
  • 2,181
  • 2
  • 16
  • 42

1 Answers1

7

Typescript usses structural typing to best emulate the way Javascript duck typing works. This has the advantage of allowing us to model a lot of Javascript scenarios in a staticly type way.

The problem is that you must never forget that when typescript checks any type compaitbility (even in conditional types) it is not checking for nominal inheritance like C# or Java would, it is checking for structural subtyping.

In your example, since all classes are empty, they are structurally equivalent to {}, which means that yes, ClassA does extends ClassB and ClassB extends ClassA since they are all structually the same type.

Add any member to the classes and this goes away, private members ensure maximum incopatibility because another class (or interface) can't just declare redeclare them to mimic subtyping.

class Base {}
class ClassA extends Base { private a: number}
class ClassB extends Base { private b: number}
type Condition = ClassA extends ClassB ? true : false; // false now
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • In my code I'm using `type Id = string` and when I use this type in conditional the extends returns that the types match, independent of the generic `T`. Is that because typescript basically looks at it as just `string`? – Maroš Beťko Mar 21 '19 at 11:34
  • @MarošBeťko If you want to do it with string you are looking for branded types. I have several answers on this https://stackoverflow.com/search?q=user%3A125734+branded+types, This seems the closest to what you want: https://stackoverflow.com/questions/49260143/how-do-you-emulate-nominal-typing-in-typescript/49260286#49260286 – Titian Cernicova-Dragomir Mar 21 '19 at 11:48
  • That helped a lot! Haven't heard about that patter so far. Could you help me with one a bit more complex question? – Maroš Beťko Mar 21 '19 at 15:38
  • @MarošBeťko Sure, shoot, but if it's a longer question it would be best to post it separately, comments are a bad place to put a lot of code. – Titian Cernicova-Dragomir Mar 21 '19 at 15:39
  • I didn't want to write full on question because it would take a while to draft a complete and working example but I'm on it – Maroš Beťko Mar 21 '19 at 15:46
  • Here is the followup question https://stackoverflow.com/questions/55284964/infer-generic-type-from-key-of-object – Maroš Beťko Mar 21 '19 at 16:22