Friday, 2 March 2018

TypeScript Inheritance Oddities

In addition to Structural Typing, TypeScript provides some other surprising features (that indeed make sense in part thanks to this Structural Typing).

In "conventional" languages (C#, Java) inheritance is based on this idea:

  • Classes extend classes
  • Classes implement interfaces
  • Interfaces extend interfaces

In Java we have the extends and implements keywords, in C# it's just a matter of vocabulary (we just use ":" for inheritance, both for extending or implementing).

TypeScript comes with 2 surprising features.

  • Classes can implement classes
  • Interfaces can extend classes

I think the idea is that a class can be used as an interface, so it seems to make sense that another interface can extend it, and a class can implement it.

Notice an important detail, while a class can extend only 1 class (so it follows the JavaScript logic of "class to class" single inheritance), an interface can extend multiple classes.

I've checked some stackoverflow discussions and the TypeScript documentation dealing with this topic, but I'll write down here my personal vision on how these feature can be useful.

A class implementing another class (rather than extending it)
As the class implements rather than extends, it's not inheriting the method implementations from the parent, so it will have to implement all those methods on its own (so the parent class is behaving like an interface for you). If you are going to inherit from another class but planning to implement all the methods, using implements rather than extends has a semantic value, it makes it clear that you are not reusing anything from the parent class. In a way this is a patch, cause the right design would have been having defined the interface from the beginning and having both classes implementing it.

An interface extending a class
The interface will inherit the method declarations, but not the implementation. When extending a single class, this again feels a bit like a patch. Let's say you want to define new classes that have the same contract as the "parent class" but that need to implement everything. I's useful then to declare this interface "in between" that inherits from that parent class, it saves you the keystrokes of putting those (empty) method declarations in the interface. This is similar to the first case, and again the good design would have been having defined the interface since the beginning, with all the classes implementing it.
On the other side, if extending several classes, this technique is an alternative to Intersection Types. I mean, these 2 codes would be equivalent:

class Person{
 talk(){}
}

class Animal{
 growl(){}
}

function doSomething (item: Person & Animal){

}

//or I could do:
//this is fine, an interface can extend multiple interfaces
interface IPersonAnimal extends Person, Animal
{}

function doSomething2(item: IPersonAnimal){

}

let perAn = {
 talk(){},
 growl(){}
};

doSomething(perAn);

doSomething2(perAn);

Notice that other odd extend/implement combinations like a class extending an interface, an interface implementing another interface, or an interface implementing a class are not allowed.

No comments:

Post a Comment