Inheritance in Typescript

javascript Aug 04, 2019

Inheritance is the ability of a class to extend the functionality of an existing class. Inheritance creates a way for multiple objects to share a common core set of code and then extend or modify this as necessary for a specific purpose. Take for example our Shape class, which is very simple with a couple of properties in it, but these properties don't provide specific details as to what shape is actually being represented. In the following code, you can see we have modified the Shape class to be more generic and created a subclass using the extends construct in TypeScript:

interface IShape {
    location: IPoint;
    move(newLocation: IPoint);
}
class Shape implements IShape {
    public location: IPoint = new Point(0, 0);
    constructor() {
    }
    public move(newLocation: IPoint) {
        this.location = newLocation;
    }
}
interface IRectangle extends IShape {
    height: number;
    width: number;
    area(): number;
    resize(height: number, width: number);
}
class Rectangle extends Shape implements IRectangle {
    public height: number = 0;
    public width: number = 0;
    constructor() {
        super();
    }
    public area(): number {
        return this.height * this.width;
    }
}

As you can see, we have a generic interface for the Shape class that implements only the basic functionality needed for a shape object, which is a location on a plane. Then a new interface gives us the properties and methods needed to define a basic rectangle shape. This interface is not explicitly necessary, but having it defined will allow us to extend or replace the Rectangle class easily in code later if we need to. This is followed by the class definition for a rectangle which, like the IRectangle definition, shows the usage of the extends keyword. This tells the compiler that the Rectangle class is a shape and should inherit all of the properties that come with it. Inside the constructor, an extra line must be added that initializes the base class instance.

Tip

The call to the base class constructor must be the first statement inside the subclass constructor otherwise a compile-time error will occur.

Now every Rectangle object created will have not only the height, width, and area members but it will also contain a Location property and the Move method that are defined by the Shape class. This is not a default construct in the language of JavaScript so let's take a quick look at the output provided by the compiler to see how this is implemented:

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var Point = (function () {
    function Point(x, y) {
        if (typeof x === "undefined") { x = 0; }
        if (typeof y === "undefined") { y = 0; }
        this.x = x;
        this.y = y;
    }
       return Point;
})();

var Shape = (function () {
    function Shape() {
        this.location = new Point(0, 0);
    }
    Shape.prototype.move = function (newLocation) {
        this.location = newLocation;
    };
    return Shape;
})();

var Rectangle = (function (_super) {
    __extends(Rectangle, _super);
    function Rectangle() {
        _super.call(this);
        this.height = 0;
        this.width = 0;
    }
    Rectangle.prototype.area = function () {
        return this.height * this.width;
    };
    return Rectangle;
})(Shape);

As you can see, all of our classes are created as modules as discussed previously, but before any of these are generated, the compiler injects a special segment of code to handle extending classes. The TypeScript compiler implements object-oriented principles for us by applying a standard set of well-known patterns—member addition and prototype copying. This code takes in two object types as parameters. The first parameter is the subclass that is in need of all of the base class's properties, and the second is the base class itself. This function then loops through each of the available members on the base class and adds them to the subclass. Then the prototype of the base class is copied onto the subclass's prototype. When we look at the module that is output for the subclass, you can see that the Shape type is passed to the Rectangle's module definition for use in the newly generated function. The first thing that happens in the Rectangle's type definition is that it makes a call to this newly generated extends function and then proceeds to initialize the Rectangle as it normally would. The final piece of this is the call to initialize a new Shape object using the call method and passing in the current object instance. This tells the Shape constructor to perform all of its operations on the current object rather than initializing a new object.

TypeScript, like most object-oriented languages, only allows for single inheritance when defining a class. So, creating two classes that perform different functions and then attempting to merge them with a single subclass is not possible and a compile error will be generated. This is very different from the way interfaces work but it is necessary because it would be impossible to ensure proper functionality in all scenarios where this could occur. These two separate classes could define properties or methods with similar names and call signatures and the subclass would have no way of differentiating between them. If you find yourself in a scenario where this is needed, you should consider refactoring your code.

While multiple inheritance is not possible for a single class, we are able to chain a series of classes together to merge functionality in this manner. In the next example, we create a new interface called IBox to define the new requirements for a box object. This interface will extend the IRectangle interface to merge their definitions. The concrete implementation of IBox will inherit from the Rectangle class we created earlier to bring its methods and properties on to the Box class. This creates an inheritance tree stemming from the basic Shape class we created initially, to the Rectangle object, and finally into the Box class, as you can see in the following example:

interface IBox extends IRectangle {
    depth: number;
}
class Box extends Rectangle implements IBox {
    public depth: number = 0;
    constructor() {
        super();
    }
    public resize(height: number, width: number, depth: number) {
        super.resize(height, width);
        this.depth = depth;
            }
}

An extra property is added to the IBox interface, representing a three-dimensional object and all of the functionality of both Shape and Rectangle is available. The resize method has been modified to take an extra parameter that will allow us to modify the new property. There is a problem with this example though; it is in violation of one of the SOLID principles that we discussed earlier. Attempting to compile this code will result in an incompatible type error because the resize method of Rectangle has a different call signature than Box's implementation of it. To fix this, change the resize method to the following:

public resize(height: number, width: number, depth: number = 0) {
    super.resize(height, width);
    this.depth = depth;
}

This parameter must be optional, otherwise we risk breaking the Liskov Substitution Principle ; however, we want to ensure it has a valid value if it is not provided to the constructor. For this reason, the depth parameter is given an initializer rather than just making it optional: depth?: number. If this parameter was not optional, then we would receive compilation errors if we changed any Rectangle object in our application to a Box, as shown in the following code sample:

//var rect = new Rectangle();
var rect = new Box();
rect.resize(4, 3);

Neeraj Dana

Experienced Software Engineer with a demonstrated history of working in the information technology and services industry. Skilled in Angular, React, React-Native, Vue js, Machine Learning