Inheritance lets you create new classes based on existing ones, reusing code and creating class hierarchies. Polymorphism allows objects of different classes to be treated as objects of a common base class. These are fundamental object-oriented programming concepts that enable code reuse and flexible design. Let's master inheritance and polymorphism!
Basic Inheritance with extends
Use the extends keyword to create a subclass (child class) that inherits from a base class (parent class).
// Base class (parent/superclass)
class Animal {
constructor(public name: string, public age: number) {}
eat(): void {
console.log(`${this.name} is eating`);
}
sleep(): void {
console.log(`${this.name} is sleeping`);
}
makeSound(): void {
console.log("Some generic sound");
}
}
// Derived class (child/subclass)
class Dog extends Animal {
constructor(name: string, age: number, public breed: string) {
super(name, age); // Call parent constructor
}
// Add new method specific to Dog
fetch(): void {
console.log(`${this.name} is fetching the ball`);
}
// Override parent method
makeSound(): void {
console.log(`${this.name} says: Woof! Woof!`);
}
}
// Another derived class
class Cat extends Animal {
constructor(name: string, age: number, public color: string) {
super(name, age);
}
// Add new method specific to Cat
scratch(): void {
console.log(`${this.name} is scratching`);
}
// Override parent method
makeSound(): void {
console.log(`${this.name} says: Meow!`);
}
}
// Using inherited classes
const dog = new Dog("Max", 3, "Golden Retriever");
const cat = new Cat("Whiskers", 2, "Orange");
// Inherited methods from Animal
dog.eat(); // "Max is eating"
dog.sleep(); // "Max is sleeping"
cat.eat(); // "Whiskers is eating"
cat.sleep(); // "Whiskers is sleeping"
// Overridden methods
dog.makeSound(); // "Max says: Woof! Woof!"
cat.makeSound(); // "Whiskers says: Meow!"
// Class-specific methods
dog.fetch(); // "Max is fetching the ball"
cat.scratch(); // "Whiskers is scratching"
// Inherited properties
console.log(dog.name); // "Max" (from Animal)
console.log(dog.breed); // "Golden Retriever" (from Dog)
console.log(cat.color); // "Orange" (from Cat)💡 The super Keyword
super() calls the parent class constructor and must be called before accessing this in a derived class. Use super.methodName() to call parent class methods.
The super Keyword
Calling Parent Constructor
class Person {
constructor(
public firstName: string,
public lastName: string,
public age: number
) {
console.log("Person constructor called");
}
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
class Employee extends Person {
constructor(
firstName: string,
lastName: string,
age: number,
public employeeId: number,
public department: string
) {
// Must call super() first
super(firstName, lastName, age);
console.log("Employee constructor called");
// Now can use 'this'
console.log(`Employee ID: ${this.employeeId}`);
}
getDetails(): string {
return `${this.getFullName()} - ${this.department} (#${this.employeeId})`;
}
}
const employee = new Employee("Yusuf", "Ibrahim", 25, 1001, "Engineering");
// Logs:
// "Person constructor called"
// "Employee constructor called"
// "Employee ID: 1001"
console.log(employee.getFullName()); // "Yusuf Ibrahim" (inherited)
console.log(employee.getDetails()); // "Yusuf Ibrahim - Engineering (#1001)"
// Default values in parent constructor
class Vehicle {
constructor(
public brand: string,
public year: number = new Date().getFullYear()
) {}
}
class Car extends Vehicle {
constructor(brand: string, public model: string, year?: number) {
super(brand, year); // year is optional, uses default if not provided
}
}
const car1 = new Car("Toyota", "Camry", 2024);
const car2 = new Car("Honda", "Civic"); // Uses current year
console.log(`${car1.brand} ${car1.model} (${car1.year})`);
console.log(`${car2.brand} ${car2.model} (${car2.year})`);Calling Parent Methods
class Rectangle {
constructor(public width: number, public height: number) {}
getArea(): number {
return this.width * this.height;
}
displayInfo(): void {
console.log(`Rectangle: ${this.width}x${this.height}`);
console.log(`Area: ${this.getArea()}`);
}
}
class ColoredRectangle extends Rectangle {
constructor(width: number, height: number, public color: string) {
super(width, height);
}
// Override and extend parent method
displayInfo(): void {
super.displayInfo(); // Call parent implementation
console.log(`Color: ${this.color}`); // Add new functionality
}
// New method
paint(newColor: string): void {
console.log(`Changing color from ${this.color} to ${newColor}`);
this.color = newColor;
}
}
const rect = new ColoredRectangle(10, 20, "red");
rect.displayInfo();
// Rectangle: 10x20
// Area: 200
// Color: red
rect.paint("blue"); // "Changing color from red to blue"
// Calling parent method explicitly
class Counter {
protected count: number = 0;
increment(): void {
this.count++;
console.log(`Count: ${this.count}`);
}
reset(): void {
this.count = 0;
console.log("Counter reset");
}
}
class DoubleCounter extends Counter {
increment(): void {
super.increment(); // Increment once (parent behavior)
super.increment(); // Increment again (double increment)
}
reset(): void {
console.log("Resetting double counter");
super.reset(); // Call parent reset
}
}
const counter = new DoubleCounter();
counter.increment(); // Count: 1, Count: 2
counter.increment(); // Count: 3, Count: 4
counter.reset(); // "Resetting double counter", "Counter reset"Method Overriding
Subclasses can override parent methods to provide their own implementation while maintaining the same method signature.
class Shape {
constructor(public name: string) {}
getArea(): number {
return 0; // Default implementation
}
getPerimeter(): number {
return 0; // Default implementation
}
describe(): void {
console.log(`This is a ${this.name}`);
console.log(`Area: ${this.getArea()}`);
console.log(`Perimeter: ${this.getPerimeter()}`);
}
}
class Circle extends Shape {
constructor(public radius: number) {
super("Circle");
}
// Override getArea
getArea(): number {
return Math.PI * this.radius ** 2;
}
// Override getPerimeter
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Square extends Shape {
constructor(public side: number) {
super("Square");
}
// Override getArea
getArea(): number {
return this.side ** 2;
}
// Override getPerimeter
getPerimeter(): number {
return 4 * this.side;
}
}
class Triangle extends Shape {
constructor(
public base: number,
public height: number,
public side1: number,
public side2: number,
public side3: number
) {
super("Triangle");
}
// Override getArea
getArea(): number {
return (this.base * this.height) / 2;
}
// Override getPerimeter
getPerimeter(): number {
return this.side1 + this.side2 + this.side3;
}
}
// Using polymorphism
const circle = new Circle(5);
const square = new Square(10);
const triangle = new Triangle(8, 6, 8, 6, 10);
circle.describe();
// This is a Circle
// Area: 78.53981633974483
// Perimeter: 31.41592653589793
square.describe();
// This is a Square
// Area: 100
// Perimeter: 40
triangle.describe();
// This is a Triangle
// Area: 24
// Perimeter: 24
// Array of shapes (polymorphism in action)
const shapes: Shape[] = [circle, square, triangle];
shapes.forEach(shape => {
console.log(`${shape.name}: Area = ${shape.getArea().toFixed(2)}`);
});
// Circle: Area = 78.54
// Square: Area = 100.00
// Triangle: Area = 24.00Polymorphism
Polymorphism allows objects of different types to be treated as objects of a common base type. The correct method is called based on the actual object type at runtime.
// Base class
class Payment {
constructor(public amount: number) {}
processPayment(): void {
console.log(`Processing payment of ₦${this.amount}`);
}
getDescription(): string {
return "Generic payment";
}
}
// Derived classes with different behaviors
class CashPayment extends Payment {
processPayment(): void {
console.log(`Processing cash payment of ₦${this.amount}`);
console.log("Cash received and verified");
}
getDescription(): string {
return `Cash payment: ₦${this.amount}`;
}
}
class CardPayment extends Payment {
constructor(
amount: number,
public cardNumber: string,
public cardHolder: string
) {
super(amount);
}
processPayment(): void {
console.log(`Processing card payment of ₦${this.amount}`);
console.log(`Card: ****${this.cardNumber.slice(-4)}`);
console.log(`Holder: ${this.cardHolder}`);
console.log("Card authorized");
}
getDescription(): string {
return `Card payment: ₦${this.amount} (${this.cardHolder})`;
}
}
class BankTransfer extends Payment {
constructor(
amount: number,
public accountNumber: string,
public bankName: string
) {
super(amount);
}
processPayment(): void {
console.log(`Processing bank transfer of ₦${this.amount}`);
console.log(`Bank: ${this.bankName}`);
console.log(`Account: ${this.accountNumber}`);
console.log("Transfer initiated");
}
getDescription(): string {
return `Bank transfer: ₦${this.amount} to ${this.bankName}`;
}
}
class MobileMoneyPayment extends Payment {
constructor(
amount: number,
public phoneNumber: string,
public provider: string
) {
super(amount);
}
processPayment(): void {
console.log(`Processing mobile money payment of ₦${this.amount}`);
console.log(`Provider: ${this.provider}`);
console.log(`Phone: ${this.phoneNumber}`);
console.log("Payment sent to mobile wallet");
}
getDescription(): string {
return `Mobile money: ₦${this.amount} via ${this.provider}`;
}
}
// Function that works with any Payment type (polymorphism)
function processPayments(payments: Payment[]): void {
console.log(`Processing ${payments.length} payments:\n`);
let total = 0;
payments.forEach((payment, index) => {
console.log(`Payment ${index + 1}:`);
console.log(payment.getDescription());
payment.processPayment(); // Calls the correct overridden method
console.log("");
total += payment.amount;
});
console.log(`Total processed: ₦${total}\n`);
}
// Create different payment types
const payments: Payment[] = [
new CashPayment(50000),
new CardPayment(100000, "1234567812345678", "Yusuf Ibrahim"),
new BankTransfer(75000, "0123456789", "GTBank"),
new MobileMoneyPayment(25000, "+234-803-123-4567", "MTN")
];
// Process all payments polymorphically
processPayments(payments);
// Each payment type processes differently based on its actual class!Protected Members in Inheritance
Protected members are accessible in the base class and all derived classes, making them perfect for shared functionality.
class BankAccount {
protected balance: number; // Accessible in subclasses
constructor(
public accountNumber: string,
initialBalance: number
) {
this.balance = initialBalance;
}
public getBalance(): number {
return this.balance;
}
protected logTransaction(type: string, amount: number): void {
console.log(`[${new Date().toISOString()}] ${type}: ₦${amount}`);
}
}
class SavingsAccount extends BankAccount {
private interestRate: number = 0.05;
deposit(amount: number): void {
// Can access protected balance
this.balance += amount;
// Can call protected method
this.logTransaction("Deposit", amount);
console.log(`New balance: ₦${this.balance}`);
}
applyInterest(): void {
const interest = this.balance * this.interestRate;
this.balance += interest;
this.logTransaction("Interest", interest);
console.log(`Interest applied: ₦${interest}`);
}
}
class CheckingAccount extends BankAccount {
private overdraftLimit: number = 50000;
withdraw(amount: number): boolean {
// Can access protected balance
if (this.balance - amount < -this.overdraftLimit) {
console.log("Overdraft limit exceeded");
return false;
}
this.balance -= amount;
this.logTransaction("Withdrawal", amount);
console.log(`Withdrew ₦${amount}. Balance: ₦${this.balance}`);
return true;
}
}
const savings = new SavingsAccount("SAV-001", 100000);
savings.deposit(50000);
// [timestamp] Deposit: ₦50000
// New balance: ₦150000
savings.applyInterest();
// [timestamp] Interest: ₦7500
// Interest applied: ₦7500
const checking = new CheckingAccount("CHK-001", 20000);
checking.withdraw(60000);
// [timestamp] Withdrawal: ₦60000
// Withdrew ₦60000. Balance: ₦-40000
// Cannot access protected members from outside
// console.log(savings.balance); // ❌ Error: protected
// savings.logTransaction("Test", 100); // ❌ Error: protectedMulti-Level Inheritance
Classes can form inheritance chains where a subclass becomes a base class for another subclass.
// Level 1: Base class
class LivingBeing {
constructor(public name: string) {}
breathe(): void {
console.log(`${this.name} is breathing`);
}
}
// Level 2: Inherits from LivingBeing
class Animal extends LivingBeing {
constructor(name: string, public species: string) {
super(name);
}
move(): void {
console.log(`${this.name} is moving`);
}
eat(): void {
console.log(`${this.name} is eating`);
}
}
// Level 3: Inherits from Animal (which inherits from LivingBeing)
class Mammal extends Animal {
constructor(name: string, species: string, public furColor: string) {
super(name, species);
}
giveBirth(): void {
console.log(`${this.name} is giving birth to live young`);
}
feedYoung(): void {
console.log(`${this.name} is feeding young with milk`);
}
}
// Level 4: Inherits from Mammal
class Dog extends Mammal {
constructor(name: string, furColor: string, public breed: string) {
super(name, "Canine", furColor);
}
bark(): void {
console.log(`${this.name} barks: Woof!`);
}
// Override inherited method
move(): void {
console.log(`${this.name} runs on four legs`);
}
}
// Create instance
const dog = new Dog("Max", "Golden", "Retriever");
// Call methods from all levels
dog.breathe(); // From LivingBeing (Level 1)
dog.eat(); // From Animal (Level 2)
dog.giveBirth(); // From Mammal (Level 3)
dog.bark(); // From Dog (Level 4)
dog.move(); // Overridden in Dog
console.log(dog.name); // From LivingBeing
console.log(dog.species); // From Animal
console.log(dog.furColor); // From Mammal
console.log(dog.breed); // From Dog
// Type hierarchy
console.log(dog instanceof Dog); // true
console.log(dog instanceof Mammal); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof LivingBeing); // truePractical Examples
Example 1: Employee Management System
abstract class Employee {
constructor(
public readonly id: number,
public name: string,
protected baseSalary: number
) {}
// Abstract method - must be implemented by subclasses
abstract calculateSalary(): number;
// Concrete method available to all employees
displayInfo(): void {
console.log(`ID: ${this.id}`);
console.log(`Name: ${this.name}`);
console.log(`Salary: ₦${this.calculateSalary().toLocaleString()}`);
}
protected getBaseSalary(): number {
return this.baseSalary;
}
}
class FullTimeEmployee extends Employee {
constructor(
id: number,
name: string,
baseSalary: number,
private benefits: number
) {
super(id, name, baseSalary);
}
calculateSalary(): number {
return this.baseSalary + this.benefits;
}
getBenefits(): number {
return this.benefits;
}
}
class PartTimeEmployee extends Employee {
constructor(
id: number,
name: string,
private hourlyRate: number,
private hoursWorked: number
) {
super(id, name, 0); // No base salary
}
calculateSalary(): number {
return this.hourlyRate * this.hoursWorked;
}
getHoursWorked(): number {
return this.hoursWorked;
}
}
class ContractEmployee extends Employee {
constructor(
id: number,
name: string,
private contractAmount: number,
private projectBonus: number = 0
) {
super(id, name, contractAmount);
}
calculateSalary(): number {
return this.contractAmount + this.projectBonus;
}
addBonus(amount: number): void {
this.projectBonus += amount;
console.log(`Added bonus: ₦${amount}`);
}
}
// Polymorphic function
function processPayroll(employees: Employee[]): void {
console.log("\n=== Monthly Payroll ===\n");
let totalPayroll = 0;
employees.forEach(employee => {
employee.displayInfo();
const salary = employee.calculateSalary();
totalPayroll += salary;
console.log("");
});
console.log(`Total Payroll: ₦${totalPayroll.toLocaleString()}\n`);
}
// Create different employee types
const employees: Employee[] = [
new FullTimeEmployee(1, "Yusuf Ibrahim", 500000, 100000),
new PartTimeEmployee(2, "Amina Hassan", 5000, 80),
new ContractEmployee(3, "Chidi Okafor", 750000, 50000)
];
// Add bonus to contract employee
const contractor = employees[2] as ContractEmployee;
contractor.addBonus(25000);
// Process payroll polymorphically
processPayroll(employees);Example 2: Notification System
abstract class Notification {
constructor(
public readonly id: string,
public recipient: string,
public message: string,
protected timestamp: Date = new Date()
) {}
abstract send(): void;
getInfo(): string {
return `[${this.timestamp.toISOString()}] To: ${this.recipient}`;
}
}
class EmailNotification extends Notification {
constructor(
id: string,
recipient: string,
message: string,
private subject: string,
private from: string = "noreply@example.com"
) {
super(id, recipient, message);
}
send(): void {
console.log(`\nSending Email:`);
console.log(`From: ${this.from}`);
console.log(`To: ${this.recipient}`);
console.log(`Subject: ${this.subject}`);
console.log(`Message: ${this.message}`);
console.log("Email sent successfully ✓");
}
}
class SMSNotification extends Notification {
constructor(
id: string,
recipient: string, // Phone number
message: string,
private provider: string = "MTN"
) {
super(id, recipient, message);
}
send(): void {
console.log(`\nSending SMS:`);
console.log(`To: ${this.recipient}`);
console.log(`Provider: ${this.provider}`);
console.log(`Message: ${this.message}`);
console.log("SMS sent successfully ✓");
}
}
class PushNotification extends Notification {
constructor(
id: string,
recipient: string, // Device ID
message: string,
private title: string,
private icon?: string
) {
super(id, recipient, message);
}
send(): void {
console.log(`\nSending Push Notification:`);
console.log(`Device: ${this.recipient}`);
console.log(`Title: ${this.title}`);
console.log(`Message: ${this.message}`);
if (this.icon) {
console.log(`Icon: ${this.icon}`);
}
console.log("Push notification sent successfully ✓");
}
}
class NotificationService {
private notifications: Notification[] = [];
addNotification(notification: Notification): void {
this.notifications.push(notification);
}
sendAll(): void {
console.log(`\n=== Sending ${this.notifications.length} Notifications ===`);
this.notifications.forEach(notification => {
notification.send();
});
console.log(`\n=== All notifications sent ===\n`);
}
sendByType(type: string): void {
const filtered = this.notifications.filter(n => n.constructor.name === type);
console.log(`\n=== Sending ${filtered.length} ${type}s ===`);
filtered.forEach(n => n.send());
}
}
// Create notification service
const service = new NotificationService();
// Add different notification types
service.addNotification(
new EmailNotification(
"email-001",
"yusuf@example.com",
"Your order has been shipped!",
"Order Update"
)
);
service.addNotification(
new SMSNotification(
"sms-001",
"+234-803-123-4567",
"Your OTP is: 123456. Valid for 5 minutes."
)
);
service.addNotification(
new PushNotification(
"push-001",
"device-xyz-789",
"You have a new message from Amina",
"New Message",
"message-icon.png"
)
);
// Send all notifications polymorphically
service.sendAll();
// Or send specific type
// service.sendByType("EmailNotification");Test Your Knowledge
Type Safety Check
Which inheritance usage is correct?
class Animal {
constructor(public name: string) {}
makeSound(): void {
console.log("Some sound");
}
}
// Option A
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
makeSound(): void {
console.log("Woof!");
}
}
// Option B
class Cat extends Animal {
constructor(public name: string, public color: string) {
// Missing super() call
}
makeSound(): void {
console.log("Meow!");
}
}
// Option C
class Bird extends Animal {
constructor(name: string) {
this.name = name; // Setting before super()
super(name);
}
}
// Option D
class Fish extends Animal {
constructor(name: string, public species: string) {
super(name);
console.log(this.name); // Access after super()
}
}Common TypeScript Error
class Vehicle {
constructor(public brand: string, public year: number) {}
displayInfo(): void {
console.log(`${this.brand} (${this.year})`);
}
}
class Car extends Vehicle {
constructor(brand: string, year: number, public model: string) {
// Error: Must call super() before accessing 'this'
this.model = model; // ❌ Error: 'this' used before super()
super(brand, year);
}
}
const car = new Car("Toyota", 2024, "Camry");❌ 'super' must be called before accessing 'this' in the constructor of a derived class.
What's Wrong?
In a derived class (subclass), you must call super() before you can use 'this'. This is because the parent class constructor needs to initialize the object first.
class Vehicle {
constructor(public brand: string, public year: number) {}
displayInfo(): void {
console.log(`${this.brand} (${this.year})`);
}
}
class Car extends Vehicle {
constructor(brand: string, year: number, public model: string) {
// ✅ Call super() first
super(brand, year);
// ✅ Now can access 'this'
this.model = model;
console.log("Car created:", this.brand, this.model);
}
// Override parent method
displayInfo(): void {
super.displayInfo(); // Call parent method
console.log(`Model: ${this.model}`);
}
}
const car = new Car("Toyota", 2024, "Camry");
car.displayInfo();
// Toyota (2024)
// Model: CamrySolution: Call super() first, then access 'this'
Best Practices
- Always call super() first in derived class constructors
- Use protected for shared functionality in class hierarchies
- Override methods to specialize behavior while maintaining interface
- Call super.method() to extend rather than replace parent behavior
- Keep inheritance hierarchies shallow - prefer composition when possible
- Use abstract classes for common base functionality
- Favor polymorphism over type checking with instanceof
- Document inheritance relationships clearly
// ✅ Good: Shallow hierarchy with clear purpose
abstract class Vehicle {
constructor(public brand: string) {}
abstract start(): void;
}
class Car extends Vehicle {
start(): void {
console.log("Car engine started");
}
}
// ✅ Good: Call super() first
class ElectricCar extends Car {
constructor(brand: string, public batteryCapacity: number) {
super(brand); // ✅ First thing in constructor
this.chargeBattery();
}
private chargeBattery(): void {
console.log("Battery charging...");
}
}
// ✅ Good: Extend parent behavior
class SmartCar extends Car {
start(): void {
console.log("Running diagnostics...");
super.start(); // ✅ Call parent implementation
console.log("Smart features activated");
}
}
// ❌ Avoid: Deep inheritance hierarchies
class Level1 {}
class Level2 extends Level1 {}
class Level3 extends Level2 {}
class Level4 extends Level3 {}
class Level5 extends Level4 {} // Too deep!
// ❌ Avoid: Using 'this' before super()
class Bad extends Vehicle {
constructor(brand: string) {
this.brand = brand; // ❌ Error: before super()
super(brand);
}
start(): void {}
}Key Takeaways
- Use
extendsto create inheritance relationships super()must be called before accessingthisin derived classessuper.method()calls parent class methods- Override methods to provide specialized behavior
- Protected members are accessible in derived classes
- Polymorphism allows treating different types through a common interface
- Derived classes inherit all public and protected members
- Method overriding enables runtime polymorphism
- Multi-level inheritance creates class hierarchies
- Inheritance promotes code reuse and extensibility
What's Next?
Excellent work! You've mastered inheritance and polymorphism. Next, we'll explore Abstract Classes and Interfaces—how to create contracts that classes must follow, define abstract base classes that cannot be instantiated, and use the implements keyword. This completes the Classes & OOP category!
Get ready to master abstraction and interfaces!