Skip to content

TypeScript Inheritance — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about TypeScript Inheritance. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript inheritance via extends lets you build class hierarchies where child classes reuse, override, and extend parent behavior, with super calling parent methods and mixins simulating multiple inheritance for complex reuse scenarios.

What You'll Learn

  • The extends keyword for single inheritance
  • The super keyword for parent access
  • Method overriding and access
  • Polymorphism and type-safe hierarchies
  • Mixins for multiple inheritance

Why It Matters

Inheritance is a fundamental OOP concept that models "is-a" relationships — a Dog is an Animal, a SavingsAccount is a BankAccount. Properly designed inheritance hierarchies reduce code duplication and make your system's domain model explicit.

Real-World Use

The Doda Browser extension API has a hierarchy of event types: EventTabEventTabCreatedEvent, each adding more specific properties. NestJS uses inheritance for its base controller and service classes, providing common CRUD operations.

Learning Path

flowchart LR
  A[Abstract Classes] --> B[Inheritance]
  B --> C[Decorators]
  B --> D[You Are Here]
  C --> E[This Typing]
  E --> F[Index Signatures]

Basic Inheritance

class Animal {
  constructor(public name: string) {}

  speak(): string {
    return `${this.name} makes a sound`;
  }

  move(distance: number): string {
    return `${this.name} moves ${distance}m`;
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name); // Must call super first!
  }

  speak(): string {
    return `${this.name} barks`;
  }
}

const dog = new Dog("Rex");
console.log(dog.speak());   // Rex barks
console.log(dog.move(10));  // Rex moves 10m

Think of extends as saying: "Dog IS an Animal with all its capabilities, plus its own special behavior."

Method Overriding

Subclasses can override methods and call the parent version with super:

class BaseLogger {
  log(level: string, message: string): void {
    console.log(`[${level}] ${message}`);
  }
}

class TimestampLogger extends BaseLogger {
  log(level: string, message: string): void {
    const timestamp = new Date().toISOString();
    super.log(level, `[${timestamp}] ${message}`);
  }
}

class FilteredLogger extends BaseLogger {
  private minLevel: number = 0;

  setMinLevel(level: number): void {
    this.minLevel = level;
  }

  log(level: string, message: string): void {
    const levelMap: Record<string, number> = { info: 0, warn: 1, error: 2 };
    if ((levelMap[level] ?? 0) >= this.minLevel) {
      super.log(level, message);
    }
  }
}

const logger = new TimestampLogger();
logger.log("info", "Server started");
// [info] [2026-06-28T12:00:00.000Z] Server started

Constructor Inheritance

class Employee {
  constructor(
    public name: string,
    public salary: number
  ) {}
}

class Manager extends Employee {
  constructor(
    name: string,
    salary: number,
    public department: string
  ) {
    super(name, salary * 1.2); // Managers get 20% more
  }
}

class Intern extends Employee {
  constructor(name: string) {
    super(name, 0); // Unpaid
  }
}

const manager = new Manager("Alice", 80000, "Engineering");
console.log(manager.salary); // 96000

Polymorphism

Polymorphism lets you treat different subclass instances through the same parent type:

abstract class PaymentMethod {
  abstract process(amount: number): string;
  abstract getType(): string;
}

class CreditCard extends PaymentMethod {
  process(amount: number): string {
    return `Charged $${amount} to credit card`;
  }
  getType(): string { return "Credit Card"; }
}

class PayPal extends PaymentMethod {
  process(amount: number): string {
    return `Charged $${amount} via PayPal`;
  }
  getType(): string { return "PayPal"; }
}

class Crypto extends PaymentMethod {
  process(amount: number): string {
    return `Charged $${amount} in cryptocurrency`;
  }
  getType(): string { return "Crypto"; }
}

function processPayment(method: PaymentMethod, amount: number): void {
  console.log(`[${method.getType()}] ${method.process(amount)}`);
}

const payments: PaymentMethod[] = [
  new CreditCard(),
  new PayPal(),
  new Crypto(),
];

for (const payment of payments) {
  processPayment(payment, 99.99);
}
// [Credit Card] Charged $99.99 to credit card
// [PayPal] Charged $99.99 via PayPal
// [Crypto] Charged $99.99 in cryptocurrency

The protected Access Modifier

Protected members are accessible in subclasses but not from outside:

class Animal {
  protected dna: string = "ATCG";
  private heartbeat(): void {} // Not inherited
}

class Dog extends Animal {
  constructor() {
    super();
    console.log(this.dna); // OK — protected
    // this.heartbeat(); // Error — private
  }
}

const dog = new Dog();
// dog.dna; // Error — protected

Mixins

TypeScript doesn't support multiple inheritance, but mixins simulate it:

// Mixin functions
function Timestamped<TBase extends new (...args: any[]) => object>(Base: TBase) {
  return class extends Base {
    createdAt = new Date();

    getAge(): number {
      return Date.now() - this.createdAt.getTime();
    }
  };
}

function Activatable<TBase extends new (...args: any[]) => object>(Base: TBase) {
  return class extends Base {
    isActive: boolean = false;

    activate(): void { this.isActive = true; }
    deactivate(): void { this.isActive = false; }
  };
}

class UserBase {
  constructor(public name: string) {}
}

// Apply mixins
const ActiveTimestampedUser = Activatable(Timestamped(UserBase));

class User extends ActiveTimestampedUser {
  constructor(name: string) {
    super(name);
  }

  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

const user = new User("Alice");
user.activate();
console.log(user.greet());            // Hello, I'm Alice
console.log(user.isActive);           // true
console.log(user.createdAt instanceof Date); // true

Common Mistakes

1. Forgetting to Call super()

class Parent {
  constructor(public name: string) {}
}

class Child extends Parent {
  constructor(name: string) {
    // super(name); // Error: Must call super in derived class before accessing 'this'
  }
}

2. Calling super() After Accessing this

class Child extends Parent {
  constructor(name: string) {
    // this.greeting = "Hi"; // Error: 'super' must be called before accessing 'this'
    super(name);
    this.greeting = "Hi"; // OK
  }
  private greeting!: string;
}

3. Overriding Without Calling Super When Needed

class Parent {
  init(): void { console.log("Parent init"); }
}

class Child extends Parent {
  init(): void {
    // Forgot to call super.init() — parent initialization is skipped
    console.log("Child init");
  }
}

4. Confusing Inheritance with Composition

Inheritance models "is-a". Composition models "has-a". Favor composition over inheritance.

// Inheritance (is-a): Dog IS an Animal
class Dog extends Animal {}

// Composition (has-a): Car HAS an Engine
class Car {
  constructor(private engine: Engine) {}
}

5. Creating Deep Inheritance Hierarchies

More than 2-3 levels of inheritance usually signals a design problem. Prefer composition or interfaces.

6. Not Using protected Constructors for Abstract-Like Behavior

class BaseClass {
  protected constructor() {} // Cannot be instantiated directly, but no abstract methods
}

Practice Questions

  1. What must a subclass constructor call first? super() with the parent constructor's required arguments.

  2. What is the difference between private and protected? private members are not accessible in subclasses. protected members are.

  3. What is polymorphism? The ability to use objects of different types through a common interface, where the correct method is determined at runtime.

  4. How do mixins simulate multiple inheritance? By using functions that take a base class and return a new class extending it, then combining them.

Challenge: Create an inheritance hierarchy for a document processing system: Document (base) → PDFDocument, SpreadsheetDocument, TextDocument. Each subclass implements render(): string and parse(): void differently. Use polymorphism to process a mixed array.

FAQ

Can TypeScript classes inherit from multiple classes?

No, TypeScript (like Java and C#) supports single inheritance only. Use mixins or interfaces for multiple inheritance simulation.

What is the difference between `extends` and `implements`?

extends inherits implementation from a class. implements checks that a class conforms to an interface (no implementation inheritance).

Can I override a method with a different return type?

Yes, as long as the return type is covariant (a subtype of the original).

What is the `super` keyword?

It refers to the parent class prototype, allowing access to overridden methods and the parent constructor.

Can I prevent a class from being extended?

Yes, mark the class as final conceptually by making the constructor private and using a static Factory Methodtory" >}} method.

Mini Project: Notification System

// src/notifications.ts

abstract class Notification {
  constructor(
    protected recipient: string,
    protected message: string
  ) {}

  abstract send(): Promise<boolean>;
  abstract getType(): string;

  protected log(success: boolean): void {
    console.log(`[${this.getType()}] To: ${this.recipient}${success ? "Sent" : "Failed"}`);
  }
}

class EmailNotification extends Notification {
  async send(): Promise<boolean> {
    console.log(`Sending email to ${this.recipient}: ${this.message}`);
    this.log(true);
    return true;
  }

  getType(): string { return "Email"; }
}

class SMSNotification extends Notification {
  async send(): Promise<boolean> {
    console.log(`Sending SMS to ${this.recipient}: ${this.message}`);
    this.log(true);
    return true;
  }

  getType(): string { return "SMS"; }
}

class PushNotification extends Notification {
  async send(): Promise<boolean> {
    console.log(`Sending push to ${this.recipient}: ${this.message}`);
    this.log(true);
    return true;
  }

  getType(): string { return "Push"; }
}

class NotificationService {
  private notifications: Notification[] = [];

  add(notification: Notification): void {
    this.notifications.push(notification);
  }

  async sendAll(): Promise<{ sent: number; failed: number }> {
    let sent = 0;
    let failed = 0;

    for (const notification of this.notifications) {
      const success = await notification.send();
      if (success) sent++; else failed++;
    }

    return { sent, failed };
  }
}

const service = new NotificationService();
service.add(new EmailNotification("alice@example.com", "Scan complete"));
service.add(new SMSNotification("+1234567890", "Threat detected!"));
service.add(new PushNotification("device-abc", "Update available"));

const result = await service.sendAll();
console.log(`Sent: ${result.sent}, Failed: ${result.failed}`);

What's Next

Now explore decorators for Metaprogramming:

Lesson Description
{{< ref "/programming-languages/typescript/20-abstract-classes" >}} Review abstract classes
{{< ref "/programming-languages/typescript/22-decorators" >}} Class, method, property, parameter decorators
{{< ref "/programming-languages/typescript/23-this-typing" >}} This parameter binding and typing

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro