Inheritance & Polymorphism
Extend classes to inherit functionality and override methods for specialized behavior in derived classes.
Inheritance & Polymorphism in TypeScript
Master inheritance and polymorphism in TypeScript with free flashcards and spaced repetition practice. This lesson covers class hierarchies, method overriding, abstract classes, interfaces, and runtime polymorphismβessential concepts for building scalable object-oriented applications.
Welcome to Object-Oriented TypeScript π»
Inheritance and polymorphism are two of the most powerful pillars of object-oriented programming. Inheritance allows you to create new classes based on existing ones, promoting code reuse and establishing "is-a" relationships. Polymorphism enables objects of different types to be treated uniformly through a common interface, while still maintaining their unique behaviors. Together, these concepts form the backbone of flexible, maintainable codebases.
In this lesson, you'll learn how to leverage TypeScript's class system to build hierarchical relationships, override methods effectively, work with abstract classes and interfaces, and harness the power of runtime polymorphism. By the end, you'll be able to design elegant class structures that scale with your application's complexity.
Core Concepts π―
Understanding Inheritance πΊ
Inheritance is the mechanism by which one class (the child or derived class) acquires properties and methods from another class (the parent or base class). This creates a hierarchical relationship where specialized classes extend more general ones.
βββββββββββββββββββββββββββββββ
β Animal (Base) β
β + name: string β
β + move(): void β
ββββββββββββ¬βββββββββββββββββββ
β
β extends
β
βββββββ΄βββββββ
β β
ββββββΌββββββ βββββΌβββββββ
β Dog β β Bird β
β + bark() β β + fly() β
ββββββββββββ ββββββββββββ
Key benefits of inheritance:
- Code reuse: Common functionality lives in the base class
- Logical organization: Related classes form natural hierarchies
- Polymorphism: Derived classes can be used wherever base classes are expected
- Maintainability: Changes to shared behavior only need to happen once
π‘ Tip: Use inheritance when there's a clear "is-a" relationship. A Dog is an Animal, a Manager is an Employee.
The extends Keyword π
TypeScript uses the extends keyword to establish inheritance:
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number = 0): void {
console.log(`${this.name} moved ${distance} meters.`);
}
}
class Dog extends Animal {
bark(): void {
console.log(`${this.name} barks: Woof! Woof!`);
}
}
const dog = new Dog("Buddy");
dog.move(10); // Inherited method
dog.bark(); // Own method
Key points:
- The child class inherits all properties and methods from the parent
- Use
protectedfor members that should be accessible to child classes - Use
privatefor members that should only be accessible within the class itself - Use
public(default) for members accessible anywhere
The super Keyword π―
The super keyword serves two crucial purposes:
- Calling the parent constructor: Required when the child class has its own constructor
- Accessing parent methods: Useful when overriding methods but still needing parent functionality
class Animal {
constructor(protected name: string) {}
move(distance: number = 0): void {
console.log(`${this.name} moved ${distance} meters.`);
}
}
class Horse extends Animal {
constructor(name: string, private speed: number) {
super(name); // MUST call parent constructor first!
}
move(distance: number = 45): void {
console.log("Galloping...");
super.move(distance); // Call parent's move method
}
}
β οΈ Common Mistake: Forgetting to call super() in a child class constructor will cause a compile error!
Method Overriding π
Method overriding occurs when a child class provides its own implementation of a method inherited from the parent class. The method signature must match (same name and parameters).
class Shape {
getArea(): number {
return 0; // Default implementation
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
// Override parent's getArea method
getArea(): number {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
π‘ Best Practice: Use the override keyword (TypeScript 4.3+) to make your intention explicit:
class Circle extends Shape {
override getArea(): number {
return Math.PI * this.radius * this.radius;
}
}
This helps catch errors if you accidentally misspell the method name or change the parent's signature.
Understanding Polymorphism π
Polymorphism (Greek: "many forms") is the ability of objects of different types to be treated as instances of a common parent type. At runtime, the correct method implementation is called based on the object's actual type.
function calculateAreas(shapes: Shape[]): void {
shapes.forEach(shape => {
// Polymorphism in action!
// Calls Circle.getArea() or Rectangle.getArea()
// depending on the actual object type
console.log(`Area: ${shape.getArea()}`);
});
}
const shapes: Shape[] = [
new Circle(5),
new Rectangle(4, 6),
new Circle(3)
];
calculateAreas(shapes);
Real-world analogy π: Think of a remote control (interface). You can control different devices (TV, stereo, AC) with the same "power" button, but each device responds differently. That's polymorphism!
π§ Mnemonic: "POLY-MORPH"
Parent type referenceObject decides behavior
Late binding (runtime)
Yields different results
Many forms
One interface
Runtime resolution
Powerful abstraction
Hierarchy enabled
Abstract Classes ποΈ
An abstract class is a base class that cannot be instantiated directly. It may contain abstract methods (methods without implementation) that child classes must implement.
abstract class Shape {
constructor(protected color: string) {}
// Abstract method - no implementation
abstract getArea(): number;
abstract getPerimeter(): number;
// Concrete method - has implementation
describe(): void {
console.log(`A ${this.color} shape with area ${this.getArea()}`);
}
}
class Circle extends Shape {
constructor(color: string, private radius: number) {
super(color);
}
// MUST implement abstract methods
getArea(): number {
return Math.PI * this.radius * this.radius;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
// β Error: Cannot create an instance of an abstract class
// const shape = new Shape("red");
// β
This works
const circle = new Circle("blue", 5);
circle.describe(); // Inherited concrete method
When to use abstract classes:
- You want to provide shared implementation alongside abstract methods
- You need to define fields or constructors
- You're modeling a clear hierarchy with shared state
Interfaces vs Abstract Classes π
TypeScript offers both interfaces and abstract classes for defining contracts. Here's when to use each:
| Feature | Interface | Abstract Class |
|---|---|---|
| Multiple inheritance | β Can implement many | β Can extend only one |
| Implementation | β No method bodies | β Can have concrete methods |
| Fields/State | β No instance fields | β Can have fields |
| Constructor | β No constructor | β Can have constructor |
| Access modifiers | β All public | β public/protected/private |
| Runtime presence | β Erased after compilation | β Exists at runtime |
// Interface approach - pure contract
interface Drawable {
draw(): void;
getColor(): string;
}
interface Resizable {
resize(scale: number): void;
}
// Can implement multiple interfaces!
class Canvas implements Drawable, Resizable {
constructor(private color: string) {}
draw(): void {
console.log("Drawing canvas");
}
getColor(): string {
return this.color;
}
resize(scale: number): void {
console.log(`Resizing by ${scale}x`);
}
}
π‘ Rule of thumb: Use interfaces for "can-do" relationships (capabilities) and abstract classes for "is-a" relationships (hierarchies).
Type Casting and Type Guards π‘οΈ
When working with polymorphic code, you sometimes need to check or assert the specific type of an object:
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
bark(): void {
console.log("Woof!");
}
}
class Cat extends Animal {
meow(): void {
console.log("Meow!");
}
}
function makeSound(animal: Animal): void {
// Type guard using instanceof
if (animal instanceof Dog) {
animal.bark(); // TypeScript knows it's a Dog here
} else if (animal instanceof Cat) {
animal.meow(); // TypeScript knows it's a Cat here
}
}
const pets: Animal[] = [new Dog("Buddy"), new Cat("Whiskers")];
pets.forEach(makeSound);
Type assertion (use sparingly!):
const animal: Animal = new Dog("Max");
// Type assertion - you're telling TypeScript "trust me"
(animal as Dog).bark();
β οΈ Warning: Type assertions bypass TypeScript's type checking. Use type guards (instanceof, custom type predicates) when possible!
Practical Examples π§
Example 1: Employee Management System π
Let's build a realistic employee hierarchy demonstrating inheritance and polymorphism:
abstract class Employee {
constructor(
protected name: string,
protected id: number,
protected baseSalary: number
) {}
// Abstract methods each employee type must implement
abstract calculateSalary(): number;
abstract getRole(): string;
// Concrete method shared by all employees
getDetails(): string {
return `${this.name} (ID: ${this.id}) - ${this.getRole()}`;
}
protected getBaseSalary(): number {
return this.baseSalary;
}
}
class Developer extends Employee {
constructor(
name: string,
id: number,
baseSalary: number,
private programmingLanguages: string[]
) {
super(name, id, baseSalary);
}
calculateSalary(): number {
// Developers get bonus for each language
const languageBonus = this.programmingLanguages.length * 5000;
return this.getBaseSalary() + languageBonus;
}
getRole(): string {
return "Software Developer";
}
getLanguages(): string {
return this.programmingLanguages.join(", ");
}
}
class Manager extends Employee {
constructor(
name: string,
id: number,
baseSalary: number,
private teamSize: number
) {
super(name, id, baseSalary);
}
calculateSalary(): number {
// Managers get bonus based on team size
const teamBonus = this.teamSize * 3000;
return this.getBaseSalary() + teamBonus;
}
getRole(): string {
return "Manager";
}
getTeamSize(): number {
return this.teamSize;
}
}
class Intern extends Employee {
constructor(
name: string,
id: number,
baseSalary: number,
private university: string
) {
super(name, id, baseSalary);
}
calculateSalary(): number {
// Interns get base salary only (no bonus)
return this.getBaseSalary();
}
getRole(): string {
return `Intern from ${this.university}`;
}
}
// Polymorphism in action!
function processPayroll(employees: Employee[]): void {
let totalPayroll = 0;
employees.forEach(employee => {
const salary = employee.calculateSalary();
totalPayroll += salary;
console.log(`${employee.getDetails()}: $${salary}`);
});
console.log(`\nTotal Payroll: $${totalPayroll}`);
}
// Usage
const employees: Employee[] = [
new Developer("Alice", 101, 80000, ["TypeScript", "Python", "Go"]),
new Manager("Bob", 102, 90000, 5),
new Intern("Charlie", 103, 30000, "MIT"),
new Developer("Diana", 104, 85000, ["Java", "Kotlin"])
];
processPayroll(employees);
Output:
Alice (ID: 101) - Software Developer: $95000
Bob (ID: 102) - Manager: $105000
Charlie (ID: 103) - Intern from MIT: $30000
Diana (ID: 104) - Software Developer: $95000
Total Payroll: $325000
Key takeaways from this example:
- The abstract
Employeeclass defines the contract all employees must follow - Each subclass provides its own salary calculation logic
- The
processPayrollfunction works with anyEmployeetype through polymorphism - Adding new employee types (e.g.,
Contractor) doesn't require changing existing code
Example 2: Shape Drawing Application π¨
Let's create a graphics system showcasing interface implementation and polymorphism:
interface Drawable {
draw(): void;
getArea(): number;
}
interface Colorable {
setColor(color: string): void;
getColor(): string;
}
// Implementing multiple interfaces
abstract class Shape implements Drawable, Colorable {
protected color: string = "black";
abstract draw(): void;
abstract getArea(): number;
setColor(color: string): void {
this.color = color;
}
getColor(): string {
return this.color;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
draw(): void {
console.log(`Drawing a ${this.color} circle with radius ${this.radius}`);
}
getArea(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
draw(): void {
console.log(`Drawing a ${this.color} rectangle ${this.width}x${this.height}`);
}
getArea(): number {
return this.width * this.height;
}
}
class Triangle extends Shape {
constructor(private base: number, private height: number) {
super();
}
draw(): void {
console.log(`Drawing a ${this.color} triangle (base: ${this.base}, height: ${this.height})`);
}
getArea(): number {
return (this.base * this.height) / 2;
}
}
// Graphics engine using polymorphism
class Canvas {
private shapes: Shape[] = [];
addShape(shape: Shape): void {
this.shapes.push(shape);
}
renderAll(): void {
console.log("=== Rendering Canvas ===");
this.shapes.forEach(shape => shape.draw());
}
getTotalArea(): number {
return this.shapes.reduce((sum, shape) => sum + shape.getArea(), 0);
}
}
// Usage
const canvas = new Canvas();
const circle = new Circle(5);
circle.setColor("red");
const rectangle = new Rectangle(10, 6);
rectangle.setColor("blue");
const triangle = new Triangle(8, 4);
triangle.setColor("green");
canvas.addShape(circle);
canvas.addShape(rectangle);
canvas.addShape(triangle);
canvas.renderAll();
console.log(`\nTotal area covered: ${canvas.getTotalArea().toFixed(2)} sq units`);
Why this design works:
- Interfaces (
Drawable,Colorable) define capabilities without implementation - Abstract
Shapeclass provides common behavior while enforcing contracts - Concrete shapes focus on their specific drawing and calculation logic
- The
Canvasclass treats all shapes uniformly through polymorphism
Example 3: Payment Processing System π³
A real-world example showing how inheritance and polymorphism handle different payment methods:
abstract class PaymentMethod {
constructor(protected accountHolder: string) {}
abstract processPayment(amount: number): boolean;
abstract getPaymentType(): string;
abstract validatePayment(): boolean;
displayReceipt(amount: number, success: boolean): void {
console.log(`\n--- Payment Receipt ---`);
console.log(`Account Holder: ${this.accountHolder}`);
console.log(`Payment Method: ${this.getPaymentType()}`);
console.log(`Amount: $${amount.toFixed(2)}`);
console.log(`Status: ${success ? "SUCCESS" : "FAILED"}`);
console.log(`----------------------\n`);
}
}
class CreditCard extends PaymentMethod {
constructor(
accountHolder: string,
private cardNumber: string,
private cvv: string,
private expiryDate: string
) {
super(accountHolder);
}
validatePayment(): boolean {
// Simple validation (in reality, much more complex)
return this.cardNumber.length === 16 && this.cvv.length === 3;
}
processPayment(amount: number): boolean {
if (!this.validatePayment()) {
console.log("Invalid credit card details!");
return false;
}
console.log(`Processing credit card payment of $${amount}...`);
// Simulate payment processing
const success = Math.random() > 0.1; // 90% success rate
this.displayReceipt(amount, success);
return success;
}
getPaymentType(): string {
return "Credit Card";
}
}
class PayPal extends PaymentMethod {
constructor(
accountHolder: string,
private email: string,
private isVerified: boolean
) {
super(accountHolder);
}
validatePayment(): boolean {
return this.email.includes("@") && this.isVerified;
}
processPayment(amount: number): boolean {
if (!this.validatePayment()) {
console.log("PayPal account not verified!");
return false;
}
console.log(`Processing PayPal payment of $${amount}...`);
const success = Math.random() > 0.05; // 95% success rate
this.displayReceipt(amount, success);
return success;
}
getPaymentType(): string {
return "PayPal";
}
}
class BankTransfer extends PaymentMethod {
constructor(
accountHolder: string,
private bankAccount: string,
private routingNumber: string
) {
super(accountHolder);
}
validatePayment(): boolean {
return this.bankAccount.length >= 8 && this.routingNumber.length === 9;
}
processPayment(amount: number): boolean {
if (!this.validatePayment()) {
console.log("Invalid bank account details!");
return false;
}
console.log(`Processing bank transfer of $${amount}...`);
// Bank transfers are slower but more reliable
const success = Math.random() > 0.02; // 98% success rate
this.displayReceipt(amount, success);
return success;
}
getPaymentType(): string {
return "Bank Transfer";
}
}
// Payment processor using polymorphism
class CheckoutSystem {
processOrder(paymentMethod: PaymentMethod, amount: number): void {
console.log(`\nInitiating payment using ${paymentMethod.getPaymentType()}...`);
const result = paymentMethod.processPayment(amount);
if (result) {
console.log("β
Order completed successfully!");
} else {
console.log("β Payment failed. Please try another method.");
}
}
}
// Usage
const checkout = new CheckoutSystem();
const creditCard = new CreditCard("John Doe", "1234567812345678", "123", "12/25");
const paypal = new PayPal("jane@example.com", "jane@example.com", true);
const bank = new BankTransfer("Alice Smith", "12345678", "123456789");
checkout.processOrder(creditCard, 99.99);
checkout.processOrder(paypal, 149.50);
checkout.processOrder(bank, 500.00);
Benefits of this polymorphic design:
- Adding new payment methods (cryptocurrency, gift cards) requires no changes to
CheckoutSystem - Each payment method encapsulates its own validation and processing logic
- The system treats all payment methods uniformly through the
PaymentMethodinterface - Easy to test individual payment methods in isolation
Example 4: Notification System π§
A practical example showing how polymorphism simplifies handling multiple notification channels:
interface NotificationChannel {
send(recipient: string, message: string): Promise<boolean>;
getChannelName(): string;
}
abstract class BaseNotification implements NotificationChannel {
constructor(protected serviceName: string) {}
abstract send(recipient: string, message: string): Promise<boolean>;
getChannelName(): string {
return this.serviceName;
}
protected logNotification(recipient: string, status: string): void {
console.log(`[${this.serviceName}] To: ${recipient} - Status: ${status}`);
}
}
class EmailNotification extends BaseNotification {
constructor() {
super("Email");
}
async send(recipient: string, message: string): Promise<boolean> {
console.log(`Sending email to ${recipient}...`);
console.log(`Subject: Important Notification`);
console.log(`Body: ${message}`);
// Simulate async email sending
await new Promise(resolve => setTimeout(resolve, 1000));
const success = Math.random() > 0.1;
this.logNotification(recipient, success ? "Delivered" : "Failed");
return success;
}
}
class SMSNotification extends BaseNotification {
constructor() {
super("SMS");
}
async send(recipient: string, message: string): Promise<boolean> {
console.log(`Sending SMS to ${recipient}...`);
console.log(`Message (${message.length} chars): ${message.substring(0, 50)}...`);
// Simulate async SMS sending
await new Promise(resolve => setTimeout(resolve, 500));
const success = Math.random() > 0.05;
this.logNotification(recipient, success ? "Sent" : "Failed");
return success;
}
}
class PushNotification extends BaseNotification {
constructor() {
super("Push");
}
async send(recipient: string, message: string): Promise<boolean> {
console.log(`Sending push notification to device ${recipient}...`);
console.log(`Alert: ${message}`);
// Simulate async push notification
await new Promise(resolve => setTimeout(resolve, 300));
const success = Math.random() > 0.02;
this.logNotification(recipient, success ? "Delivered" : "Not Delivered");
return success;
}
}
// Notification manager using polymorphism
class NotificationManager {
private channels: NotificationChannel[] = [];
registerChannel(channel: NotificationChannel): void {
this.channels.push(channel);
console.log(`β
Registered ${channel.getChannelName()} channel`);
}
async sendToAll(recipient: string, message: string): Promise<void> {
console.log(`\nπ’ Broadcasting message to ${this.channels.length} channels...\n`);
const results = await Promise.all(
this.channels.map(channel => channel.send(recipient, message))
);
const successCount = results.filter(r => r).length;
console.log(`\nβ
Successfully sent: ${successCount}/${this.channels.length}`);
}
async sendViaChannel(
channelName: string,
recipient: string,
message: string
): Promise<boolean> {
const channel = this.channels.find(
c => c.getChannelName().toLowerCase() === channelName.toLowerCase()
);
if (!channel) {
console.log(`β Channel '${channelName}' not found`);
return false;
}
return await channel.send(recipient, message);
}
}
// Usage
async function demo() {
const manager = new NotificationManager();
// Register notification channels
manager.registerChannel(new EmailNotification());
manager.registerChannel(new SMSNotification());
manager.registerChannel(new PushNotification());
// Send via all channels
await manager.sendToAll(
"user@example.com",
"Your order #12345 has been shipped!"
);
// Send via specific channel
console.log("\n--- Sending urgent SMS ---");
await manager.sendViaChannel(
"SMS",
"+1234567890",
"Your verification code is: 456789"
);
}
demo();
Why polymorphism shines here:
- The
NotificationManagerdoesn't need to know implementation details of each channel - Adding Slack, Discord, or WhatsApp notifications requires zero changes to the manager
- Each channel can have vastly different implementation (REST API, SMTP, WebSockets) but presents a uniform interface
- Easy to mock channels for testing
Common Mistakes β οΈ
1. Forgetting to Call super() in Constructor
β Wrong:
class Dog extends Animal {
constructor(name: string, breed: string) {
// Missing super() call!
this.breed = breed; // Error!
}
}
β Correct:
class Dog extends Animal {
constructor(name: string, private breed: string) {
super(name); // Must call parent constructor first!
}
}
2. Accessing Private Members from Child Classes
β Wrong:
class Animal {
private name: string; // Private!
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark(): void {
console.log(`${this.name} barks`); // Error: name is private!
}
}
β Correct:
class Animal {
protected name: string; // Use protected instead!
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark(): void {
console.log(`${this.name} barks`); // Works!
}
}
3. Incorrect Method Signature When Overriding
β Wrong:
class Shape {
getArea(): number {
return 0;
}
}
class Circle extends Shape {
// Different return type - not valid override!
getArea(): string { // Error!
return "Area is 10";
}
}
β Correct:
class Circle extends Shape {
override getArea(): number { // Same signature
return Math.PI * this.radius ** 2;
}
}
4. Not Implementing All Abstract Methods
β Wrong:
abstract class Shape {
abstract getArea(): number;
abstract getPerimeter(): number;
}
class Circle extends Shape {
// Missing getPerimeter()!
getArea(): number {
return Math.PI * this.radius ** 2;
}
} // Error: must implement getPerimeter()!
β Correct:
class Circle extends Shape {
getArea(): number {
return Math.PI * this.radius ** 2;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
5. Misusing Type Assertions Instead of Type Guards
β Wrong (unsafe):
function makeSound(animal: Animal): void {
// Dangerous! What if it's not actually a Dog?
(animal as Dog).bark();
}
β Correct (safe):
function makeSound(animal: Animal): void {
if (animal instanceof Dog) {
animal.bark(); // TypeScript knows it's safe
}
}
6. Creating Deep Inheritance Hierarchies
β Wrong (inheritance chain too deep):
class Animal { }
class Mammal extends Animal { }
class Carnivore extends Mammal { }
class Feline extends Carnivore { }
class BigCat extends Feline { }
class Lion extends BigCat { } // Too deep!
β Better (favor composition):
class Animal {
constructor(
private diet: Diet,
private habitat: Habitat
) {}
}
class Lion extends Animal {
constructor() {
super(new CarnivoreDiet(), new SavannahHabitat());
}
}
π‘ Best Practice: Limit inheritance depth to 2-3 levels. Beyond that, consider composition or interfaces.
Key Takeaways π―
π Quick Reference Card
| Concept | Keyword/Syntax | Purpose |
|---|---|---|
| Inheritance | extends |
Create class hierarchies (is-a relationships) |
| Parent access | super(), super.method() |
Call parent constructor or methods |
| Method override | override keyword |
Replace parent method implementation |
| Abstract class | abstract class |
Base class that cannot be instantiated |
| Abstract method | abstract methodName() |
Method that must be implemented by children |
| Interface | interface |
Contract defining shape of objects |
| Implement interface | implements |
Promise to fulfill interface contract |
| Type guard | instanceof |
Check object type at runtime |
| Access modifiers | public, protected, private |
Control member visibility |
| Polymorphism | Method overriding + parent type references | Treat different types uniformly |
π§ Core Principles
1. Inheritance creates "is-a" relationships
- A Dog is an Animal
- A Manager is an Employee
- Use when specialization makes sense
2. Polymorphism enables flexibility
- Write code once, work with many types
- Parent type reference, child type behavior
- Runtime method resolution
3. Abstract classes enforce contracts
- Cannot be instantiated directly
- Mix abstract and concrete methods
- Provide shared implementation
4. Interfaces define capabilities
- Pure contracts with no implementation
- Support multiple inheritance
- Ideal for "can-do" relationships
5. Protected vs Private
private: Only within the classprotected: Within class and childrenpublic: Everywhere (default)
6. Favor composition over deep inheritance
- Limit inheritance depth to 2-3 levels
- Use interfaces for multiple capabilities
- Compose objects from smaller parts
Further Study π
Ready to dive deeper? Here are excellent resources:
TypeScript Handbook - Classes: https://www.typescriptlang.org/docs/handbook/2/classes.html - Official documentation with comprehensive examples
Refactoring Guru - OOP Concepts: https://refactoring.guru/design-patterns/what-is-pattern - Learn design patterns that leverage inheritance and polymorphism
TypeScript Deep Dive - Classes: https://basarat.gitbook.io/typescript/future-javascript/classes - Advanced patterns and best practices
π Congratulations! You've mastered inheritance and polymorphism in TypeScript. These foundational concepts will serve you throughout your object-oriented programming journey. Practice building class hierarchies, experiment with different designs, and remember: good OOP design favors flexibility and maintainability over clever tricks!
π₯ Next Steps: Try refactoring some existing code to use inheritance and polymorphism. Look for repeated patterns, "is-a" relationships, and opportunities to treat different types uniformly. The more you practice, the more natural these concepts will become!