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:
- Encapsulation
- Abstraction
- Inheritance
- 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
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
Benefits: Promotes reusability and easier updates.
For interfaces (another abstraction tool):
interface Shape {
calculateArea(): number;
}
class Circle implements Shape {
// Implementation as above
}
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!
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)
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
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
andDog
). -
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)