Class Inheritance in TypeScript
Strong Object-Oriented Programming (OOP) capabilities, such as native support for classes and inheritance, and optional static typing are added by TypeScript, a potent superset of JavaScript. In contrast, earlier iterations of JavaScript mostly used inheritance chains based on prototypes. TypeScript classes offer a consistent paradigm that is already familiar to OOP developers and a helpful structural abstraction.
Classes: Basics using the Keyword
A class encapsulates activity (functions or methods) and data (fields) and acts as a blueprint for building things.
Simple Class Structure
In TypeScript, a simple class structure uses the class keyword and the class name, which is often written in PascalCase:
class Point {
// Fields
x: number;
y: number;
// Constructor
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
// Method (function)
add(point: Point) {
return new Point(this.x + point.x, this.y + point.y);
}
}
Fields and Access Modifiers
Fields are variables specified within a class that represent the information about an object. They are also referred to as properties or attributes.
Three access modifiers are supported by TypeScript that control class members’ visibility:
- Public (Default): Available on instances, within the class, and by child classes. In accordance with JavaScript’s convenience, a member is implicitly public if no access modifier is supplied.
- protected: Accessible on class instances outside of the hierarchy, but not on the defining class or its offspring.
- Private: Only accessible within the class that defines it.
The resulting JavaScript has no runtime significance for these modifiers, which are only present in the TypeScript type system and enforce compile-time errors if misused (unless newer private fields using #
are used, which is not covered by the private
keyword in the type system).
A basic class that displays fields, for instance:
class Car {
public position: number = 0; // Public field, initialized
private speed: number = 42; // Private field
protected safetyRating: number = 5; // Protected field
// ... constructor and methods
}
Constructors
Memory allocation and class field initialisation are handled by the constructor
, a specific function. There is nothing wrong with a class not having a constructor defined.
This
keyword, which refers to the current instance, is used in the constructor of the previous Point
example to explicitly take arguments and assign them to the instance fields:
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
Declaring a field and using the constructor to initialise it is a typical TypeScript pattern. TypeScript provides a handy shortcut called Parameter Properties. If you prefix a constructor parameter with readonly
or an access modifier (public
, private
, or protected
), the parameter is immediately initialised from the argument and defined as a field on the class.
Using this shorthand, the lengthy Car
class setup can be made simpler:
Original (Verbose) | Simplified (Parameter Property Shorthand) |
typescript class Car { public position: number; protected speed: number; constructor(position: number, speed: number) { this.position = position; this.speed = speed; } } | typescript class Car { constructor(public position: number, protected speed: number) {} } |
The shortcut takes a lot less code than the other structure, yet both produce the same functional JavaScript output.
Basic Class Inheritance
Due to TypeScript’s support for single inheritance, an existing class (the base/parent class) can pass on attributes and methods to a new class (the derived/child class).
Using Extends
To accomplish inheritance, use the extends
keyword.
// Base Class: Point (from earlier)
class Point {
x: number;
y: number;
// ... constructor and add method
}
// Derived Class: Point3D extends Point
class Point3D extends Point {
z: number;
// ... constructor and overridden methods
}
The behaviour and structure of Point
are carried over into the derived class Point3D
.
Using Super
The constructor and method overriding are the two main situations in which the super
keyword is essential when working with inherited classes.
Calling the Parent Constructor ()
When a derived class specifies its own constructor, it needs to use super()
to invoke the constructor of the parent class. This guarantees that the base class correctly configures the fields that are required on this
instance. Additionally, the derived constructor must access this
reference after the call to super()
.
In Point3D
, we call super(x, y)
to initialize the inherited x
and y
properties defined in Point
:
class Point3D extends Point {
z: number;
constructor(x: number, y: number, z: number) {
super(x, y); // Must call parent constructor first
this.z = z; // Then add additional initialization
}
// ...
}
Accessing Overridden Methods ()
A derived class can simply declare a method with the same name as a parent member function to override it. The implementation of the base class function can still be accessed and used within the overridden method by utilising the super.
syntax.
In Point3D
, the add
method is overridden to handle the third dimension (z
), but it reuses the original Point.add
functionality for the x
and y
dimensions:
class Point3D extends Point {
// ... constructor ...
add(point: Point3D) {
var point2D = super.add(point); // Calls Point.add(point)
return new Point3D(point2D.x, point2D.y, this.z + point.z);
}
}
Complete Inheritance
The full TypeScript code illustrating the fundamental class structure, the extends
keyword, and the use of super
for constructors and methods is shown below:
// TypeScript Code (Point.ts)
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
add(point: Point) {
return new Point(this.x + point.x, this.y + point.y);
}
}
class Point3D extends Point {
z: number;
constructor(x: number, y: number, z: number) {
super(x, y); // Call parent constructor
this.z = z;
}
// Override the add method
add(point: Point3D) {
var point2D = super.add(point); // Use parent implementation for x, y
return new Point3D(point2D.x, point2D.y, this.z + point.z);
}
}
// Usage and Expected Output:
var p1 = new Point(5, 10);
var p2 = new Point(10, 20);
var p_sum = p1.add(p2);
// p_sum will be { x: 15, y: 30 }
var p3d_1 = new Point3D(1, 2, 3);
var p3d_2 = new Point3D(10, 20, 30);
var p3d_sum = p3d_1.add(p3d_2);
// p3d_sum will be { x: 11, y: 22, z: 33 }
// Output simulation (accessing properties for verification)
console.log(`P_sum: {x: ${p_sum.x}, y: ${p_sum.y}}`);
console.log(`P3D_sum: {x: ${p3d_sum.x}, y: ${p3d_sum.y}, z: ${p3d_sum.z}}`);
Output:
P_sum: {x: 15, y: 30}
P3D_sum: {x: 11, y: 22, z: 33}
TypeScript is able to maintain a clean, object-oriented model while compiling down to idiomatic JavaScript thanks to its structural support for classes. In older targets like ES5, it usually uses function patterns and prototypes for inheritance. An Immediately Invoked Function Expression (IIFE) in the produced JavaScript is frequently used to encapsulate the update of the class prototype.