I am not an expert on TypeScript (just taking the course), but I believe the biggest difference between Type and Class is that Type does not allow you to have an implementation or default values (it just tells you the structure of the variable, nothing more). Also, classes can implement Interfaces (which I do not believe is possible with Types).
In practice, it looks like you should use a Type when you want a simple value class. For example:
type Point = {
readonly x: number,
readonly y: number,
}
Interfaces are perfect when you just want to declare the functions that a type should have:
interface Drawable {
draw(): void
}
class Circle implements Drawable {
...
draw(): void {
// code that draws the circle
}
}
Classes should be used whenever you need to share implementation (for example, you need all Circles to have the same draw()
method as shown above).
As for the issue of Composition vs Inheritance, here is an example:
class Pushable {
push(): void {
// details omitted
}
}
class InheritedPushable extends Pushable {
// details omitted
}
class ComposedWithPushable {
pushable: Pushable
constructor(pushable: Pushable) {
this.pushable = pushable;
}
// details omitted, but can use pushable.push() when needed
}
In general, you should only use inheritance when the two classes have an “is-a” relationship (in this case, InheritedPushable
is a Pushable
). Programmers are often tempted to treat something like this even though the classes do not have an “is-a” relationship leading to degenerate inheritance graphs.
When you just need behavior, you can compose the two classes so that the new class which needs the behavior has the other class inside. In our example, a ComposedWithPushable
is not a Pushable
but has one so that it can call the push()
method when necessary.
A classic example where programmers are tempted to do the wrong thing is the relationship between a Rectangle
and a Square
. Mathematically, it seems to make sense that a Square
is a special case of a Rectangle
, but see this code for how this can go wrong:
class Rectangle {
constructor(private length: number, private width: number) {}
area(): number {
return this.length * this.width;
}
setWidth(width: number): void {
this.width = width;
}
// other details omitted
}
class Square extends Rectangle {
constructor(side: number) {
super(side, side);
}
}
Did you see how Square
inherited the setWidth
class which allows us to have a square which has sides of different lengths! Instead, we should use composition where a Square
has a Rectangle
and delegates to that rectangle for the area()
:
class Square {
rectangle: Rectangle
constructor(side: number) {
this.rectangle = new Rectangle(side, side);
}
area(): number {
return this.rectangle.area();
}
}