Skip to main content

TS Ep 21: Class Inheritance, `super()`, and the `override` Keyword

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 21: This Article
Class Inheritance allows a derived class (subclass) to inherit fields, methods, getters, and setters from a base class (superclass) using the extends keyword. TypeScript enforces strict initialization order with super() and prevents silent method typos using the override modifier.

1. Class Inheritance with extends
#

When a class extends another class, it establishes a prototypal inheritance link. The derived class inherits all public and protected members of the parent class.

// Base Superclass
class UserNotification {
  public id: string;
  public createdAt: Date = new Date();

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

  public send(recipient: string): void {
    console.log(`[Notification ${this.id}]: Sent to ${recipient}`);
  }
}

// Derived Subclass
class EmailNotification extends UserNotification {
  // Inherits 'id', 'createdAt', and 'send()' automatically!
  public emailSubject: string = "Account Update";

  public attachPdf(filename: string) {
    console.log(`Attached PDF: ${filename} to Notification ${this.id}`);
  }
}

const email = new EmailNotification("notif_9981");
email.attachPdf("statement.pdf");
email.send("[email protected]"); // Inherited method from UserNotification!

2. Constructor Chaining with super()
#

If a derived subclass defines its own explicit constructor(), it MUST call super() as its first action before accessing this or returning.

The Order of Initialization Rule:
#

  1. super(...args) is executed, initializing the base superclass fields and running the parent constructor.
  2. Parameter properties and instance fields of the subclass are initialized.
  3. The remaining body of the subclass constructor runs.
class BaseComponent {
  public elementId: string;

  constructor(elementId: string) {
    this.elementId = elementId;
    console.log(`[1. BaseComponent]: Initialized DOM element #${this.elementId}`);
  }
}

class ModalComponent extends BaseComponent {
  public isOpen: boolean = false;

  constructor(elementId: string, public title: string) {
    // ❌ Compiler Error: 'super' must be called before accessing 'this' in the constructor!
    // console.log(this.title); 

    // 🟢 CORRECT: Must call super() FIRST!
    super(elementId);

    // NOW it is safe to use 'this'
    console.log(`[2. ModalComponent]: Initialized Modal "${this.title}"`);
  }
}

const modal = new ModalComponent("app-modal", "Delete Account Confirmation");

3. Method Overriding & super.method()
#

A subclass can override (redefine) a method inherited from its parent class to provide specialized behavior.

If the subclass still wants to execute the parent’s original implementation, it can invoke super.methodName(...).

class BaseRepository {
  public save(data: unknown): void {
    console.log("[BaseRepo]: Persisting data to SQL database...");
  }
}

class CachingRepository extends BaseRepository {
  // Overriding save()
  public override save(data: unknown): void {
    console.log("[CachingRepo]: Evicting Redis cache layer...");
    
    // Delegate physical persistence to parent class using super!
    super.save(data);
    
    console.log("[CachingRepo]: Save complete.");
  }
}

const repo = new CachingRepository();
repo.save({ id: 101, name: "Order" });

Terminal Output:

[CachingRepo]: Evicting Redis cache layer...
[BaseRepo]: Persisting data to SQL database...
[CachingRepo]: Save complete.

4. The override Modifier (TypeScript 4.3+)
#

Imagine a scenario where a parent class has a method named renderUI(). In a subclass, you override renderUI().

A year later, an engineer renames the parent method to render() during a refactoring.

Without protection, your subclass method renderUI() is no longer an override—it silently becomes a completely new, uncalled method! This leads to massive, unhandled runtime bugs.

To eliminate this vulnerability, TypeScript 4.3 introduced the override keyword (and the "noImplicitOverride": true tsconfig flag):

{
  "compilerOptions": {
    "noImplicitOverride": true
  }
}

When "noImplicitOverride": true is enabled, you MUST explicitly add the override keyword to any subclass method intended to override a parent method:

class ParentView {
  public render(): void {
    console.log("Rendering Parent");
  }
}

class ChildView extends ParentView {
  // 🟢 Explicit 'override' keyword verifies the parent method exists!
  public override render(): void {
    console.log("Rendering Child");
  }

  // ❌ Compiler Error: This member cannot have an 'override' modifier 
  // because it is not declared in the base class 'ParentView'.
  /*
  public override renderOld(): void {
    console.log("Old Render");
  }
  */
}

5. Prototypal Inheritance Under the Hood
#

In JavaScript’s prototype model, extends configures two prototype pointers:

class Animal {}
class Dog extends Animal {}

console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Dog.__proto__ === Animal);                     // true (Static inheritance!)

This means static fields and methods are also inherited across extends chains!


Summary & Next Steps
#

In this episode:

  • We derived subclasses using extends.
  • We mastered Constructor Chaining: super() must be called before accessing this.
  • We invoked parent class implementations using super.methodName().
  • We configured "noImplicitOverride": true and used the override keyword to prevent silent refactoring bugs.

In Episode 22: Abstract Classes, we will learn how to build non-instantiable base blueprints using abstract class and abstract method signatures!

typescript - This article is part of a series.
Part 21: This Article