Greetings! In today's lesson, we'll unravel the concept of polymorphism in TypeScript's Object-Oriented Programming (OOP). Grasping polymorphism enables us to use a single entity (a method, class, or interface) to represent different types in various scenarios. With TypeScript's static typing, we can efficiently manage polymorphic behaviors and enforce type safety for more robust applications. Let's proceed.
Polymorphism, a pillar of OOP, allows one object to embody multiple forms. Visualize a button in software; depending on its type (for instance, a submit button or a radio button), the action resulting from pressing it varies. This dynamic encapsulates the spirit of polymorphism!
Polymorphism in OOP can generally be categorized into two types:
- Static Polymorphism (Compile-time): This occurs when the method to call is determined at compile-time, typically through method overloading (having multiple methods with the same name but different parameters). Although TypeScript doesn’t support traditional method overloading as in some other languages, we can achieve similar functionality through function overloading.
- Dynamic Polymorphism (Runtime): This is achieved through method overriding, where a subclass provides a specific implementation of a method defined in its superclass or interface. In TypeScript, this is commonly implemented using interfaces or abstract classes.
Let's observe polymorphism in action within a simple application involving shapes. The base Shape
class has an area
method, which calculates the area for shapes. This method is uniquely implemented in the subclasses Rectangle
and Circle
.
TypeScript1abstract class Shape { 2 abstract area(): number; 3} 4 5class Rectangle extends Shape { 6 private length: number; 7 private width: number; 8 9 constructor(length: number, width: number) { 10 super(); 11 this.length = length; 12 this.width = width; 13 } 14 15 area(): number { // calculate rectangle area 16 return this.length * this.width; 17 } 18} 19 20class Circle extends Shape { 21 private radius: number; 22 23 constructor(radius: number) { 24 super(); 25 this.radius = radius; 26 } 27 28 area(): number { // calculate circle area 29 return 3.14 * this.radius * this.radius; 30 } 31} 32 33const rectangle = new Rectangle(2, 3); 34console.log(rectangle.area()); // Prints: 6 35 36const circle = new Circle(5); 37console.log(circle.area()); // Prints: 78.5
Here, polymorphism shines as the area
function takes on multiple forms and behaves differently depending on whether it's part of a Rectangle
or a Circle
.
TypeScript supports dynamic polymorphism through method overriding within subclasses. By utilizing interfaces and abstract classes, we can define polymorphic behaviors efficiently.
For instance, TypeScript allows achieving polymorphism by defining an interface or abstract class and implementing it in multiple classes:
TypeScript1interface Area { 2 area(): number; 3} 4 5class Triangle implements Area { 6 private base: number; 7 private height: number; 8 9 constructor(base: number, height: number) { 10 this.base = base; 11 this.height = height; 12 } 13 14 area(): number { 15 return (this.base * this.height) / 2; 16 } 17} 18 19const triangle = new Triangle(4, 5); 20console.log(triangle.area()); // Prints: 10
In TypeScript, static polymorphism can also be simulated using function overloading. While TypeScript doesn't support classic method overloading, you can overload functions by declaring multiple signatures and implementing logic within:
TypeScript1class Calculator { 2 add(a: number, b: number): number; 3 add(a: string, b: string): string; 4 add(a: any, b: any): any { 5 if (typeof a === 'number' && typeof b === 'number') { 6 return a + b; 7 } 8 if (typeof a === 'string' && typeof b === 'string') { 9 return a + b; 10 } 11 return null; 12 } 13} 14 15const calculator = new Calculator(); 16console.log(calculator.add(1, 2)); // Prints: 3 17console.log(calculator.add('Hello, ', 'World!')); // Prints: Hello, World!
Abstract classes and interfaces in TypeScript both define structures for other classes to implement, but they serve different purposes:
-
Abstract Class: An abstract class can contain both implemented methods and abstract methods (methods without a body). It provides the possibility to include shared code or common behavior across subclasses. However, an abstract class cannot be instantiated directly, and a class can only extend one abstract class. Use an abstract class when you want to provide default behavior for subclasses.
-
Interface: An interface defines a purely structural contract, specifying the shape of an object without included logic or method implementations. Unlike abstract classes, interfaces do not provide any implementation details. A class can implement multiple interfaces, making interfaces a flexible option for enforcing a specific structure across different classes without dictating any behavior.
Great job! We've now learned about polymorphism in TypeScript, observed its implementation through interfaces, abstract classes, and method overloading, and discovered its applications. Prepare for hands-on practice tasks. Apply what you've learned and explore TypeScript's capabilities further, focusing on type annotations and interface use in polymorphism. Happy coding!