Right, so you’ve got a class. It’s a lovely little blueprint. But now you want a new class that does everything the first one does, plus some extra stuff, or maybe a slightly different variation. Your first instinct might be to just copy-paste the first class and start hacking away. Don’t. That’s how you create a maintenance nightmare where a bug fix needs to be applied in five different places. This is where extends comes in—it’s TypeScript’s mechanism for classical inheritance, letting you create a new class based on an existing one.

The child class (the one you’re creating, also called the derived class) inherits all the properties and methods of the parent class (the base class). This is the “is-a” relationship. A SavingsAccount is a BankAccount. A Cat is an Animal. You get the idea.

The extends Keyword

You use extends to literally tell TypeScript which class you’re building upon. It’s straightforward:

class Animal {
    name: string;

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

    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    // Snake inherits 'name', 'constructor', and 'move' from Animal
    // But we can add new stuff specific to snakes...
    slither() {
        console.log(`${this.name} is slithering.`);
    }
}

const mySnake = new Snake("Sid");
mySnake.move(5); // "Sid moved 5m." <- Inherited method
mySnake.slither(); // "Sid is slithering." <- Its own method

See? Snake got move for free. But here’s the first rough edge: what if the base class’s behavior isn’t quite right for the child class? A snake doesn’t “move,” it “slithers.” The inherited move method works, but it’s not specific. We need to override it.

Overriding Methods and Using super

You override a method simply by redefining it in the child class. But here’s the catch: you often don’t want to replace the parent’s behavior entirely, you want to extend it. This is where super becomes your best friend. super is a keyword that refers to the parent class.

class Snake extends Animal {

    // Override the parent's 'move' method
    move(distanceInMeters: number = 5) { // Give it a sensible default
        console.log("Slithering...");
        // Call the parent's version of 'move' to reuse its logic
        super.move(distanceInMeters);
    }
}

const mySnake = new Snake("Sid");
mySnake.move(); // "Slithering..." then "Sid moved 5m."

super isn’t just for methods. It’s absolutely critical for the constructor. This is a classic pitfall.

The Constructor and super() Call

A derived class has its own constructor. However, it must call super() before it can use this. Why? Because the parent class is responsible for setting up its own part of the object. TypeScript (and JavaScript) will throw a very clear error at you if you forget this. It’s the language saying, “Hey, I can’t initialize the ‘child’ part of this object until the ‘parent’ part is ready.”

class Dog extends Animal {
    breed: string;

    constructor(name: string, breed: string) {
        // 'this' can't be used here... it's a ReferenceError waiting to happen.
        // this.breed = breed; // ❌ Nope. Illegal.

        super(name); // ✅ MUST call this first. This initializes the 'Animal' part.
        this.breed = breed; // ✅ Now it's safe to set Dog-specific properties.

        // This is the designers' choice. It's a bit rigid, but it prevents a whole class of nasty bugs.
    }

    bark() {
        console.log(`${this.name} (a ${this.breed}) woofs!`);
    }
}

const myDog = new Dog("Rex", "German Shepherd");
myDog.bark(); // "Rex (a German Shepherd) woofs!"

The Access Modifier Gotcha (protected)

You’ve seen public (everyone can see it) and private (only this class can see it). But inheritance introduces a third visibility: protected. A protected member is accessible within the class itself and by any derived classes, but not from the outside world. It’s like a family secret.

This is incredibly useful but also a common source of confusion. If you make something private in the base class, the derived class has no idea it even exists.

class Base {
    private secret = "base secret";
    protected familySecret = "family secret";
}

class Derived extends Base {
    trySecrets() {
        console.log(this.familySecret); // ✅ Allowed
        console.log(this.secret); // ❌ Property 'secret' is private and only accessible within class 'Base'.
    }
}

const derived = new Derived();
console.log(derived.familySecret); // ❌ Not allowed from the outside either.

The Weirdness of Inheriting Static Members

Here’s a bit of trivia that often surprises people: static members are also inherited.

class Animal {
    static planet = "Earth";

    static getPlanet() {
        return `We live on ${this.planet}`;
    }
}

class Snake extends Animal {
    // Snake.planet exists and is "Earth"
    // Snake.getPlanet() exists and works
}

console.log(Snake.planet); // "Earth"
console.log(Snake.getPlanet()); // "We live on Earth"

It works, but use this feature with intention, not by accident. It reinforces that relationship—a Snake and an Animal are both from the same planet.

The big takeaway? Inheritance is a powerful tool for creating hierarchies and sharing behavior. But it’s also a sharp tool. Overuse it, and you’ll create a tangled, fragile “class hierarchy” that’s impossible to change. The modern trend is to favor composition over inheritance (“has-a” instead of “is-a”) for this very reason. But when you genuinely have that “is-a” relationship, extends and super are how you do it without copy-pasting your way into oblivion.