TypeScript Advanced OOP Concepts

Object-Oriented Programming (OOP) is a programming paradigm centered around objects that combine both data (properties) and behavior (methods). TypeScript, being a statically typed superset of JavaScript, brings several advanced OOP features to the table that enhance code quality, maintainability, and scalability.

Classes and Inheritance in TypeScript

Overview of Classes in TypeScript

A class in TypeScript is a blueprint for creating objects (instances). It defines properties and methods that the objects will have. Classes bring structure to TypeScript by enabling code reuse and encapsulation of logic.

Let’s review a basic class example before moving into inheritance.

				
					class Person {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    introduce(): void {
        console.log(`Hi, I am ${this.name}, and I am ${this.age} years old.`);
    }
}

const person = new Person("Alice", 30);
person.introduce(); // Output: Hi, I am Alice, and I am 30 years old.

				
			

Explanation:

  • name and age are properties.
  • constructor() initializes the properties when a new object is created.
  • introduce() is a method that logs a message to the console.

Output:

				
					Hi, I am Alice, and I am 30 years old.
				
			

Inheritance in TypeScript

Inheritance allows one class (child class) to inherit the properties and methods of another class (parent class). This enables code reuse and the extension of class functionality.

Example:

				
					class Employee extends Person {
    jobTitle: string;

    constructor(name: string, age: number, jobTitle: string) {
        super(name, age);
        this.jobTitle = jobTitle;
    }

    work(): void {
        console.log(`${this.name} is working as a ${this.jobTitle}.`);
    }
}

const employee = new Employee("Bob", 35, "Software Engineer");
employee.introduce(); // Output: Hi, I am Bob, and I am 35 years old.
employee.work();      // Output: Bob is working as a Software Engineer.
				
			

Explanation:

  • extends is used to establish inheritance between Person and Employee.
  • The super() keyword is used to call the parent class’s constructor.
  • The Employee class inherits the introduce() method from Person.

Output:

				
					Hi, I am Bob, and I am 35 years old.
Bob is working as a Software Engineer.
				
			

Method Overriding

In TypeScript, a child class can override the methods of its parent class to provide specific functionality.

Example:

				
					class Manager extends Employee {
    constructor(name: string, age: number, jobTitle: string) {
        super(name, age, jobTitle);
    }

    // Override the work method
    work(): void {
        console.log(`${this.name} is managing the team.`);
    }
}

const manager = new Manager("Charlie", 40, "Project Manager");
manager.work(); // Output: Charlie is managing the team.
				
			

Explanation:

  • The Manager class overrides the work() method to provide its own implementation.

Output:

				
					Charlie is managing the team.
				
			

Encapsulation in TypeScript

Encapsulation is the concept of bundling data (properties) and methods that operate on the data within one unit, usually a class, and controlling the access to that data.

In TypeScript, encapsulation is achieved through access modifiers like:

  • public: The property or method is accessible everywhere.
  • private: The property or method is accessible only within the class it is declared.
  • protected: The property or method is accessible within the class and its subclasses.

Example:

				
					class BankAccount {
    public owner: string;
    private balance: number;

    constructor(owner: string, balance: number) {
        this.owner = owner;
        this.balance = balance;
    }

    public deposit(amount: number): void {
        this.balance += amount;
        console.log(`Deposited ${amount}. New balance: ${this.balance}`);
    }

    public withdraw(amount: number): void {
        if (amount <= this.balance) {
            this.balance -= amount;
            console.log(`Withdrew ${amount}. Remaining balance: ${this.balance}`);
        } else {
            console.log("Insufficient funds");
        }
    }

    private showBalance(): void {
        console.log(`Account balance: ${this.balance}`);
    }
}

const account = new BankAccount("Alice", 1000);
account.deposit(200);  // Deposited 200. New balance: 1200
account.withdraw(500); // Withdrew 500. Remaining balance: 700
// account.showBalance(); // Error: Property 'showBalance' is private.

				
			

Explanation:

  • balance is marked as private, so it can’t be accessed from outside the class.
  • The methods deposit and withdraw allow safe manipulation of the account balance.

Output:

				
					Deposited 200. New balance: 1200
Withdrew 500. Remaining balance: 700

				
			

Abstract Classes in TypeScript

An abstract class is a class that cannot be instantiated directly. It can only be used as a base class and typically contains one or more abstract methods that must be implemented in any derived class.

Abstract Class Example

				
					abstract class Animal {
    constructor(public name: string) {}

    abstract makeSound(): void;

    move(): void {
        console.log(`${this.name} is moving.`);
    }
}

class Dog extends Animal {
    makeSound(): void {
        console.log(`${this.name} says: Woof!`);
    }
}

const dog = new Dog("Buddy");
dog.makeSound(); // Output: Buddy says: Woof!
dog.move();      // Output: Buddy is moving.

				
			

Explanation:

  • Animal is an abstract class with an abstract method makeSound().
  • The Dog class extends Animal and provides an implementation for the makeSound() method.

Output:

				
					Buddy says: Woof!
Buddy is moving.
				
			

Interfaces in TypeScript

An interface in TypeScript defines a contract for classes to follow. It specifies what properties and methods a class should have but doesn’t provide implementations.

Interface Example

				
					interface Drivable {
    startEngine(): void;
    stopEngine(): void;
}

class Car implements Drivable {
    startEngine(): void {
        console.log("Engine started.");
    }

    stopEngine(): void {
        console.log("Engine stopped.");
    }
}

const myCar = new Car();
myCar.startEngine(); // Output: Engine started.
myCar.stopEngine();  // Output: Engine stopped.

				
			

Explanation:

  • Drivable is an interface that defines the methods startEngine and stopEngine.
  • The Car class implements this interface and provides the required method implementations.

Output:

				
					Engine started.
Engine stopped.
				
			

Polymorphism in TypeScript

Polymorphism is the ability of different classes to be treated as instances of the same class through inheritance. It allows one method to be used in different ways depending on the object calling it.

Example:

				
					class Shape {
    area(): number {
        return 0;
    }
}

class Rectangle extends Shape {
    constructor(public width: number, public height: number) {
        super();
    }

    area(): number {
        return this.width * this.height;
    }
}

class Circle extends Shape {
    constructor(public radius: number) {
        super();
    }

    area(): number {
        return Math.PI * this.radius * this.radius;
    }
}

let shapes: Shape[] = [new Rectangle(5, 10), new Circle(7)];

shapes.forEach((shape) => {
    console.log(shape.area());
});

				
			

Explanation:

  • Shape is the base class with an area() method.
  • Rectangle and Circle are subclasses that provide their own implementation of the area() method.
  • Polymorphism allows us to treat all shapes as instances of the Shape class and call the area() method without knowing the exact type of shape.

Output:

				
					50
153.93804002589985
				
			

Generics in TypeScript OOP

Generics allow you to create reusable components that can work with a variety of types while retaining type safety.

Generic Classes

				
					class Box<T> {
    content: T;

    constructor(content: T) {
        this.content = content;
    }

    getContent(): T {
        return this.content;
    }
}

let numberBox = new Box<number>(100);
console.log(numberBox.getContent()); // Output: 100

let stringBox = new Box<string>("TypeScript");
console.log(stringBox.getContent()); // Output: TypeScript

				
			

Explanation:

  • Box<T> is a generic class where T can be any type.
  • We created two instances of Box, one for a number and one for a string, demonstrating type flexibility.

Output:

				
					100
TypeScript

				
			

Decorators in TypeScript

Decorators are a special kind of declaration that can be attached to a class, method, or property to modify its behavior. Decorators are frequently used in frameworks like Angular.

Example:

				
					function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
        console.log(`Called: ${propertyKey} with args: ${JSON.stringify(args)}`);
        return originalMethod.apply(this, args);
    };
}

class MathOperations {
    @log
    add(a: number, b: number): number {
        return a + b;
    }
}

const math = new MathOperations();
math.add(5, 3); // Output: Called: add with args: [5,3]
				
			

Explanation:

  • The log decorator logs the method name and arguments every time the add() method is called.
  • Decorators modify the behavior of the method without altering its original implementation.

Output:

				
					Called: add with args: [5,3]

				
			

We’ve covered advanced OOP concepts in Inheritance: Allowing one class to inherit properties and methods from another. Encapsulation: Protecting internal data using access modifiers. Abstract Classes: Providing a blueprint for subclasses. Interfaces: Defining contracts for classes. Polymorphism: Allowing different classes to be treated as the same type. Generics: Enabling type-safe reuse of code. Decorators: Modifying class behavior in a clean and declarative way. Happy Coding!❤️

Table of Contents