Introduction
TypeScript provides a robust foundation for object-oriented programming, including the use of class inheritance. This tutorial delves into the principles of class inheritance in TypeScript with practical examples.
Understanding Basics of Class Inheritance
In object-oriented programming, inheritance enables new classes to receive, or inherit, properties and methods from existing classes. Let’s consider a simple class in TypeScript:
class Animal {
constructor(public name: string) {}
move(distanceInMeters: number): void {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
This Animal
class has a name and a method to move. We can subclass it to create more specific types of Animals.
class Snake extends Animal {
move(distanceInMeters = 5): void {
console.log('Slithering...');
super.move(distanceInMeters);
}
}
class Horse extends Animal {
move(distanceInMeters = 45): void {
console.log('Galloping...');
super.move(distanceInMeters);
}
}
Snake
and Horse
inherit from Animal and also define their move behavior. Notice the use of super.move()
; it calls the move method on the base class, Animal
.
Advanced Inheritance Patterns
We can also have abstract classes, which are base classes from which other classes may be derived, but cannot be instantiated on their own.
abstract class Quadruped {
abstract makeSound(): void;
walk(): void {
console.log('Walking on all fours.');
}
}
class Dog extends Quadruped {
makeSound(): void {
console.log('Woof! Woof!');
}
}
In this case, a subclass of Quadruped
must implement makeSound
since it’s an abstract method.
Tackling the Diamond Problem
TypeScript’s class system copy is based on single inheritance, which means each class can only inherit from one other class, effectively avoiding the ‘diamond problem’ of multiple inheritance. However, through interfaces and mixins, we can approximate the behavior of multiple inheritance;
type Constructor = new (...args: any[]) => T;
function Flyable(Base: TBase) {
return class extends Base {
fly() {
console.log('Flying!');
}
};
}
class Bird extends Flyable(Animal) {}
let pigeon = new Bird('Pigeon');
pigeon.fly(); // Flying!
pigeon.move(10); // Pigeon moved 10m.
Here, a Flyable
mixin provides flying functionality to the Bird
class, which continues to inherit from the base Animal
class.
Interfaces and Implementation
Interfaces in TypeScript can be used to define contracts within your code and are a way to ensure that a class meets a particular contract.
interface IFlyable {
fly(): void;
}
class Helicopter implements IFlyable {
fly() {
console.log('Helicopter is flying.');
}
}
This ensures that any Helicopter
class will have a fly
method implementation.
Generics and Inheritance
Generics provide a way to make classes, functions, and interfaces work with any data type. Combined with inheritance, generics promote reusability and type safety.
class Cage {
contents: T;
constructor(contents: T) {
this.contents = contents;
}
}
class Lion extends Animal {}
let lionCage = new Cage(new Lion('Simba'));
console.log(lionCage.contents.name); // Simba
The Cage
class is generic and works with any type. We can then define a Lion
cage that can only contain Lion
instances, preventing runtime errors.
Decorators and Class Inheritance
Decorators are a stage 2 proposal for JavaScript and are available in TypeScript. They can be used to modify classes and class members’ behaviors at design time.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return 'Hello, ' + this.greeting;
}
}
The @sealed
decorator prevents new properties from being added to the Greeter
class, and its prototype, and marks them as non-configurable.
Summary
TypeScript’s class inheritance allows developers to build a structured, maintainable, and scalable codebase. This guide explored the basic and advanced patterns of inheritance, abstract classes, mixins, interfaces, generics, and decorators.