Skip to content

Extending Classes

Video Lecture

Section Video Links
Extending Classes Extending Classes Extending Classes 

Overview

You can extend any existing class templates by using the extends keyword. The new class definition will be made up of the original class, but can optionally include its own new bespoke constructor, properties and/or methods. The new class definition is known as the derived class or subclass.

Extending a class is a different concept than implementing an interface. An Interface describes the property types and method signature rules that the class implementing it should comply with. Extending a class copies the base class template and allows you to refine or specialize it further.

With the derived class, the original class being extended is called the base or super class. It is a class that may have methods and properties that are common, but another class can be created from it that extends from this base/super class and has the option to override the constructor, methods and properties. The derived class also has the option to create additional methods and properties specific for its own needs. If the base class is using an interface, then any derived class will already comply provided that the base class was already correctly complying with its chosen interface.

Extended Class Example 1

In example 1, the Cat and Dog classes are derived from the Animal base/super class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Animal {
    name: string
    age: number

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    feed(food: string, amount: number): void {
        console.log(
            'Feeding ' +
                this.name +
                ' the ' +
                this.constructor.name +
                ' ' +
                amount +
                ' kg of ' +
                food
        )
    }
}

class Cat extends Animal {}

class Dog extends Animal {}

const CAT = new Cat('Cosmo', 8)
const DOG = new Dog('Rusty', 12)
CAT.feed('Fish', 0.1)
DOG.feed('Beef', 0.25)

Replace ./src/test.ts with this code above, compile and execute it.

tsc -p ./src
node ./dist/test.js

Outputs

Feeding Cosmo the Cat, 0.1kg of Fish
Feeding Rusty the Dog, 0.25kg of Beef

Note that the Cat and Dog classes don't actually contain any properties, constructor or methods. They extend the Animal base class, so they contain the necessary constructor, properties and methods already.

Both Cat and Dog can call the feed method, by using this.feed which will redirect to the base/super class version of the feed method.

Also note that if you compiled the JavaScript code using the ES3 target, you will see that the extends keywords does not exist natively in the output but specialized functions for it have been added. The JavaScript extends keyword was introduced to the JavaScript language in the ES6/ES2015 updates.

As an exercise, you can change the target parameter in your tsconfig.json to ES3, e.g.,

{
    "compilerOptions": {
        "strict": true,
        "target": "ES3",
        "module": "CommonJS",
        "outDir": "../dist",
        "rootDir": "./",
        "moduleResolution": "node"
    },
    "include": ["**/*.ts"]
}

Stop any TSC Watch process that you have running (Use CTRL-C in your terminal window to stop it), and manually run tsc .\src\test.ts. You may need to use tsc.cmd .\src\test.ts if using PowerShell.

Now look at the compiled JavaScript in ./dist/test.js and you will see that it is significantly different code and much harder to read. The extends functionality has been written as functions into the compiled JavaScript since ES3 does not understand the extends keyword natively.

Once finished change the target back to ES2015. Note that you can also set it to ES6, since ES6 and ES2015 are the same. Restart your TSC process in watch mode if you prefer to use this technique. Review the ES6 version of ./dist/test.js and you will see it is now much easier to read. NodeJS supports ES6 syntax, so there is no need to target ES3.

Also take note of the usage of this.constructor.name in the feed method in the above example. Since the CAT and DOG objects are instantiated from their own new Cat and Dog classes, rather than the Animal class directly, their constructor name is either CAT or DOG. For a test, you could instantiate the Dog from the Animal class directly and see the difference in the printed output.

29
30
//const DOG = new Dog('Rusty', 12)
const DOG = new Animal('Rusty', 12)

Outputs

Feeding Rusty the Animal 0.25 kg of Beef

Extended Class Example 2

In example 1 above, the Cat and Dog properties, methods and constructors were created automatically behind the scenes. It was unnecessary to override there constructor, property values and feed method. They simply used whatever they got when they extended the Animal class. If you wanted to customize the constructor/properties/methods, then you can override them.

Look at the altered Cat class below. While it extends the Animal class, it also has its own additional isHungry property, it overrides the constructor and also overrides the feed method with its own bespoke implementations. The Dog class remains unchanged.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Animal {
    name: string
    age: number

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    feed(food: string, amount: number): void {
        console.log(
            'Feeding ' +
                this.name +
                ' the ' +
                this.constructor.name +
                ' ' +
                amount +
                ' kg of ' +
                food
        )
    }
}

class Cat extends Animal {
    isHungry: boolean
    constructor(name: string, age: number, isHungry: boolean) {
        super(name, age)
        this.isHungry = isHungry
    }

    feed(food: string, amount: number): void {
        if (this.isHungry) {
            super.feed(food, amount)
        } else {
            console.log(
                this.name +
                    ' the ' +
                    this.constructor.name +
                    ' is not hungry'
            )
        }
    }
}

class Dog extends Animal {}

const CAT = new Cat('Cosmo', 8, false)
const DOG = new Dog('Rusty', 12)
CAT.feed('Fish', 0.1)
DOG.feed('Beef', 0.25)

Replace ./src/test.ts with this code above, compile and execute it.

tsc -p ./src
node ./dist/test.js

Outputs

Cosmo the Cat is not hungry
Feeding Rusty the Dog 0.25 kg of Beef

See how the Cat class has its own overridden constructor, and instantiated a new variable that it can use called isHungry.

Also note how the constructor also calls the super() method along with any attributes required by the super classes constructor. The super() method points to the base/super classes constructor.

In derived classes, it is compulsory to call the base classes super() method in the constructor otherwise you get an error,

Constructors for derived classes must contain a 'super' call

In the overridden feed method, I have also called the super.feed(food, amount) method. This is calling the base classes feed method directly. It is not necessary to call any methods of the base class directly in your overridden methods if you don't really want to. I did it to show that it is still possible. If the cat is not hungry, then the base classes feed method won't actually be called at all.

Extended Class Example 3

Be aware if overriding any properties in your derived classes. They will now have preference over the equivalent properties in the base class if you refer to them using the this keyword. If you have overridden a method or property, and you still want to reference the base classes copy of the method or property, then you can use the super keyword as I did in the Cats feed method in example 2.

In the below example I have declared an instance of Cat to default with the name of Emmy. So using this.name will now point to the Cats overridden name property, whereas before, it would have redirected to the base classes name property.

Example 3 tries to highlight the difference of when you are referring to a base classes property/method versus a subclasses copy of an overridden property/method.

See that I attempt to set a default of "Emmy" for the Cat classes name property. And then when it is instantiated I pass in the name "Cosmo". I then call the super() method passing in the name "Cosmo". While this does update the super classes copy of the name property, it does not update the subclasses overridden copy of name. So when I print this.name in the feed method, it points to the subclasses copy of name which is still set as "Emmy" by default.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Animal {
    name: string
    age: number

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    feed(food: string, amount: number): void {
        console.log(
            'Feeding ' +
                this.name +
                ' the ' +
                this.constructor.name +
                ' ' +
                amount +
                ' kg of ' +
                food
        )
    }
}

class Cat extends Animal {
    isHungry: boolean
    name = 'Emmy'
    constructor(name: string, age: number, isHungry: boolean) {
        super(name, age)
        this.isHungry = isHungry
    }

    feed(food: string, amount: number): void {
        if (this.isHungry) {
            super.feed(food, amount)
        } else {
            console.log(
                this.name +
                    ' the ' +
                    this.constructor.name +
                    ' is not hungry'
            )
        }
    }
}

class Dog extends Animal {}

const CAT = new Cat('Cosmo', 8, false)
const DOG = new Dog('Rusty', 12)
CAT.feed('Fish', 0.1)
DOG.feed('Beef', 0.25)

Outputs

Emmy the Cat is not hungry
Feeding Rusty the Dog 0.25 kg of Beef

If I wanted to use the new name "Cosmo" to override the predefined "Emmy" in the derived class, I would need to set it explicitly somewhere in the derived class, for example as I do in the Cat constructor this.name = name. See updated code below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Animal {
    name: string
    age: number

    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }

    feed(food: string, amount: number): void {
        console.log(
            'Feeding ' +
                this.name +
                ' the ' +
                this.constructor.name +
                ' ' +
                amount +
                ' kg of ' +
                food
        )
    }
}

class Cat extends Animal {
    isHungry: boolean
    name = 'Emmy'
    constructor(name: string, age: number, isHungry: boolean) {
        super(name, age)
        this.isHungry = isHungry
        this.name = name
    }

    feed(food: string, amount: number): void {
        if (this.isHungry) {
            super.feed(food, amount)
        } else {
            console.log(
                this.name +
                    ' the ' +
                    this.constructor.name +
                    ' is not hungry'
            )
        }
    }
}

class Dog extends Animal {}

const CAT = new Cat('Cosmo', 8, false)
const DOG = new Dog('Rusty', 12)
CAT.feed('Fish', 0.1)
DOG.feed('Beef', 0.25)

Outputs

Cosmo the Cat is not hungry
Feeding Rusty the Dog 0.25 kg of Beef