Classes and Inheritance in TypeScript
TypeScript is an open-source programming language developed by Microsoft. It is a strict syntactical superset of JavaScript, and it adds static typing to the language. One of the key features TypeScript introduces is the concept of classes and inheritance, which allows for more robust and maintainable code.
Introduction to Classes
In programming, a class is a blueprint for creating objects. It defines properties and methods that the created objects will have. If you are familiar with Object-Oriented Programming (OOP), you might know that classes are the fundamental building blocks of OOP.
Here is a simple example of a class in TypeScript:
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } greet() { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } } let person = new Person("John", 30); person.greet();
In this example, we define a class Person
with two properties: name
and age
. We also define a constructor method that initializes these properties and a method greet
to print a greeting message. We then create an instance of the Person
class and call the greet
method.
Understanding Inheritance
Inheritance is a concept where one class can inherit properties and methods from another class. This helps in code reuse and makes it easier to create hierarchical relationships between classes.
In TypeScript, we can use the extends
keyword to create a class that inherits from another class:
class Employee extends Person { employeeId: number; constructor(name: string, age: number, employeeId: number) { super(name, age); this.employeeId = employeeId; } showEmployeeDetails() { console.log(`Employee ID: ${this.employeeId}, Name: ${this.name}, Age: ${this.age}`); } } let employee = new Employee("Jane", 28, 1001); employee.showEmployeeDetails();
In this example, we create a class Employee
that extends the Person
class. The Employee
class adds an additional property employeeId
and a method showEmployeeDetails
. Notice that in the constructor of the Employee
class, we call super(name, age)
to invoke the constructor of the Person
class.
Method Overriding
Method overriding is a feature that allows a subclass to provide a specific implementation of a method that is already defined in its superclass:
class Manager extends Employee { department: string; constructor(name: string, age: number, employeeId: number, department: string) { super(name, age, employeeId); this.department = department; } showEmployeeDetails() { console.log(`Employee ID: ${this.employeeId}, Name: ${this.name}, Age: ${this.age}, Department: ${this.department}`); } } let manager = new Manager("Alice", 35, 2001, "HR"); manager.showEmployeeDetails();
In this example, the Manager
class extends the Employee
class and overrides the showEmployeeDetails
method to include the department
property.
Access Modifiers
TypeScript provides three access modifiers: public
, private
, and protected
.
public
: Accessible from anywhere.private
: Accessible only within the class that defines it.protected
: Accessible within the class that defines it and any subclasses.
Here’s an example:
class Car { public make: string; private model: string; protected year: number; constructor(make: string, model: string, year: number) { this.make = make; this.model = model; this.year = year; } displayDetails() { console.log(`Make: ${this.make}, Model: ${this.model}, Year: ${this.year}`); } } class SportsCar extends Car { private topSpeed: number; constructor(make: string, model: string, year: number, topSpeed: number) { super(make, model, year); this.topSpeed = topSpeed; } displaySpeed() { console.log(`The top speed of this ${this.make} is ${this.topSpeed} mph.`); } } let myCar = new SportsCar("Ferrari", "488", 2021, 211); myCar.displayDetails(); myCar.displaySpeed();
In this example, the make
property is public and can be accessed from anywhere, the model
property is private and only accessible within the Car
class, and the year
property is protected and can be accessed within Car
and its subclasses.
Static Properties and Methods
Static properties and methods belong to the class itself rather than any instance of the class. Use the static
keyword to declare them:
class Utility { static PI: number = 3.14159; static calculateCircumference(diameter: number): number { return this.PI * diameter; } } console.log(Utility.PI); console.log(Utility.calculateCircumference(10));
In this example, PI
is a static property and calculateCircumference
is a static method. You can access static properties and methods using the class name itself.
Getters and Setters
Getters and setters provide a way to control access to the properties of a class:
class Person { private _name: string; constructor(name: string) { this._name = name; } get name(): string { return this._name; } set name(newName: string) { if(newName.length > 0) { this._name = newName; } else { console.log("Name cannot be empty."); } } } let person = new Person("John"); console.log(person.name); person.name = "Doe"; console.log(person.name);
In this example, the _name
property is private, but we use the get
and set
methods to access and modify it. This allows us to add validation logic in the setter.
Abstract Classes
An abstract class is a class that cannot be instantiated directly. It can have methods that must be implemented by derived classes:
abstract class Animal { abstract makeSound(): void; move(): void { console.log("The animal is moving."); } } class Dog extends Animal { makeSound() { console.log("Woof! Woof!"); } } let dog = new Dog(); dog.makeSound(); dog.move();
In this example, the Animal
class is abstract. The Dog
class extends Animal
and provides an implementation for the makeSound
method.
Interfaces and Classes
Interfaces can be used to define the structure of a class:
interface Shape { area(): number; } class Circle implements Shape { radius: number; constructor(radius: number) { this.radius = radius; } area(): number { return Math.PI * this.radius * this.radius; } } class Rectangle implements Shape { width: number; height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } area(): number { return this.width * this.height; } } let circle = new Circle(5); console.log(`Circle Area: ${circle.area()}`); let rectangle = new Rectangle(10, 20); console.log(`Rectangle Area: ${rectangle.area()}`);
In this example, Shape
is an interface that defines an area
method. The Circle
and Rectangle
classes implement this interface and provide their own implementations of the area
method.
Mixins
Mixins allow you to combine multiple classes into one. TypeScript uses a specific pattern to implement mixins, involving interface
and function
:
class Disposable { isDisposed: boolean = false; dispose() { this.isDisposed = true; console.log("Disposed"); } } class Activatable { isActive: boolean = false; activate() { this.isActive = true; console.log("Activated"); } deactivate() { this.isActive = false; console.log("Deactivated"); } } class SmartObject implements Disposable, Activatable { isDisposed: boolean = false; dispose: () => void; isActive: boolean = false; activate: () => void; deactivate: () => void; constructor() { setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500); } } applyMixins(SmartObject, [Disposable, Activatable]); function applyMixins(derivedCtor: any, baseCtors: any[]) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { derivedCtor.prototype[name] = baseCtor.prototype[name]; }); }); } let smartObj = new SmartObject(); setTimeout(() => smartObj.activate(), 1000); setTimeout(() => smartObj.dispose(), 2000);
In this example, we create two classes, Disposable
and Activatable
, and mix them into the SmartObject
class using the applyMixins
function. This allows SmartObject
to inherit methods from both classes.
Conclusion
Understanding classes and inheritance in TypeScript can significantly enhance your programming skills, especially in object-oriented programming. Classes provide a blueprint for creating objects, and inheritance allows you to create a hierarchical relationship between classes, promoting code reuse and maintainability. Whether you’re building simple applications or large-scale projects, mastering these concepts will help you write more robust and maintainable code.