Polymorphism in typescript

Aug 06, 2019

Polymorphism in its pure definition means to have many shapes. When this is applied to software development, it applies to a wide variety of techniques that enable us to use a variety of different objects or methods to perform a task. In pure OOP languages, polymorphism refers to the use of method overloading, operator overloading, and method overriding.

Method overloading

Method overloading is the idea of providing multiple methods with different call signatures but the same name , TypeScript Basics, so this will be a short review of that. As we saw earlier, we are able to provide multiple call signatures for a single function as long as the parameters all share a common base type. The ICommunicator interface shown in the following code provides an example of this:

interface ICommunicator {
    speak(message: string);
    speak(message: number);
    sipeak(message: boolean);
    speak(message: any);
}
class Communicator implements ICommunicator {
    constructor() {
    }
    speak(message: string);
    speak(message: number);
     speak(message: boolean);
    speak(message: any) {
        alert(message);
    }
}

In this example, we defined a class that is used for communicating messages. The message can be of any of the types defined in the method list, and they all share a common base type that must be used in the final method signature, TypeScript Basics. In traditional object-oriented languages, we would be able to define multiple implementations of the speak method, however since the end result is still plain JavaScript, we can only provide the single method block. Through the use of conditional statements, we could provide different execution paths for each of the different types used in the parameter list, as shown in the following example:

class Communicator implements ICommunicator {
    constructor() {
    }
    speak(message: string);
    speak(message: number);
    speak(message: boolean);
    speak(message: any) {
        if (typeof message === "string") {
            alert(message);
        } else if (typeof message === "number") {
            alert("The number provided was: " + message);
        } else if (typeof message === "boolean") {
            alert("The boolean value was: " + message);
        } else {
            alert(message);
        }
    }
}

Operator overloading

JavaScript is not inherently an object-oriented language and this limits what we can do somewhat, so we are still restricted in some respects compared to languages such as C#. There is no way to perform operator overloading in JavaScript so we are forced to implement specific methods on our classes to implement the functionality we want. In the following example, we defined a Vector type that will allow us to perform vector math operations:

interface IVector {
    x: number;
    y: number;
}
class Vector implements IVector {
    public x: number = 0;
    public y: number = 0;
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
}
We know that performing vector addition is fairly simple so it would be very nice if we could use the + operator to add two instances of this class together.
var v1 = new Vector(2, 5);
var v2 = new Vector(4, 3);
var v3 = v1 + v2;
alert(v3);

However, since operator overloading is not available in JavaScript

Behind the scenes, the JavaScript engine calls the toString method associated with the objects and then concatenates them into a single value. This has the potential to cause unexpected results in our applications. To perform the functionality we want, we must explicitly implement methods to handle the desired functionality.

public add(v: IVector): IVector {
        var newVector = new Vector();
        newVector.x = this.x + v.x;
        newVector.y = this.y + v.y;
        return newVector;
    }
    public static add(v1: IVector, v2: IVector): IVector {
        return v1.add(v2);
    }

After adding these methods to our class definition and performing a method override of toString, we can simply change our application code to call the new add method and we will receive the expected result:

var v3 = Vector.add(v1, v2);
alert("X: " + v3.x + ", Y: " + v3.y);

Method overrides

Method overrides are a way to replace functionality that is inherited from super-classes. Method overrides must have the same call signature as the method that is being overridden. Take a look at the following example:

interface IEmployee {
    name: string;
    email: string;
    work(tasks: string[]);
}
class Employee implements IEmployee {
    constructor(public name: string, public email: string) {
    }
    public work(tasks: string[]) {
        for (var i = 0; i < tasks.length; i++) {
            //perform task
        }
    }
}
interface IManager extends IEmployee {
    employees: IEmployee[];
}
class Manager extends Employee implements IManager {
    public employees: IEmployee[] = [];
    constructor(name: string, email: string, employees: IEmployee[]) {
        super(name, email);
        this.employees = employees;
    }
    public work(tasks: string[]) {
        for (var i = 0; i < this.employees.length; i++) {
            for (var j = 0; j < tasks.length; j++) {
                this.employees[i].work([tasks[j]]);
            }
        }
         }
}

In this example, you can see that we have two classes. First, a generic Employee class that represents the base class for any employee within an organization. This employee has a few properties such as a name and an e-mail address to contact them with. Then, second, there is a very simple method that allows the employee to be passed a list of tasks, and this task list will be executed sequentially until all of the tasks are complete. The next class is the Manager class, which is a subclass of Employee. The manager has all of the same functions as a normal employee, they just also have a list of employees that they are responsible for. The manager is handed a list of tasks, and rather than performing each one explicitly, it will delegate each task to each of its employees for execution. If we were to add or remove parameters from the Work method, then the call signatures would be different between the subclass and superclass and would break the Liskov substitution principle. TypeScript will not stop you from changing the method signature, however, it will make your code harder to manage.

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