Skip to content

Class Inheritance

The extends keyword enables classes to inherit functionality from other classes. This lets you create hierarchies of related classes, override parent methods, and reuse code. While built on JavaScript's prototype system, class inheritance provides a more convenient syntax for these relationships.

Basic Inheritance

We can extend a base class to create more specific classes:

js
class Shape {
    constructor(colour) {
        this.colour = colour;
    }

    draw() {
        return `A ${this.colour} shape.`;
    }
}

class Square extends Shape {
    constructor(colour, size) {
        super(colour);         // Call parent constructor
        this.size = size;
    }

    draw() {
        return `A ${this.colour} square with size ${this.size}`;
    }
}

class Circle extends Shape {
    constructor(colour, radius) {
        super(colour);         // Call parent constructor
        this.radius = radius;
    }

    draw() {
        return `A ${this.colour} circle with radius ${this.radius}`;
    }
}

// Create different types of shapes
const square = new Square("red", 10);
const circle = new Circle("blue", 5);

console.log(square.draw());  // "A red square with size 10"
console.log(circle.draw());  // "A blue circle with radius 5"

Notice how much clearer this is compared to using constructor functions:

  • class keyword clearly shows we're defining a template for objects
  • constructor method is clearly marked
  • extends shows inheritance relationships
  • super() calls the parent constructor
  • Methods are defined directly in the class body

The Prototype Chain

The extends keyword creates a prototype chain behind the scenes:

js
// This class hierarchy:
class Shape { }
class Square extends Shape { }
const square = new Square();

// Creates this prototype chain:
square → Square.prototypeShape.prototypeObject.prototype

Using super()

When extending a class:

  1. You must call super() in the constructor before using this
  2. If you don't define a constructor, a default one that calls super() is created
  3. super() must be called with any parameters the parent constructor expects
js
class Shape {
    constructor(colour) {
        this.colour = colour;
    }
}

class Square extends Shape {
    constructor(colour, size) {
        super(colour);  // Must call super() first
        this.size = size;  // Now we can use this
    }
}

// This would cause an error:
class BadSquare extends Shape {
    constructor(colour, size) {
        this.size = size;  // Error: Must call super() first
        super(colour);
    }
}

Overriding Methods

Child classes can override (replace) methods from their parent class:

js
class Animal {
    speak() {
        return "Some sound";
    }
}

class Dog extends Animal {
    speak() {  // Override the parent's speak method
        return "Woof!";
    }
}

class Cat extends Animal {
    speak() {
        return "Meow!";
    }
}

const dog = new Dog();
console.log(dog.speak());  // "Woof!"

You can also call the parent's version of a method using super:

js
class Bird extends Animal {
    speak() {
        return super.speak() + " Tweet!";  // Call parent method
    }
}

const bird = new Bird();
console.log(bird.speak());  // "Some sound Tweet!"

Private Fields in Inheritance

Private fields (using the # prefix) are truly private in JavaScript - they are only accessible within the class where they are defined. This is different from languages like C++, where private members are accessible by friend classes and protected members are accessible by derived classes.

js
class BankAccount {
    #balance = 0;  // Private field

    constructor(initialBalance) {
        this.#balance = initialBalance;
    }

    getBalance() {
        return this.#balance;
    }
}

class SavingsAccount extends BankAccount {
    #interestRate = 0.05;  // Private to SavingsAccount

    addInterest() {
        // Can't access parent's #balance directly
        // this.#balance *= (1 + this.#interestRate);  // Error!
        
        // Must use public methods to interact with parent's private fields
        const currentBalance = this.getBalance();
        return currentBalance * (1 + this.#interestRate);
    }
}

const account = new SavingsAccount(1000);
console.log(account.getBalance());    // 1000
console.log(account.addInterest());   // 1050
// console.log(account.#balance);     // Error: Cannot access private field
// console.log(account.#interestRate);// Error: Cannot access private field

Unlike C++'s protected members, private fields in JavaScript are truly private and cannot be accessed by child classes. To share private data between classes, you must use public methods like getters and setters. If you need protected members (accessible by child classes but not from outside), TypeScript adds support for this with the protected keyword.

Static Members

Static methods and properties are inherited by child classes:

js
class Vehicle {
    static numberOfVehicles = 0;
    static getCount() { return this.numberOfVehicles; }
}
class Car extends Vehicle {}
console.log(Car.getCount());  // Inherited static method

Type Checking

The instanceof operator checks if an object belongs to a class or its inheritance chain:

js
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

const dog = new Dog();
console.log(dog instanceof Dog);     // true
console.log(dog instanceof Animal);  // true
console.log(dog instanceof Cat);     // false

Single Inheritance

JavaScript only supports single inheritance - a class can only extend one parent class. While JavaScript has no built-in interface concept, TypeScript adds interfaces which provide a way to define and implement multiple contracts while maintaining single inheritance.

js
class A {}
class B {}
// This is not allowed:
// class C extends A, B {}  // Error!

// Instead, use composition:
class C {
    constructor() {
        this.a = new A();
        this.b = new B();
    }
}

Abstract Classes

JavaScript doesn't have built-in support for abstract classes. While there are workarounds using new.target, it's better to use TypeScript which provides proper abstract class support with the abstract keyword.

💡 Class inheritance provides a clear and structured way to share behaviour between objects. The extends keyword and super() make it much easier to understand and maintain inheritance relationships compared to the older prototype approach.