We use cookies to enhance your experience on the site
CodeWorlds

Class Inheritance (extends)

In our Jurassic Park we have different types of dinosaurs that share many common traits, but also have their unique properties and behaviors. This is where class inheritance becomes extremely useful. It allows us to create hierarchies of classes, where derived (child) classes inherit properties and methods from the base (parent) class, while also adding or overriding their own functionality.

Basics of inheritance

In ES6 we use the

extends
keyword to inherit from a base class. Let's see how we can create a hierarchy of dinosaur classes:

1// Base class for all dinosaurs
2class Dinosaur {
3  constructor(species, weight, diet) {
4    this.species = species;
5    this.weight = weight;
6    this.diet = diet;
7    this.health = 100;
8    this.isActive = true;
9  }
10
11  makeSound() {
12    return `${this.species} makes a sound!`;
13  }
14
15  feed(food) {
16    this.health += 10;
17    return `${this.species} was fed ${food}. Health: ${this.health}`;
18  }
19}
20
21// Derived class for predators
22class Carnivore extends Dinosaur {
23  constructor(species, weight, dangerLevel) {
24    // Calling the base class constructor
25    super(species, weight, 'carnivore');
26    this.dangerLevel = dangerLevel; // New property specific to predators
27  }
28
29  // Override method from base class
30  makeSound() {
31    return `${this.species} roars menacingly! ROAR!`;
32  }
33
34  // New method specific to predators
35  hunt() {
36    return `${this.species} hunts prey!`;
37  }
38}
39
40// Derived class for herbivores
41class Herbivore extends Dinosaur {
42  constructor(species, weight, preferredPlants) {
43    super(species, weight, 'herbivore');
44    this.preferredPlants = preferredPlants; // Array of preferred plants
45  }
46
47  // Override method from base class
48  makeSound() {
49    return `${this.species} makes a gentle, low sound.`;
50  }
51
52  // New method specific to herbivores
53  graze() {
54    return `${this.species} peacefully nibbles vegetation.`;
55  }
56}

Now we can create different types of dinosaurs:

1// Creating a predator instance
2const trex = new Carnivore('Tyrannosaurus Rex', 8000, 'Extreme');
3
4// Creating a herbivore instance
5const triceratops = new Herbivore('Triceratops', 6000, ['ferns', 'cycads', 'conifers']);
6
7console.log(trex.makeSound()); // "Tyrannosaurus Rex roars menacingly! ROAR!"
8console.log(triceratops.makeSound()); // "Triceratops makes a gentle, low sound."
9
10// Both classes inherit the feed method from the base class
11console.log(trex.feed('meat')); // "Tyrannosaurus Rex was fed meat. Health: 110"
12console.log(triceratops.feed('leaves')); // "Triceratops was fed leaves. Health: 110"
13
14// Methods specific to derived classes
15console.log(trex.hunt()); // "Tyrannosaurus Rex hunts prey!"
16console.log(triceratops.graze()); // "Triceratops peacefully nibbles vegetation."

The
super
keyword

The

super
keyword has two main uses in class inheritance:

  1. Calling the superclass constructor:
    super(...)
    calls the base class constructor.
  2. Accessing superclass methods:
    super.method()
    calls a method from the base class.

Calling the superclass constructor

In the constructor of a derived class, calling

super()
is required before using
this
:

1class Dinosaur {
2  constructor(species) {
3    this.species = species;
4  }
5}
6
7class Velociraptor extends Dinosaur {
8  constructor(name, speed) {
9    // We must call super() first
10    super('Velociraptor');
11
12    // Now we can use this
13    this.name = name;
14    this.speed = speed;
15  }
16}

If we don't define a constructor in the derived class, JavaScript automatically creates one that calls

super()
with the passed arguments:

1class SimpleHerbivore extends Dinosaur {
2  // Constructor is implicitly defined as:
3  // constructor(...args) {
4  //   super(...args);
5  // }
6}
7
8// Works, even without an explicit constructor
9const stegosaurus = new SimpleHerbivore('Stegosaurus', 5000, 'herbivore');

Accessing superclass methods

We can use

super.method()
to call methods from the base class, even if they are overridden in the derived class:

1class Velociraptor extends Carnivore {
2  constructor(name, speed) {
3    super('Velociraptor', 100, 'High');
4    this.name = name;
5    this.speed = speed;
6  }
7
8  makeSound() {
9    // First call the original sound from the Carnivore class
10    const baseSound = super.makeSound();
11    // Then add the specific sound for Velociraptor
12    return `${baseSound} Additionally, it makes a characteristic hiss!`;
13  }
14
15  hunt() {
16    return `${super.hunt()} ${this.name} attacks at speed ${this.speed} km/h!`;
17  }
18}
19
20const blue = new Velociraptor('Blue', 70);
21console.log(blue.makeSound());
22// "Velociraptor roars menacingly! ROAR! Additionally, it makes a characteristic hiss!"
23
24console.log(blue.hunt());
25// "Velociraptor hunts prey! Blue attacks at speed 70 km/h!"

Multi-level inheritance

Inheritance can be multi-level, meaning a class can inherit from a class that itself is a derived class:

1// Base class
2class Dinosaur {
3  constructor(species) {
4    this.species = species;
5    this.extinct = true;
6  }
7
8  basicInfo() {
9    return `${this.species} - extinct: ${this.extinct ? 'Yes' : 'No'}`;
10  }
11}
12
13// First level of inheritance
14class Theropod extends Dinosaur {
15  constructor(species) {
16    super(species);
17    this.bipedal = true; // All theropods are bipedal
18    this.carnivorous = true; // All theropods are carnivorous
19  }
20
21  locomotion() {
22    return `${this.species} moves on ${this.bipedal ? 'two' : 'four'} legs.`;
23  }
24}
25
26// Second level of inheritance
27class Dromaeosaurid extends Theropod {
28  constructor(species, feathered = true) {
29    super(species);
30    this.feathered = feathered; // Most dromaeosaurids had feathers
31  }
32
33  description() {
34    return `${this.species} is a ${this.feathered ? 'feathered' : 'non-feathered'} dromaeosaurid.`;
35  }
36}
37
38// Third level of inheritance
39class Velociraptor extends Dromaeosaurid {
40  constructor(name) {
41    super('Velociraptor mongoliensis');
42    this.name = name;
43    this.dangerLevel = 'Extreme';
44  }
45
46  hunt() {
47    return `${this.name} hunts in an organized group, using its intelligence and speed!`;
48  }
49}
50
51// Creating instances
52const delta = new Velociraptor('Delta');
53
54// Access to methods from all levels of the hierarchy
55console.log(delta.basicInfo()); // From Dinosaur
56console.log(delta.locomotion()); // From Theropod
57console.log(delta.description()); // From Dromaeosaurid
58console.log(delta.hunt()); // From Velociraptor

Checking type and inheritance hierarchy

In JavaScript we can check if an object is an instance of a specific class using the

instanceof
operator:

1const rex = new Carnivore('Tyrannosaurus Rex', 8000, 'Extreme');
2const tric = new Herbivore('Triceratops', 6000, ['ferns', 'cycads']);
3const raptor = new Velociraptor('Charlie');
4
5console.log(rex instanceof Carnivore); // true
6console.log(rex instanceof Dinosaur); // true
7console.log(rex instanceof Herbivore); // false
8
9console.log(raptor instanceof Velociraptor); // true
10console.log(raptor instanceof Dromaeosaurid); // true
11console.log(raptor instanceof Theropod); // true
12console.log(raptor instanceof Dinosaur); // true

Limitations of inheritance

In JavaScript a class can only inherit from one base class — there is no multiple inheritance. If we need functionality from multiple different sources, we must consider other patterns such as composition or mixins.

1// This is NOT allowed in JavaScript
2// class ImpossibleDino extends Carnivore, SwimmingCreature {}

Mixins as an alternative to multiple inheritance

Mixins are a way to add functionality from different sources to a class:

1// Mixin definitions as objects with methods
2const SwimmingAbility = {
3  swim() {
4    return `${this.species} swims in the water!`;
5  },
6  dive() {
7    return `${this.species} dives underwater!`;
8  }
9};
10
11const FlyingAbility = {
12  fly() {
13    return `${this.species} soars into the air!`;
14  },
15  land() {
16    return `${this.species} lands on the ground.`;
17  }
18};
19
20// Function adding mixins to a class prototype
21function applyMixins(targetClass, ...mixins) {
22  mixins.forEach(mixin => {
23    Object.getOwnPropertyNames(mixin).forEach(name => {
24      targetClass.prototype[name] = mixin[name];
25    });
26  });
27}
28
29// Creating the base class
30class Dinosaur {
31  constructor(species) {
32    this.species = species;
33  }
34}
35
36// Creating classes with different abilities
37class Mosasaurus extends Dinosaur {
38  constructor() {
39    super('Mosasaurus');
40    this.habitat = 'ocean';
41  }
42}
43
44class Pteranodon extends Dinosaur {
45  constructor() {
46    super('Pteranodon');
47    this.wingspan = 7; // wingspan in meters
48  }
49}
50
51class Spinosaurus extends Dinosaur {
52  constructor() {
53    super('Spinosaurus');
54    this.hasGill = true; // gills
55  }
56}
57
58// Applying mixins
59applyMixins(Mosasaurus, SwimmingAbility);
60applyMixins(Pteranodon, FlyingAbility);
61applyMixins(Spinosaurus, SwimmingAbility, FlyingAbility); // This one can both swim and fly (hypothetically)
62
63// Creating instances
64const mosa = new Mosasaurus();
65const ptero = new Pteranodon();
66const spino = new Spinosaurus();
67
68// Using abilities from mixins
69console.log(mosa.swim()); // "Mosasaurus swims in the water!"
70console.log(ptero.fly()); // "Pteranodon soars into the air!"
71console.log(spino.swim()); // "Spinosaurus swims in the water!"
72console.log(spino.fly()); // "Spinosaurus soars into the air!"

Abstract base classes

In some cases we want to create a base class that should not be instantiated by itself, but serves only as a template for derived classes. In TypeScript we can use the

abstract
keyword, but in plain JavaScript we can simulate this behavior:

1class AbstractDinosaur {
2  constructor(species) {
3    // We ensure no one creates an instance of this class directly
4    if (this.constructor === AbstractDinosaur) {
5      throw new Error("Cannot instantiate abstract class!");
6    }
7
8    this.species = species;
9  }
10
11  // Method that child classes should implement
12  makeSound() {
13    throw new Error("Method makeSound() must be implemented in the child class!");
14  }
15
16  // Common method implemented in the base class
17  getSpecies() {
18    return this.species;
19  }
20}
21
22// Derived class correctly implements the abstract method
23class Diplodocus extends AbstractDinosaur {
24  constructor() {
25    super('Diplodocus');
26  }
27
28  makeSound() {
29    return "Deep, powerful humming.";
30  }
31}
32
33// Let's try to create instances
34try {
35  const abstractDino = new AbstractDinosaur('Generic'); // Will throw error
36} catch (error) {
37  console.log(error.message); // "Cannot instantiate abstract class!"
38}
39
40const diplo = new Diplodocus();
41console.log(diplo.makeSound()); // "Deep, powerful humming."
42console.log(diplo.getSpecies()); // "Diplodocus"
43
44// Derived class without implementation of abstract method
45class IncompleteImplementation extends AbstractDinosaur {
46  constructor() {
47    super('Incomplete');
48  }
49
50  // Missing makeSound() implementation
51}
52
53const incomplete = new IncompleteImplementation();
54try {
55  incomplete.makeSound(); // Will throw error
56} catch (error) {
57  console.log(error.message); // "Method makeSound() must be implemented in the child class!"
58}

Inheritance in TypeScript

In TypeScript, inheritance is more explicit, thanks to static typing and additional features such as abstract classes and access modifiers:

1// Abstract base class in TypeScript
2abstract class Dinosaur {
3  protected species: string;
4  protected health: number;
5  public isActive: boolean;
6
7  constructor(species: string) {
8    this.species = species;
9    this.health = 100;
10    this.isActive = true;
11  }
12
13  // Abstract method that child classes must implement
14  abstract makeSound(): string;
15
16  // Implemented method that child classes inherit
17  public getHealth(): number {
18    return this.health;
19  }
20
21  // Protected method that child classes can use, but is not accessible from outside
22  protected heal(amount: number): void {
23    this.health += amount;
24    if (this.health > 100) this.health = 100;
25  }
26}
27
28class TRex extends Dinosaur {
29  private attackPower: number;
30
31  constructor(name: string, attackPower: number) {
32    super('Tyrannosaurus Rex');
33    this.attackPower = attackPower;
34  }
35
36  // Implementation of abstract method
37  makeSound(): string {
38    return "POWERFUL ROAR!";
39  }
40
41  // Additional method specific to TRex
42  attack(): string {
43    return `T-Rex attacks with force ${this.attackPower}!`;
44  }
45
46  // Using the protected method from the base class
47  feed(): void {
48    this.heal(20);
49    console.log(`T-Rex was fed and recovered health. Current health: ${this.getHealth()}`);
50  }
51}

Summary

Class inheritance in ES6 is a powerful mechanism that allows us to create object hierarchies and share code between them. In our Jurassic Park, we used inheritance to:

  1. Create a common base class
    Dinosaur
    with basic functionality
  2. Divide dinosaurs into subclasses such as
    Carnivore
    and
    Herbivore
  3. Create specific species as child classes
  4. Add various abilities and behaviors using mixins
  5. Use abstract base classes to ensure consistency of implementation

Remember a few important things:

  • In JavaScript you can only inherit from one base class
  • Calling
    super()
    is required in the constructor of a derived class before using
    this
  • You can use
    super.method()
    to call a method from the base class
  • The
    instanceof
    operator lets you check if an object is an instance of a specific class or its base class
  • Mixins are an alternative to multiple inheritance

Class inheritance is one of the basic concepts of object-oriented programming and gives us powerful tools to structure code in a logical and maintainable way.

Go to CodeWorlds