-
Book Overview & Buying
-
Table Of Contents
Full-Stack React, TypeScript, and Node - Second Edition
By :
Polymorphism is a bit of an intimidating name (it comes from Greek: poly = many, morph = form), but it's actually quite powerful because it allows us to write code that works with a general type and lets the specific implementation vary at runtime, all while maintaining type safety. The key idea is that the caller doesn't need to know which concrete type it is working with.
Let's look at some code and see how this works. Create a file called polymorphism.ts and add the following code. This is going to be a fair bit of code, so let's go through it in pieces:
interface Animal {
name: string;
runMaxMiles(hours: number): number;
}
First, we've created an interface that shows an Animal type. This type has a name and a method signature of runMaxMiles. Obviously, since this is an interface, our intention is to implement this interface with specific capabilities, so let's do that now:
class Wolf implements Animal {
name: string = "";
runMaxMiles(hours: number): number {
return hours * 45;
}
}
class Cheetah implements Animal {
name: string = "";
runMaxMiles(hours: number): number {
return hours * 75;
}
}
Here, we've created two distinct implementations: Wolf and Cheetah. Each one has a certain distance it can cover within a given time period—in other words, miles per hour. Let's continue by adding the portion of our code that uses our Animal interface polymorphically. The function below selects an animal based on the duration, then calls runMaxMiles through the Animal interface without caring which concrete class is behind it:
const hours = 0.5;
function pickTheBestAnimalToRun(hours: number): number {
let animal: Animal | undefined;
if (hours >= 0.5) {
animal = new Wolf();
animal.name = "wolfie";
} else {
animal = new Cheetah();
animal.name = "cheetos";
}
if (animal instanceof Wolf) {
console.log("This is a wolf");
}
if (animal instanceof Cheetah) {
console.log("This is a cheetah");
}
return animal.runMaxMiles(hours);
}
pickTheBestAnimalToRun(hours);
So, what have we done in our pickTheBestAnimalToRun function? First, we created an animal variable of the Animal type. Then, based on the hours needed, we select either a Wolf or a Cheetah. This selection part is really a factory pattern: picking which concrete class to create. The actual polymorphism happens at the bottom: animal.runMaxMiles(hours) works correctly regardless of whether animal is a Wolf or a Cheetah, because both conform to the Animal interface. If we later added a third class, say Horse implements Animal, this same line would work unchanged. That is the essence of polymorphism: existing code stays closed to modification but open to extension.
The example also includes instanceof checks that print which concrete type was selected. These are here just for demonstration logging; in real code, heavy use of instanceof to branch on concrete types is actually the opposite of polymorphism and usually means the interface should be doing the work instead:

Figure 2.15 – The polymorphism result
As you can see, our code runs and the pickTheBestAnimalToRun function selects the wolf since our hours value was 0.5. The important takeaway is that the call to runMaxMiles worked through the Animal interface without the caller needing to know it was specifically a Wolf.
Note that TypeScript's structural typing, which we covered earlier, means that polymorphism here works by shape, not by name. A class with the right name and runMaxMiles members would be compatible with Animal even without writing implements Animal;. The implements keyword just makes the compiler verify the shape for you at the point of declaration.
Next, let's learn about generics, which provide another form of polymorphism called parametric polymorphism, where a type itself becomes a parameter.
Change the font size
Change margin width
Change background colour