DEV Community

Cover image for The Four Pillars of Object-Oriented Programming in TypeScript
coder7475
coder7475

Posted on • Edited on

The Four Pillars of Object-Oriented Programming in TypeScript

Object-Oriented Programming (OOP) is built on four fundamental principles, often referred to as the "pillars" of OOP. These principles help developers create modular, maintainable, and scalable software systems. The four pillars are:

  1. Encapsulation
  2. Abstraction
  3. Inheritance
  4. Polymorphism

Let’s explore each pillar with practical TypeScript examples.

1. Encapsulation

Encapsulation bundles data and methods into a class while restricting direct access to internal details, using access modifiers and getters/setters. It hides complexity and protects state integrity.

Purpose: Safeguards data from invalid modifications, improves maintainability, and supports modularity.

Example:

class BankAccount {
    private balance: number = 0;

    public deposit(amount: number): void {
        if (amount > 0) this.balance += amount;
    }

    public withdraw(amount: number): void {
        if (amount > 0 && amount <= this.balance) this.balance -= amount;
    }

    public getBalance(): number {
        return this.balance;
    }
}

const account = new BankAccount();
account.deposit(100);
account.withdraw(50);
console.log(account.getBalance());  // Output: 50
Enter fullscreen mode Exit fullscreen mode

Benefits: Enhances security, reduces dependencies on internal implementations.

2. Abstraction

Abstraction conceals intricate details, exposing only essential functionalities. It is realized through abstract classes or interfaces, emphasizing "what" over "how."

Purpose: Simplifies complexity, boosts readability, and facilitates loose coupling.

Example using an abstract class:

abstract class Shape {
    abstract calculateArea(): number;
}

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

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

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

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

const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach(shape => console.log(shape.calculateArea()));
// Output: 78.53981633974483 \n 24
Enter fullscreen mode Exit fullscreen mode

Benefits: Promotes reusability and easier updates.

For interfaces (another abstraction tool):

interface Shape {
    calculateArea(): number;
}

class Circle implements Shape {
    // Implementation as above
}
Enter fullscreen mode Exit fullscreen mode

Interfaces define contracts without implementations, supporting multiple inheritance.

Difference Between Abstract Class and Interface: Abstract classes can include implemented methods and are suited for shared behavior in related classes (single inheritance only). Interfaces mandate full implementation and allow multiple implementations for unrelated classes.

3. Inheritance

Inheritance enables a child class to acquire properties and methods from a parent class, fostering code reuse and hierarchical structures.

Purpose: Minimizes duplication, models real-world relationships, and supports extension.

Example:

class Animal {
    constructor(protected name: string) {}

    move(distance: number): void {
        console.log(`${this.name} moved ${distance} meters.`);
    }
}

class Dog extends Animal {
    constructor(name: string) { super(name); }

    bark(): void {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog('Buddy');
dog.move(10);  // Output: Buddy moved 10 meters.
dog.bark();    // Output: Woof! Woof!
Enter fullscreen mode Exit fullscreen mode

Benefits: Enhances extensibility, but caution against overuse to avoid fragile hierarchies; prefer composition where appropriate.

4. Polymorphism

Polymorphism permits objects of varied classes to be handled via a common interface, with behavior varying by type. It includes compile-time (overloading) and runtime (overriding) forms.

Purpose: Increases flexibility and adaptability.

Types:

  • Compile-time (Static Binding/Method Overloading): Methods with identical names but differing parameters; resolved at compile time.
  • Runtime (Dynamic Binding/Method Overriding): Child classes redefine parent methods; resolved at runtime using virtual methods.

Overloading example:

class MessageFormatter {
  // Overload signatures
  format(message: string): string;
  format(message: string, userId: number): string;
  format(message: string, userId: number, timestamp: Date): string;

  // Single implementation
  format(message: string, userId?: number, timestamp?: Date): string {
    let formatted = message;

    if (userId !== undefined) {
      formatted = `[User ${userId}] ${formatted}`;
    }
    if (timestamp !== undefined) {
      formatted = `${formatted} (at ${timestamp.toISOString()})`;
    }

    return formatted;
  }
}

const formatter = new MessageFormatter();

console.log(formatter.format("System started"));
// Output: System started

console.log(formatter.format("User logged in", 42));
// Output: [User 42] User logged in

console.log(formatter.format("File uploaded", 42, new Date()));
// Output: [User 42] File uploaded (at 2025-08-20T13:10:15.000Z)

Enter fullscreen mode Exit fullscreen mode

Overriding example (using virtual-like behavior via method overriding):

abstract class Payment {
  abstract process(amount: number): void;
}

class CreditCardPayment extends Payment {
  process(amount: number): void {
    console.log(`Processing credit card payment of $${amount}`);
  }
}

class PayPalPayment extends Payment {
  process(amount: number): void {
    console.log(`Processing PayPal payment of $${amount}`);
  }
}

class CryptoPayment extends Payment {
  process(amount: number): void {
    console.log(`Processing cryptocurrency payment of $${amount}`);
  }
}

const payments: Payment[] = [
  new CreditCardPayment(),
  new PayPalPayment(),
  new CryptoPayment(),
];

payments.forEach((payment) => payment.process(100));

// Output:
// Processing credit card payment of $100
// Processing PayPal payment of $100
// Processing cryptocurrency payment of $100

Enter fullscreen mode Exit fullscreen mode

What is a Virtual Method?: A method in a base class that can be overridden in child classes to enable runtime polymorphism.

Difference Between Overloading and Overriding:

  • Overloading: Same name, different parameters, same class, compile-time resolution.
  • Overriding: Same signature, different classes (child overrides parent), runtime resolution.

Benefits: Aligns with the Open/Closed Principle, allowing extension without modification.

Summary

  • Encapsulation: Protects data with controlled access (e.g., BankAccount).
  • Abstraction: Hides complexity, exposing only essentials (e.g., Shape).
  • Inheritance: Reuses and extends code (e.g., Animal and Dog).
  • Polymorphism: Enables flexible behavior (e.g., makeSound() variations).

These pillars, implemented effectively in TypeScript, empower developers to build robust, scalable systems. Let me know if you’d like further clarification or additional examples!

Top comments (0)