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.
Table of Contents
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.