In TypeScript, classes and inheritance play a crucial role in building robust and reusable code, allowing developers to create complex systems with a clear and maintainable structure. TypeScript, being a superset of JavaScript, brings the power of static typing to JavaScript's prototype-based inheritance model, enabling developers to leverage object-oriented programming (OOP) paradigms more effectively.
At its core, a class in TypeScript is a blueprint for creating objects. It encapsulates data for the object and methods to manipulate that data. TypeScript enhances JavaScript classes by adding type annotations and access modifiers, which help in catching errors early during development and enforcing encapsulation.
Defining Classes
To define a class in TypeScript, you use the class
keyword followed by the class name. Inside the class, you can define properties and methods. Here's a simple example:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m.`);
}
}
In this example, we have a class Animal
with a property name
and a constructor that initializes this property. The class also has a method move
which logs the movement of the animal.
Access Modifiers
TypeScript provides three access modifiers to control the visibility of class members: public
, private
, and protected
.
- Public: By default, all members of a class are public, meaning they can be accessed from anywhere.
- Private: Members marked as private can only be accessed within the class they are declared.
- Protected: Similar to private, but members declared as protected can also be accessed within subclasses.
Here's an example demonstrating these modifiers:
class Animal {
private name: string;
protected age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m.`);
}
}
In this example, the name
property is private, and the age
property is protected. This encapsulation ensures that sensitive data is not accessible from outside the class or its subclasses.
Inheritance
Inheritance is a fundamental concept in OOP that allows a class to inherit properties and methods from another class. In TypeScript, you use the extends
keyword to create a subclass. Here's how you can extend the Animal
class:
class Bird extends Animal {
fly(distance: number) {
console.log(`${this.name} flew ${distance}m.`);
}
}
In this example, Bird
is a subclass of Animal
. It inherits the properties and methods of Animal
and adds a new method fly
.
Method Overriding
Subclasses can override methods from their parent class to provide specific behavior. To override a method, you simply define a method in the subclass with the same name as the method in the parent class. Here's an example:
class Snake extends Animal {
move(distance: number = 5) {
console.log("Slithering...");
super.move(distance);
}
}
In this example, the move
method in the Snake
class overrides the move
method in the Animal
class. The super
keyword is used to call the parent class's method.
Abstract Classes
TypeScript allows you to define abstract classes, which cannot be instantiated directly. Abstract classes are used as base classes from which other classes can derive. They can contain implementation details and abstract methods that must be implemented by subclasses.
abstract class Department {
constructor(public name: string) {}
printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): void; // must be implemented in derived classes
}
In this example, Department
is an abstract class with an implemented method printName
and an abstract method printMeeting
. Any subclass of Department
must implement the printMeeting
method.
Interfaces and Classes
In TypeScript, interfaces can be used to define the shape of an object, and classes can implement interfaces. This allows for a contract that a class must adhere to. Here's an example:
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) {
this.currentTime = new Date();
}
setTime(d: Date) {
this.currentTime = d;
}
}
In this example, the Clock
class implements the ClockInterface
interface, ensuring that it provides a definition for the currentTime
property and the setTime
method.
Mixins
TypeScript supports mixins, a pattern that allows classes to be composed from reusable components. Mixins are useful when you want to share functionality between classes without using inheritance. Here's a simple example:
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
class Disposable {
isDisposed: boolean = false;
dispose() {
this.isDisposed = true;
}
}
class Activatable {
isActive: boolean = false;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
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]);
In this example, the SmartObject
class uses mixins to combine the functionality of the Disposable
and Activatable
classes.
Conclusion
Classes and inheritance in TypeScript provide a structured way to define and organize code, enabling developers to build scalable and maintainable applications. By leveraging TypeScript's type system, access modifiers, and features like abstract classes and interfaces, developers can create robust object-oriented designs that are both flexible and type-safe. Understanding these concepts is fundamental to mastering TypeScript and harnessing its full potential to improve JavaScript development.