Abstract classes in TypeScript

This blog is a part of my TypeScript series, and the previous ones are:

1. Why program in TypeScript
2. Structural vs nominal typing
3. Getting started with TypeScript classes
4. Access modifiers public, private, and protected

If you add the abstract keyword to the class declaration, it can’t be instantiated. An abstract class may include methods that are implemented as well as the abstract ones that are only declared.

And why would you even want to create a class that can’t be instantiated? The reason is that you may want to include some non-implemented methods in a class, but you want to make sure that these methods will be implemented in its subclasses. This concept is easier to grasp while working on a specific task, so let’s consider a coding assignment and see how abstract classes can be used there.

An assignment with abstract classes

A company has employees and contractors. Design the classes to represent workers of this company. Any worker’s object should support the following methods:

constructor(name: string)

changeAddress(newAddress: string)

giveDayOff()

promote(percent: number)

increasePay(percent: number)

In our scenario, to promote means giving one day off and raising the salary by the specified percent. The method increasePay() should raise the yearly salary for employees but increase the hourly rate for contractors. How do you implement methods is irrelevant; a method can just have one console.log() statement.

Let’s work on this assignment. We’ll need to create the classes Employee and Contractor, which should have some common functionality. For example, changing the address and giving a day off should work the same way for contractors and employees, but increasing pay requires different implementation for these categories of workers.

Here’s the plan: we’ll create the abstract class Person with two descendants: Employee and Contractor. The class Person will implement methods changeAddress(), giveDayOff(), and promote(). This class will also include a declaration of the abstract method increasePay(), which will be implemented (differently!) in the subclasses of Person shown next.

abstract class Person {  // 1

constructor(public name: string) { };

changeAddress(newAddress: string ) {  // 2
   console.log(`Changing address to ${newAddress}`);
}

giveDayOff() {  // 2
console.log(`Giving a day off to ${this.name}`);
}

promote(percent: number) {  // 2
   this.giveDayOff();
   this.increasePay(percent);  // 3
}

abstract increasePay(percent: number);  // 4

}

1. Declaring an abstract class
2. Declaring and implementing a method
3. “Invoking” the abstract method
4. Declaring an abstract method

If you don’t want to allow invoking the method giveDayOff() from external scripts, add private to its declaration. If you want to allow invoking giveDayOff() only from the class Person and its descendants, male this method protected.

Note that you are allowed to write a statement that “invokes” the abstract method. Since the class is abstract, it can’t be instantiated, and there is no way that the abstract (unimplemented method) will be actually invoked. If you want to create a descendant of the abstract class that can be instantiated, you must implement all abstract methods of the ancestor. The code in the next listing shows how we implemented the classes Employee and Constructor.

class Employee extends Person {

  increasePay(percent: number) {
    console.log(`Increasing the salary of ${this.name} by ${percent}%`);  // 1
  }
}

class Contractor extends Person {
  increasePay(percent: number) {
    console.log(`Increasing the hourly rate of ${this.name} by ${percent}%`);  // 2
  }
}

1. Implementing the method increasePay() for employees
2. Implementing the method increasePay() for contractors

The next listing shows how can we see our solution in action. We’ll create an array of workers with one employee and one contractor and then iterate through this array invoking the method promote() on each object.

const workers: Person[] = [];  // 1

workers[0] = new Employee('John');
workers[1] = new Contractor('Mary');

workers.forEach(worker => worker.promote(5));  // 2

1. Declaring an array of the superclass type
2. Invoking promote() on each object

The workers array is of type Person, which allows us to store there the instances of descendant objects as well.

Since the descendants of Person don’t declare their own constructors, the constructor of the ancestor will be invoked automatically when we instantiate Employee and Contractor. If any of the descendants declared its own constructor, we’d have to use super() to ensure that the constructor of the Person is invoked.

You can run this code sample in the TypeScript playground, and the browser console will show the following output:

Giving a day off to John
Increasing the salary of John by 5%
Giving a day off to Mary
Increasing the hourly rate of Mary by 5%

The previous code fragment gives an impression that we iterate through the objects of type Person invoking Person.promote(). But realistically, some of the objects can be of type Employee while others are instances of Contractor. The actual type of the object is evaluated only during runtime, which explains why the correct implementation of the increasePay() is invoked on each object. This is an example of polymorphism – a feature that each object-oriented language supports.

Protected constructors

In the previous blog, we declared a private constructor to create a singleton. There’s some use for protected constructors as well. Say you need to declare a class that can’t be instantiated, but its subclasses can. Then, you could declare a protected constructor in the superclass and invoke it using the method super() from the subclass constructor.

This mimics one of the features of abstract classes. But a class with protected constructor wouldn’t let you declare abstract methods unless the class itself is declared as abstract.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s