TypeScript | A Practical Guide to Use Extends in TypeScript

With the introduction of ECMAScript 2015, commonly known as ES6, you can implement object-oriented programming as a class-based approach in JavaScript/TypeScript. Before that, JavaScript developers would use the prototype-based approach to implement the object-oriented pattern in their code. Similar to object-oriented languages like Java and C#, now TypeScript developers can use various object-oriented features like interfaces, inheritance, abstraction, and encapsulation as a class-based approach.

The extends keyword in TypeScript is used to implement inheritance, a class-based object-oriented characteristic that lets the child class or the interface acquire the members from their parents. The extends keyword also comes in handy while performing abstraction, which uses inheritance. 

Inheritance is a mechanism where the child classes can access the properties and methods of the parent classes. Meanwhile, abstraction is a technique that hides the detailed code implementation from the user to reduce the complexity and increase efficiency.

The article will serve you as a practical guide on how to use extends keyword while implementing the different object-oriented characteristics like abstraction, inheritance, and encapsulation in TypeScript.

Using extends While Implementing Abstraction

In TypeScript, one can implement abstraction using either the abstract class or interfaces. During abstraction, a contract is created using either of these two methods.

A contract only contains the prototype, which the sub-classes must implement. In simpler terms, the prototypes are created in abstract class or the interface.

These declarations must be implemented by the classes derived from the parent class or the interface. The extends keyword makes the inheritance possible to achieve abstraction while using the abstract class. Likewise, the keyword is also functional when you need to extend an interface by another interface.

Using extends with Abstract Class

You can apply the abstract keyword to both, the class and its members. The extends keyword comes into play while a sub-class inherits the abstract classes. The sub-class implements the abstract members defined in the abstract class.

Creating an Abstract Class

First, let’s create an abstract class Student as follows.

abstract class Student {

    name: string;
    fee: number;

    constructor(name: string, fee: number) {
        this.name = name;
        this.fee = fee;
    }

    public abstract cost(): number;
}

You just created an abstract class Student with properties name and fee. The constructor sets the value to the properties. There is an abstract method cost() that returns a number.

Now, let’s try to instantiate the abstract class and see what happens.

let student = new Student('Subodh',10000)

Oops! You get the following error.

Cannot create an instance of an abstract class.ts(2511)

You just learned something! You cannot instantiate an abstract class. The concept of abstract class is to provide abstraction, that is, to define the prototypes which will be later implemented by sub-classes via inheritance. The extends keyword makes the inheritance possible.

Extending the Abstract Class

Create a sub-class and inherit the abstract class using the extends keyword. You will implement the abstract method in the sub-class.

abstract class Student {

    name: string;
    fee: number;

    constructor(name: string, fee: number) {
        this.name = name;
        this.fee = fee;
    }

    public abstract cost(): number;
}

class DomesticStudent extends Student {

    constructor(name: string, fee: number) {
        super(name, fee)
    }

    cost():number {
        return this.fee;
    }
}

class InternationalStudent extends Student {
    constructor(name: string, fee: number) {
        super(name,fee)
    }
    cost():number {
        return this.fee + 5000;
    }
}

let domesticStudent: Student = new DomesticStudent('jack',10000)
let internationalStudent: Student = new InternationalStudent('jim',10000)

console.log(domesticStudent.cost()) // 10000
console.log(internationalStudent.cost()) // 15000

In the above code snippet, there are two classes, DomesticStudent and InternationalStudent. These classes have inherited the abstract class Student using the extends keyword. Now, these classes can access the methods and properties of the Student class.

Inside each of these classes, there is a constructor that has invoked the super() expression. What super() does is it invokes the constructor of the parent class, which is the Student class’ constructor. The cost() method is an abstract method that is implemented by both of the sub-classes. The method calculates the student’s fee according to the student type in both classes.

Note: The abstract members in the abstract class must be implemented by the classes that inherit the abstract class. Otherwise, an error will occur.

The above example demonstrated the use of the extends keyword in abstraction in TypeScript using the abstract class.

Using extends with Interface

Interface in TypeScript has two purposes. One is to create the contract that the classes must implement. Another is to perform the type declaration, as TypeScript is a strongly-typed language, a distinctive feature from JavaScript.

The extends keyword makes the inheritance between interfaces functional. In the example below, you will learn how to use extends while performing inheritance among classes to achieve abstraction.

Creating the Interfaces

First, create an interface RegularPhone with the following properties and methods. Remember, you only need to set the rules in the interface.

interface RegularPhone {
    quantity: number;
    price: number;  
    cost(): number;
  }

Next, create another interface ImportedPhone that extends the RegularPhone interface as follows.

  interface ImportedPhone extends RegularPhone {
    taxPercent: number;
    taxAmount():number;  
  }

You can see the use of the extends keyword while inheriting an interface by another interface. It means that the ImportedPhone interface can access the methods and properties defined in the RegularPhone interface.

Implementing the Interface

Now, let’s perform the implementation in a class Phone as follows.

 class Phone implements ImportedPhone {
    quantity: number;
    price: number;
    taxPercent: number;

    constructor(quantity: number, price: number, taxPercent: number){
        this.price = price;
        this.quantity = quantity;
        this.taxPercent= taxPercent;
    }

    taxAmount(): number{
        return this.taxPercent / 100 * this.price
    }

    cost(): number{
        if(this.taxAmount()!=0){
            return (this.taxAmount() + this.price) * this.quantity
        }
        
        return this.price*this.quantity
        
    }
  }

let phone: Phone = new Phone(1,1000,15)
let phone1: Phone = new Phone(1,1000,0)
console.log(phone.cost()) //1150
console.log(phone1.cost()) //1000

The Phone class is an implementation of the contract in the ImportedPhone interface that you created above. The foremost thing you should do is implement all the properties and methods from the interface.

The properties quantity and price from the RegularPhone interface and taxPercent from the importedPhone interface are implemented in the class. The constructor sets the values to these properties.

The next step is the implementation of the methods cost() and taxAmount(). The taxAmount() method calculates the tax to be paid for a phone. Similarly, the cost() method calculates the total cost of phones according to the tax.

Finally, the class Phone is instantiated and arguments are passed accordingly.

Thus, you used the extends keyword to implement the inheritance among the interfaces and achieved abstraction.

Extending the Multiple Interfaces

You can achieve the same goal as above(abstraction) by extending multiple interfaces by an interface. This technique is also known as multiple inheritance. For example, create an interface RegularPhone and ImportedPhone as follows.

interface RegularPhone {
    quantity: number;
    price: number;  
  }

interface ImportedPhone {
    taxPercent: number;
  }

Next, create another interface PhoneInterface, that uses the extends keyword to inherit these two interfaces, as shown below.

interface PhoneInterface extends RegularPhone, ImportedPhone{
    cost(): number;
  }

Then, create a concrete class Phone that implements the PhoneInterface. The class must implement all the methods and properties of the interfaces.

class Phone implements PhoneInterface {
    quantity: number;
    price: number;
    taxPercent: number;

    constructor(quantity: number, price: number, taxPercent: number){
        this.price = price;
        this.quantity = quantity;
        this.taxPercent= taxPercent;
    }

    cost(): number{
        if(this.taxPercent != 0){
            let tax = this.taxPercent/100 * this.price 
            return (tax+this.price)*this.quantity
        }
     return this.price*this.quantity   
    }
  }

let phone: Phone = new Phone(1,1000,15)
let phone1: Phone = new Phone(1,1000,0)
console.log(phone.cost()) //1150
console.log(phone1.cost()) //1000

In the concrete class, the constructor is used to set the values to properties, and the cost() method is implemented to calculate the phone cost.

This way, you can achieve abstraction with multiple inheritance using the extends keyword with interface in TypeScript.

Using extends While Interface Inherit Classes

In TypeScript, interfaces can inherit classes using extends. It means that the interface can use the methods and properties of a class to create a prototype but cannot implement those. The concrete class does the implementation.

Interface Extending a Class

Let’s see a quick example of an interface extending a class.

class Vehicle {
    name: string;
    year: number;
}

The class Vehicle contains the properties name and year. Remember that the properties are public.

Next, create an interface that extends the Vehicle class.


interface CarInterface extends Vehicle{
    brandName: string;
    display(): void;
}

The CarInterface inherits the properties from the Vehicle class and has a property brandName and a method display().

To implement the CarInterface, create a concrete class Car as follows.

class Car implements CarInterface {
    brandName: string;
    name: string;
    year: number;

    constructor(brandName: string, name: string, year: number){
        this.brandName = brandName;
        this.name= name;
        this.year= year;

    }

    display():void {
        console.log(`BrandName: ${this.brandName} Name: ${this.name} Year: ${this.year}`)
    }
}

The class has implemented all the methods and properties from the CarInterface. Now, create an object of the class and call the display()method as shown below.

let car:Car = new Car('Toyota','Hilux',2022);
car.display() // BrandName: Toyota Name: Hilux Year: 2022

The method displays the information about the car. The code execution is smooth. You just used extends to inherit the class by an interface.

Interface Extending Class with Private Members (will throw an error)

Now let’s perform a little tweak in the code. Set the access modifier private for the year property in the Vehicle class.

class Vehicle {
    name: string;
    private year: number;
}

Leave the `CarInterface` interface as it is.

interface CarInterface extends Vehicle{
    brandName: string;
    display(): void;
}

With the modification above, make sure you inherit the Vehicle class in the concrete class Car using extends. And do not forget to call the super() expression in the constructor. The Car class looks like this.

class Car extends Vehicle implements CarInterface{
    brandName: string;

    constructor(brandName: string, name: string, year: number){
        super()
        this.brandName = brandName;
        this.name= name;
        this.year= year;

    }

    display():void {
        console.log(`BrandName: ${this.brandName} Name: ${this.name} Year: ${this.year}`)
    }
}

When you try to run this code, it gives the following error.

Property 'year' is private and only accessible within class 'Vehicle'.ts(2341)

The problem is you are trying to access the private property year of the Vehicle class from the Car class. In order to make this code work, you need to change the private access modifier to protected in the Vehicle class.

The key takeaway is that the protected members can be accessed from sub-classes while the private members cannot.

Mistake and Correction During the Implementation of the Interface While Extending Class

There is yet another important point to note about interfaces extending classes. Whenever an interface extends a class with private or public members, then the implementation of the interface can only be done by the class or sub-class from which the interface was extended. Otherwise, the class cannot access those protected or private members.

To validate it, look back to the Car class above (the first example). Though the class is not a sub-class of the Vehicle class, having the public members in the Vehicle class makes those members accessible from the Car class. Thus, the interface implementation is possible in the sub-class.

The Mistake

Let’s examine what happens when the interface is implemented when one of the Vehicle class members is protected.

class Vehicle {
   name: string;
   protected year: number;

   constructor(name: string, year: number) {
      this.name = name;
      this.year = year;
   }
}

interface CarInterface extends Vehicle{
    brandName: string;
    display(): void;
}

class Car implements CarInterface{
    brandName: string;
    name: string;
    year: number;

    constructor(brandName: string, name: string, year: number){
        this.brandName = brandName;
        this.name= name;
        this.year= year;

    }

    display():void {
        console.log(`BrandName: ${this.brandName} Name: ${this.name} Year: ${this.year}`)
    }
}

Notice that the year property in the Vehicle class is protected, and the class Car only implements the CarInterface but does not extend the Vehicle base class.

The following error is encountered.

  Property 'year' is protected but type 'Car' is not a class derived from 'Vehicle'.ts(2420)

The comprehensive error message makes it clear that in order to access the protected member, the Car class should inherit the Vehicle class.

The Correction

The solution can be easily achieved by using the extends keyword in the class as follows.

class Car extends Vehicle implements CarInterface{
    brandName: string;
    name: string;
    year: number;

    constructor(brandName: string, name: string, year: number){
        super()
        this.brandName = brandName;
        this.name= name;
        this.year= year;

    }

    display():void {
        console.log(`BrandName: ${this.brandName} Name: ${this.name} Year: ${this.year}`)
    }
}

Also, do not miss the super() expression invocation as you inherit the base class from the sub-class.

Finally, create an object of the class and call the display() method.

let car:Car = new Car('Toyota','Hilux',2022);
car.display() // BrandName: Toyota Name: Hilux Year: 2022

Conclusion

The article introduced and implemented the class-based object-oriented approach in TypeScript, mainly revolving around the concept of inheritance and abstraction. It demonstrated and depicted the essence of the extends keyword in such object-oriented practices in TypeScript.

You learned the various usages of the extends keyword including performing abstraction using abstract class and interfaces. Similarly, you also learned about inheritance and access modifiers while performing abstraction.

Was this article helpful for your learning process?

Feel free to share your thoughts by replying on Twitter.