Classes and Inheritance in TypeScript







Classes and Inheritance in TypeScript

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.



Leave a Reply

Your email address will not be published. Required fields are marked *