Typescript Polymorphism with examples

In this post, learn the basic guide to the Polymorphism concept in typescript. You can also check Typescript final keyword

What is Polymorphism in TypeScript?

Polymorphism is one of the core concepts of Object-Oriented Programming (OOP), alongside other fundamental concepts such as Classes,Interfaces, Encapsulation🔗 and Abstract classes.

Polymorphism refers to the ability of a function or method in a class to take on many forms.

TypeScript supports the polymorphism concept in various ways, including:

  • Function overloading
  • Function overriding
  • Interfaces

TypeScript Method Overriding

Method overriding allows the child class to provide an implementation of a function or method that is already provided by the superclass or parent class.

The subclass will have access to all public methods and properties of the superclass.

  • Rules:

    • A function declared in the child class must have the same name as the parent class.
    • The parameters of a function in parent and child classes are the same.
    • Child classes extend the parent class or follow inheritance.
  • Typescript method overriding Example

    In this example, we have declared Parent and Child classes. The Parent class is extended by the Child class. These two classes have the same method name, processName().

    Here, processName() is overridden in the child class and providing an implementation.

    class Parent {
      name: string;
      constructor(name: string) {
        this.name = name;
      }
      processName() {
        console.log("processName method in parent class", this.name);
      }
    }
    class Child extends Parent {
      constructor(name: string) {
        super(name);
      }
      processName() {
        console.log("processName method in Child class", this.name);
      }
    }
    let myObject = new Child("Kiran");
    myObject.processName();
    let myObject1 = new Parent("John");
    myObject1.processName();

    Output:

    processName method in the Child class
    processName method in the parent class

Method or Function Overloading

Function overloading refers to having one method with multiple signatures. Most programming languages support this concept, but TypeScript handles it differently.

For instance, in TypeScript, we can define multiple methods with different arguments, as shown below.

class MyClass {
  constructor() {}
  myMethod() {
    console.log("Method with no arguments");
  }
  myMethod(name: string) {
    console.log("Method with a string argument");
  }
}
let myClass = new MyClass();
console.log(myClass.myMethod());

However, this code does not compile and gives an error: Duplicate function implementation.

The reason is that TypeScript code is ultimately converted to JavaScript, which does not support overloading syntax.

So, how does TypeScript support function overloading?

While JavaScript lacks built-in support for overloading, TypeScript offers a workaround using any data type or optional parameter syntax.

For more details, you can refer to my other blog post on Function overload in typescript

Typescript Polymorphism: Interfaces or Duck Typing

For example, let’s consider an interface named DatabaseConnection, which consists of only one abstract method - connect(), used for connecting to the database. This method returns a value greater than 0 if successful, otherwise zero or -1 for failed cases.

interface DatabaseConnection {
  connect(): number;
}

Now, let’s provide implementations for the DatabaseConnection interface. First, let’s implement it for MySQL database - MySQLConnection.

The MySQLConnection has connect() method implementation for MySQL Database

export class MySQLConnection implements DatabaseConnection {
  public connect(): number {
    console.log("Opening HTTP Connection");
    console.log("Connection Established");
    return 1;
  }
}

Similarly, let’s implement the connection mechanism for MongoDB - MongoDBConnection, which has connect() method implementation for MongoDB Database

export class MongoDBConnection implements DatabaseConnection {
  public connect(): number {
    console.log("Opening HTTP Connection");
    console.log("Connection Established");
    return 1;
  }
}

Next, we’ll write client code that connects to the database.

We create a class called DatabaseTest which is provided a constructor by injecting DatabaseConnection.

This exemplifies Constructor Injection using interfaces, where we pass the interface rather than its implementation.

The client doesn’t need to concern itself with connections to specific databases like MySQL or MongoDB. Passing the interface adds flexibility and loose coupling between components, facilitating unit testing and allowing for runtime determination of the database implementation, hence referred to as polymorphism interface.

export class DatabaseTest {
  constructor(private dbConnection: DatabaseConnection) {
    dbConnection.connect();
  }
}

If we were to inject the implementation class MongoDBConnection into the constructor, we’d have a tightly coupled system that cannot be extended for future implementations.