Extending Classes
Video Lecture
Section | Video Links |
---|---|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
Outputs
Cosmo the Cat is not hungry
Feeding Rusty the Dog 0.25 kg of Beef