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.
- extends creates an inheritance relationship between two classes.
- super() calls the parent class constructor.
- super.methodName() calls a parent method from an overridden child method.
- Inheritance is based on JavaScript’s prototype system, even though the syntax looks class-based.
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
- Modeling a base entity and specialized variants, such as Shape with Circle and Rectangle.
- Building shared UI or domain behavior, such as a base FormField class with specialized input types.
- Creating common infrastructure objects, such as a base Repository with specialized database access methods.
- Reusing validation or formatting logic across closely related objects.
- Overriding a parent method to refine behavior while keeping the original implementation available through super.
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
- Inheritance is single in JavaScript classes: a class can extend only one parent class.
- The super keyword works differently in constructors and methods. In constructors it calls the parent constructor; in methods it calls a parent method.
- Static methods can also use super to call inherited static behavior, but the lookup follows the class itself rather than an instance.
- Subclassing built-in objects like Array or Error works in modern JavaScript, but behavior can be more subtle than with plain classes.
- Inheritance does not copy methods into the child class. They are resolved through the prototype chain, so changing the parent prototype affects all subclasses that use it.
- Deep inheritance trees can be harder to understand than small, focused classes with composition.
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
- extends creates inheritance between two classes.
- super() must run before using this in a derived constructor.
- super.methodName() lets a child method reuse parent logic.
- Inheritance is best for true parent-child relationships, not just code reuse.
- JavaScript inheritance uses the prototype chain under class syntax.
11. Practice Exercise
- Create a base class named Task with title and completed properties.
- Create a subclass named TimedTask that adds a dueDate property.
- Add a summary() method to Task.
- Override summary() in TimedTask and reuse the parent result with super.summary().
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.