Inheritance in TypeScript

Inheritance is a fundamental pillar of object-oriented programming (OOP). It allows you to create a blueprint (a class) that "inherits" the characteristics and behaviors of another blueprint. This creates a natural hierarchy in your code, similar to how a "Car" is a specific type of "Vehicle." In TypeScript, we use inheritance to promote the DRY (Don't Repeat Yourself) principle, allowing us to reuse code across multiple classes without rewriting the same logic over and over.

Best Practice: Use inheritance to model "is-a" relationships. For example, a Manager is a Employee. If you find yourself using inheritance just to share a few utility functions, consider using composition or utility modules instead.

 

Key Points About Inheritance:

  1. Base class (superclass): Think of this as the "parent" or generic class. It contains the common logic that multiple other classes will share.
  2. Derived class (subclass): This is the "child" class. It inherits everything from the base class but can also have its own unique features.
  3. Extensibility: A derived class isn't just a copy; it can add new properties, introduce new methods, or "override" (change) how a parent method works.

 

Inheriting from a Class

To implement inheritance in TypeScript, we use the extends keyword. When one class extends another, it gains access to all the non-private members of the parent. This is incredibly useful for building complex systems where different objects share a common foundation.

Example:

class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak(): void {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  breed: string;

  constructor(name: string, breed: string) {
    // We must call the parent constructor using super()
    super(name);  
    this.breed = breed;
  }

  speak(): void {
    console.log(`${this.name} barks!`);
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.speak();  // Output: Buddy barks!

In this example:

  • The Dog class extends Animal. This means every Dog automatically has a name property and a speak method.
  • We added a unique property breed that only exists on Dog, not on the general Animal class.
  • By defining speak() inside Dog, we specialized the behavior for dogs specifically.
Developer Tip: Inheritance is widely used in UI frameworks. For instance, in many libraries, every Button or Slider component inherits from a base UIElement class to share logic like positioning and visibility.

The super Keyword

The super keyword is your bridge back to the parent class. In a derived class, super serves two main purposes: calling the parent's constructor and calling the parent's methods. If your child class has its own constructor, you must call super() before you try to use this.

Example:

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

  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Cat extends Animal {
  constructor(name: string) {
    super(name);  // Passes the name up to the Animal constructor
  }

  speak() {
    console.log(`${this.name} meows.`);
  }
}

const cat = new Cat("Whiskers");
cat.speak();  // Output: Whiskers meows.
Watch Out: If you define a constructor in a subclass, you must call super() before accessing any properties with this. If you forget, TypeScript will throw a compiler error.

Access Modifiers and Inheritance

Control over who can see your data is vital in large applications. TypeScript provides three main access modifiers that behave differently during inheritance:

  • public: The default. Accessible from anywhere inside the class, subclasses, and external code.
  • protected: Only accessible within the class itself and its subclasses. This is perfect for "internal" logic you want children to use, but want to hide from the rest of the world.
  • private: Only accessible within the specific class where it was defined. Subclasses cannot see private members of their parents.

Example:

class Employee {
  public name: string;
  protected salary: number;
  private id: number;

  constructor(name: string, salary: number, id: number) {
    this.name = name;
    this.salary = salary;
    this.id = id;
  }
}

class Manager extends Employee {
  public getDetails() {
    // This works! Subclasses can access protected members.
    return `${this.name} earns ${this.salary}.`;
  }

  public getId() {
    // ERROR: Property 'id' is private and only accessible within class 'Employee'.
    // return this.id; 
  }
}
Common Mistake: Beginners often use private when they actually need protected. If you plan on extending a class and need the child to use a specific property, make it protected.

Method Overriding

Method overriding allows a subclass to provide a specific implementation of a method that is already provided by its parent. This is how we achieve polymorphism the ability for different types to be treated as their parent type while still maintaining their unique behaviors.

Example:

class Report {
  generate(): void {
    console.log("Generating a generic report...");
  }
}

class FinancialReport extends Report {
  generate(): void {
    console.log("Generating financial charts and balance sheets...");
  }
}

class PerformanceReport extends Report {
  generate(): void {
    super.generate(); // We can call the parent's version too!
    console.log("Adding employee performance metrics...");
  }
}

const reports: Report[] = [new FinancialReport(), new PerformanceReport()];
reports.forEach(r => r.generate());

In this example, PerformanceReport uses super.generate(). This is a common pattern where you want to *add* to the parent's behavior rather than completely replacing it.

Best Practice: Keep your inheritance hierarchies shallow. Deeply nested inheritance (e.g., A extends B extends C extends D...) makes code very difficult to follow and debug. Aim for no more than 2 or 3 levels of depth.

 

Summary

Inheritance is a powerful tool in TypeScript that helps you organize your code into logical hierarchies. By using the extends keyword, you can build upon existing logic, reducing duplication and making your codebase easier to maintain.

  • The extends keyword: Establishes the link between a base and derived class.
  • The super keyword: Necessary for initializing the parent class and accessing its methods.
  • Access modifiers: protected is your best friend when you want to share data with children but hide it from the public API.
  • Polymorphism: Overriding methods allows different subclasses to respond to the same method call in their own unique way.

Mastering inheritance will allow you to design more robust, scalable TypeScript applications by creating reusable "building blocks" of logic.