Skip to main content

Inheritance

What is inheritance?

Inheritance lets one class build on another class. The child class gets behavior from the parent class and can add or replace behavior of its own.

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

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

class Dog extends Animal {
wagTail() {
return `${this.name} wags its tail.`;
}
}

const dog = new Dog("Maple");

console.log(dog.speak()); // "Maple makes a sound."
console.log(dog.wagTail()); // "Maple wags its tail."

Dog inherits the constructor and speak() method from Animal. It also adds its own wagTail() method.

What happens:

  1. class Dog extends Animal makes Dog a child class of Animal.
  2. new Dog("Maple") uses the inherited Animal constructor.
  3. The dog instance can use methods from both Animal and Dog.

Why inheritance matters

Inheritance can reduce duplication when several classes share the same core behavior.

It works best when the relationship is truly an is-a relationship.

A Dog is an Animal. A SavingsAccount is an Account. A VideoLesson is a Lesson.

If the relationship feels more like has-a, composition is usually a better fit.

Rule of thumb: use inheritance when the child is a more specific kind of the parent. Use composition when one object simply uses another object.

Extending a class

Use extends to create a child class.

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

getProfile() {
return this.name;
}
}

class AdminUser extends User {
deletePost(postId) {
return `Deleted post ${postId}`;
}
}

const admin = new AdminUser("Dirk");

console.log(admin.getProfile()); // "Dirk"
console.log(admin.deletePost(123)); // "Deleted post 123"

AdminUser inherits the constructor and getProfile() method from User.

It also adds its own deletePost() method.

The inheritance mental model

When JavaScript looks for a method, it starts on the instance's class. If it does not find the method there, it looks up the inheritance chain.

class Animal {
eat() {
return "Eating";
}
}

class Dog extends Animal {
bark() {
return "Woof";
}
}

const dog = new Dog();

console.log(dog.bark()); // "Woof"
console.log(dog.eat()); // "Eating"

bark() comes from Dog. eat() comes from Animal.

Calling the parent constructor

If a child class defines its own constructor, it must call super() before using this.

class Lesson {
constructor(title) {
this.title = title;
}
}

class VideoLesson extends Lesson {
constructor(title, durationInMinutes) {
super(title);
this.durationInMinutes = durationInMinutes;
}
}

const lesson = new VideoLesson("Objects", 12);

console.log(lesson.title); // "Objects"
console.log(lesson.durationInMinutes); // 12

super(title) runs the parent constructor.

After that, the child constructor can set its own properties.

In a child constructor, super() must run before you use this.

The following code without super() first would cause an error:

class VideoLesson extends Lesson {
constructor(title, durationInMinutes) {
// This would cause an error because `super()` has not run yet:
// this.durationInMinutes = durationInMinutes;

super(title);
this.durationInMinutes = durationInMinutes;
}
}

Overriding methods

A child class can define a method with the same name as a parent method.

This is called overriding.

class Notification {
send() {
return "Sending notification.";
}
}

class EmailNotification extends Notification {
send() {
return "Sending email notification.";
}
}

const notification = new EmailNotification();

console.log(notification.send()); // "Sending email notification."

The child version replaces the parent version for instances of the child class.

Calling a parent method

Use super.methodName() when the child method should reuse the parent behavior.

class Report {
print() {
return "Printing report";
}
}

class DetailedReport extends Report {
print() {
return `${super.print()} with details`;
}
}

const report = new DetailedReport();

console.log(report.print()); // "Printing report with details"

This is useful when the child class wants to extend behavior instead of replacing it completely.

Inheritance chains

A class can inherit from a class that already inherits from another class.

class Shape {
describe() {
return "This is a shape.";
}
}

class Rectangle extends Shape {}

class Square extends Rectangle {}

const square = new Square();

console.log(square.describe()); // "This is a shape."

This works, but deep inheritance chains can become difficult to understand.

Prefer shallow inheritance.

Composition as an alternative

Composition means giving an object another object or function to use.

class Logger {
log(message) {
console.log(message);
}
}

class OrderService {
constructor(logger) {
this.logger = logger;
}

createOrder(item) {
this.logger.log(`Creating order for ${item}`);
}
}

const service = new OrderService(new Logger());

service.createOrder("coffee"); // Logs "Creating order for coffee"

OrderService has a logger.

It does not need to inherit from Logger.

Composition is usually more flexible because you can swap the helper object without changing the class hierarchy.

When inheritance is a poor fit

Inheritance is easy to overuse. If the parent and child relationship is not clear, the code can become harder to change.

class Logger {
log(message) {
console.log(message);
}
}

// Weak model: an OrderService is not really a Logger.
class OrderService extends Logger {
createOrder(item) {
this.log(`Creating order for ${item}`);
}
}

This only uses inheritance to borrow a helper method. Composition is clearer:

class OrderService {
constructor(logger) {
this.logger = logger;
}

createOrder(item) {
this.logger.log(`Creating order for ${item}`);
}
}

If the class only wants to use another object's behavior, give it that object instead of extending it.

Common patterns

Specialized versions of a shared concept

Use inheritance for specialized versions of a shared concept.

class Lesson {
constructor(title) {
this.title = title;
}

getLabel() {
return this.title;
}
}

class QuizLesson extends Lesson {
constructor(title, questionCount) {
super(title);
this.questionCount = questionCount;
}

getLabel() {
return `${super.getLabel()} (${this.questionCount} questions)`;
}
}

const lesson = new QuizLesson("Inheritance", 5);

console.log(lesson.getLabel()); // "Inheritance (5 questions)"

QuizLesson is a kind of Lesson, so inheritance fits.

Different behavior for the same action

Use method overriding when child classes need different behavior for the same action.

class PaymentMethod {
getFee() {
return 0;
}
}

class CreditCardPayment extends PaymentMethod {
getFee() {
return 2.5;
}
}

class BankTransferPayment extends PaymentMethod {
getFee() {
return 0.5;
}
}

const cardPayment = new CreditCardPayment();
const transferPayment = new BankTransferPayment();

console.log(cardPayment.getFee()); // 2.5
console.log(transferPayment.getFee()); // 0.5

Each payment method understands the same action, but the details differ.

Reusing part of the parent behavior

Use super.methodName() when the parent behavior is still useful.

class Message {
format() {
return "Message";
}
}

class UrgentMessage extends Message {
format() {
return `Urgent: ${super.format()}`;
}
}

const message = new UrgentMessage();

console.log(message.format()); // "Urgent: Message"

Best practices

  • Use inheritance for clear is-a relationships: A Dog is an Animal; an OrderService is not a Logger.
  • Keep inheritance trees shallow: One parent-child layer is easier to understand than a long chain.
  • Call super() first in child constructors: Use it before touching this.
  • Use super.methodName() to extend behavior: Use it when the parent method is still helpful.
  • Do not inherit only to share a helper method: Use composition or a plain function instead.
  • Watch for repeated overrides: If subclasses override most parent methods, the model may be fighting the problem.

Summary

Inheritance lets child classes reuse and specialize parent class behavior. Use extends to create a child class and super to call parent constructors or methods. Inheritance works best for clear is-a relationships, but composition is often better when behavior needs to be mixed, swapped, or reused across unrelated classes.