JavaScript Inheritance and super: Extend Classes and Call Parents

JavaScript inheritance lets one class reuse and specialize the behavior of another class. The super keyword is how a subclass calls the parent constructor or parent methods, which makes class hierarchies easier to build and maintain.

Quick answer: Use extends to make one class inherit from another, then use super() in the subclass constructor before touching this. Use super.methodName() to reuse parent method logic inside an override.

Difficulty: Beginner

You'll understand this better if you know: basic class syntax, constructors, methods, and how objects store state in JavaScript.

1. What Is JavaScript Inheritance and super?

Inheritance is a way to build a new class from an existing class. The new class, called a subclass, gets the parent class’s methods and can add new behavior or replace existing behavior. The super keyword connects the two classes.

In practice, inheritance is useful when one type is a more specific version of another type, such as Car and ElectricCar, or Employee and Manager.

2. Why JavaScript Inheritance Matters

Inheritance helps you avoid repeating shared behavior across related objects. Instead of copying the same methods into several classes, you define them once in a parent class and reuse them in child classes.

It matters because it can make code easier to organize when multiple types share a common base. It also lets you add specialized behavior without losing the shared behavior.

You should not use inheritance just to share any code. If two classes are not truly related, composition or helper functions are often clearer than a deep inheritance tree.

3. Basic Syntax or Core Idea

Extending a parent class

The extends keyword tells JavaScript that one class inherits from another. The child class then gets the parent class’s prototype methods automatically.

class Animal {
  constructor(name) {
    this.name = name;
  }

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

class Dog extends Animal {
  bark() {
    return `${this.name} barks.`;
  }
}

This example creates a parent class and a subclass. Dog inherits speak() from Animal and adds its own bark() method.

Calling the parent constructor with super()

If the parent class needs initialization, the child constructor must call super(). That runs the parent setup before the child adds its own state.

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
}

super(name) passes the name argument to the parent constructor. Only after that can the child safely use this.

4. Step-by-Step Examples

Example 1: Inheriting a parent method

Here, the child class adds a new method but still uses the parent’s shared behavior.

class Animal {
  constructor(name) {
    this.name = name;
  }

  eat() {
    return `${this.name} is eating.`;
  }
}

class Cat extends Animal {
  meow() {
    return `${this.name} says meow.`;
  }
}

const cat = new Cat("Milo");
console.log(cat.eat());
console.log(cat.meow());

The Cat instance can call both inherited and own methods. This is the simplest form of inheritance.

Example 2: Overriding a method and using super.methodName()

A child class can replace a parent method while still reusing part of the parent’s logic.

class Vehicle {
  describe() {
    return "This is a vehicle.";
  }
}

class Car extends Vehicle {
  describe() {
    return `${super.describe()} It has four wheels.`;
  }
}

const car = new Car();
console.log(car.describe());

super.describe() runs the parent version of describe(). This is useful when the subclass needs to extend, not completely replace, the parent behavior.

Example 3: Adding child-specific state after super()

Subclass constructors often accept extra data. The parent handles the shared data, and the child handles the new fields.

class Employee {
  constructor(name) {
    this.name = name;
  }
}

class Manager extends Employee {
  constructor(name, teamSize) {
    super(name);
    this.teamSize = teamSize;
  }
}

const manager = new Manager("Ava", 8);

The parent class initializes the shared name property, and the child adds teamSize. This pattern keeps shared and specific state separate.

Example 4: Using inherited methods through instances

Inherited methods are available on the child instance as if they were defined there, but they actually come from the parent prototype chain.

class Account {
  constructor(balance) {
    this.balance = balance;
  }

  deposit(amount) {
    this.balance += amount;
    return this.balance;
  }
}

class SavingsAccount extends Account {
  addInterest(rate) {
    this.balance *= 1 + rate;
    return this.balance;
  }
}

const account = new SavingsAccount(100);
account.deposit(25);
account.addInterest(0.1);

This shows that inherited and child-specific methods work together on the same object. The child class does not lose the parent’s functionality.

5. Practical Use Cases

Inheritance works best when the child class really is a kind of parent class. If the relationship feels forced, prefer composition, where one object contains another object and delegates work to it.

6. Common Mistakes

Mistake 1: Using this before super() in a subclass constructor

In a derived class, JavaScript requires super() to run before you access this. This rule exists because the parent constructor must initialize the instance first.

Problem: This causes a runtime error such as ReferenceError: Must call super constructor in derived class before accessing 'this'.

class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Dog extends Animal {
  constructor(name) {
    this.name = name;
    super(name);
  }
}

Fix: Call super() first, then use this.

class Dog extends Animal {
  constructor(name) {
    super(name);
    this.breed = "unknown";
  }
}

The corrected version works because the parent constructor is responsible for initializing the subclass instance before any child-specific fields are assigned.

Mistake 2: Forgetting to call super() in a derived constructor

If a subclass defines its own constructor, it must call super() unless it extends a class with an empty constructor and does not use this. Most real subclasses need the parent initialization.

Problem: JavaScript throws an error because the derived constructor never initializes the instance through the parent class.

class Employee {
  constructor(name) {
    this.name = name;
  }
}

class Manager extends Employee {
  constructor(name) {
    this.role = "manager";
  }
}

Fix: Pass the needed arguments to super() before assigning child properties.

class Manager extends Employee {
  constructor(name) {
    super(name);
    this.role = "manager";
  }
}

This version works because the parent class can finish its own setup before the child adds extra state.

Mistake 3: Overriding a method and accidentally losing parent behavior

Sometimes a child method should add behavior instead of replacing it. If you override without calling the parent version, you may drop important logic.

Problem: The child method works, but it silently skips the parent method’s behavior, which can break formatting, logging, validation, or state updates.

class Report {
  format() {
    return "Base report format.";
  }
}

class SalesReport extends Report {
  format() {
    return "Sales report only.";
  }
}

Fix: Call the parent method with super when you want to extend its result.

class SalesReport extends Report {
  format() {
    return `${super.format()} Sales data included.`;
  }
}

The corrected version preserves the base behavior and adds the child-specific detail on top.

7. Best Practices

Practice 1: Use inheritance for true is-a relationships

Inheritance is strongest when the subclass can safely stand in for the parent class. If that relationship is not natural, your code will become fragile over time.

class Shape {
  area() {
    throw new Error("Implement in subclass");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }
}

This keeps the hierarchy meaningful. The parent class defines a shared contract, and each child provides its own implementation.

Practice 2: Keep parent constructors small and focused

A parent constructor should initialize shared state, not do too much work. Large constructors make subclasses harder to extend and test.

class User {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
}

When the constructor stays focused, child classes can reliably call super() and add their own state without surprising side effects.

Practice 3: Override only what needs changing

If a child class only needs a small change, reuse the parent method and adjust the result instead of rewriting everything.

class Greeter {
  message() {
    return "Hello";
  }
}

class FriendlyGreeter extends Greeter {
  message() {
    return `${super.message()} there!`;
  }
}

This approach keeps the subclass small and makes the intent obvious.

8. Limitations and Edge Cases

A common surprise is that inherited methods may appear to be “owned” by the child instance, but they are still found through prototypes behind the scenes.

9. Practical Mini Project

Let’s build a small notification system with a shared base class and two specialized subclasses. The parent class will handle common notification data, while the child classes customize how messages are formatted.

class Notification {
  constructor(recipient, message) {
    this.recipient = recipient;
    this.message = message;
  }

  summary() {
    return `To: ${this.recipient} | Message: ${this.message}`;
  }
}

class EmailNotification extends Notification {
  constructor(recipient, message, subject) {
    super(recipient, message);
    this.subject = subject;
  }

  summary() {
    return `${super.summary()} | Subject: ${this.subject}`;
  }
}

class SmsNotification extends Notification {
  summary() {
    return `SMS -> ${super.summary()}`;
  }
}

const email = new EmailNotification("[email protected]", "Your order has shipped", "Order Update");
const sms = new SmsNotification("Sam", "Your package is out for delivery");

console.log(email.summary());
console.log(sms.summary());

This mini project shows the full inheritance flow: a shared base class, child constructors, and method overrides that reuse parent logic through super.

10. Key Points

11. Practice Exercise

Expected output: The subclass should print the task title, completion state, and due date.

Hint: Put the shared fields in the parent constructor, call super() from the child constructor, and extend the parent method result instead of replacing it entirely.

class Task {
  constructor(title, completed) {
    this.title = title;
    this.completed = completed;
  }

  summary() {
    return `Task: ${this.title} | Completed: ${this.completed}`;
  }
}

class TimedTask extends Task {
  constructor(title, completed, dueDate) {
    super(title, completed);
    this.dueDate = dueDate;
  }

  summary() {
    return `${super.summary()} | Due: ${this.dueDate}`;
  }
}

const task = new TimedTask("Write docs", false, "2026-08-01");
console.log(task.summary());

12. Final Summary

JavaScript inheritance lets one class reuse and specialize another class through the extends keyword. This is a good fit when your objects have a clear parent-child relationship and share meaningful behavior.

The super keyword is the bridge between the subclass and the parent class. Use super() in a derived constructor to initialize the parent part of the object, and use super.methodName() when an overridden method needs to reuse parent logic.

When used carefully, inheritance can reduce duplication and make your class design easier to follow. The key is to keep the hierarchy shallow, the responsibilities clear, and the parent-child relationship genuinely logical.