All topics
General · Learning hub

OOP notes for developers

Master OOP with a curated set of 10 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore General notes
OOP

Inheritance and Polymorphism

Inheritance and Polymorphism Master advanced OOP concepts that enable code reuse and flexible design through inheritance hierarchies and polymorphic behavior. I

Inheritance and Polymorphism

Master advanced OOP concepts that enable code reuse and flexible design through inheritance hierarchies and polymorphic behavior.

Inheritance Hierarchies

Inheritance creates a parent-child relationship between classes, allowing child classes to inherit and extend parent functionality.

// Base class
class User {
  protected id: string;
  protected email: string;
  protected createdAt: Date;

  constructor(id: string, email: string) {
    this.id = id;
    this.email = email;
    this.createdAt = new Date();
  }

  public getProfile(): string {
    return `User: ${this.email} (ID: ${this.id})`;
  }

  public canAccessResource(resourceId: string): boolean {
    return false; // Base users can't access resources
  }
}

// First level inheritance
class RegisteredUser extends User {
  protected username: string;
  protected lastLogin: Date;

  constructor(id: string, email: string, username: string) {
    super(id, email);
    this.username = username;
    this.lastLogin = new Date();
  }

  public override getProfile(): string {
    return `${this.username} (${this.email})`;
  }

  public override canAccessResource(resourceId: string): boolean {
    return true; // Registered users can access basic resources
  }

  public updateLastLogin(): void {
    this.lastLogin = new Date();
  }
}

// Second level inheritance
class PremiumUser extends RegisteredUser {
  private subscriptionEndDate: Date;
  private features: string[];

  constructor(
    id: string,
    email: string,
    username: string,
    subscriptionMonths: number
  ) {
    super(id, email, username);
    this.subscriptionEndDate = new Date();
    this.subscriptionEndDate.setMonth(
      this.subscriptionEndDate.getMonth() + subscriptionMonths
    );
    this.features = ["ad-free", "HD-streaming", "offline-mode"];
  }

  public override getProfile(): string {
    return `⭐ PREMIUM: ${super.getProfile()}`;
  }

  public override canAccessResource(resourceId: string): boolean {
    // Premium users can access all resources
    return this.isSubscriptionActive();
  }

  private isSubscriptionActive(): boolean {
    return new Date() < this.subscriptionEndDate;
  }

  public getFeatures(): string[] {
    return [...this.features];
  }
}

class AdminUser extends RegisteredUser {
  private permissions: Set<string>;

  constructor(id: string, email: string, username: string) {
    super(id, email, username);
    this.permissions = new Set(["read", "write", "delete", "admin"]);
  }

  public override getProfile(): string {
    return `🛡️ ADMIN: ${super.getProfile()}`;
  }

  public override canAccessResource(resourceId: string): boolean {
    return true; // Admins can access everything
  }

  public hasPermission(permission: string): boolean {
    return this.permissions.has(permission);
  }

  public deleteUser(userId: string): void {
    console.log(`Admin deleted user: ${userId}`);
  }
}

// Usage
const users: User[] = [
  new User("1", "guest@example.com"),
  new RegisteredUser("2", "user@example.com", "john_doe"),
  new PremiumUser("3", "premium@example.com", "jane_premium", 12),
  new AdminUser("4", "admin@example.com", "admin_user")
];

users.forEach(user => {
  console.log(user.getProfile());
  console.log(`Can access: ${user.canAccessResource("resource-1")}`);
});

Method Overriding

Child classes can provide their own implementation of methods inherited from parent classes.

class PaymentProcessor {
  protected processingFee: number = 0;

  processPayment(amount: number): number {
    const fee = this.calculateFee(amount);
    const total = amount + fee;
    console.log(`Processing payment: $${amount} + $${fee} fee = $${total}`);
    return total;
  }

  protected calculateFee(amount: number): number {
    return 0; // Base class has no fee
  }
}

class CreditCardProcessor extends PaymentProcessor {
  protected override calculateFee(amount: number): number {
    return amount * 0.029 + 0.30; // 2.9% + $0.30
  }
}

class PayPalProcessor extends PaymentProcessor {
  protected override calculateFee(amount: number): number {
    return amount * 0.034 + 0.49; // 3.4% + $0.49
  }
}

class WireTransferProcessor extends PaymentProcessor {
  protected override calculateFee(amount: number): number {
    return 25.00; // Flat fee
  }
}

// Same method, different behavior based on object type
function processAllPayments(processors: PaymentProcessor[], amount: number): void {
  processors.forEach(processor => {
    processor.processPayment(amount);
  });
}

const processors = [
  new PaymentProcessor(),
  new CreditCardProcessor(),
  new PayPalProcessor(),
  new WireTransferProcessor()
];

processAllPayments(processors, 100);

Abstract Classes

Abstract classes cannot be instantiated directly and are meant to be extended. They can contain abstract methods that must be implemented by child classes.

abstract class DatabaseConnection {
  protected connectionString: string;
  protected isConnected: boolean = false;

  constructor(connectionString: string) {
    this.connectionString = connectionString;
  }

  // Abstract methods - must be implemented by subclasses
  abstract connect(): Promise<void>;
  abstract disconnect(): Promise<void>;
  abstract query(sql: string): Promise<any[]>;
  abstract execute(sql: string): Promise<number>;

  // Concrete method - shared by all connections
  public getStatus(): string {
    return this.isConnected ? "Connected" : "Disconnected";
  }

  protected log(message: string): void {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}

class MySQLConnection extends DatabaseConnection {
  async connect(): Promise<void> {
    this.log(`Connecting to MySQL: ${this.connectionString}`);
    // Simulate connection
    await new Promise(resolve => setTimeout(resolve, 100));
    this.isConnected = true;
    this.log("MySQL connected successfully");
  }

  async disconnect(): Promise<void> {
    this.log("Disconnecting from MySQL");
    this.isConnected = false;
  }

  async query(sql: string): Promise<any[]> {
    if (!this.isConnected) throw new Error("Not connected");
    this.log(`Executing query: ${sql}`);
    return []; // Simulated result
  }

  async execute(sql: string): Promise<number> {
    if (!this.isConnected) throw new Error("Not connected");
    this.log(`Executing command: ${sql}`);
    return 1; // Simulated affected rows
  }
}

class PostgreSQLConnection extends DatabaseConnection {
  async connect(): Promise<void> {
    this.log(`Connecting to PostgreSQL: ${this.connectionString}`);
    await new Promise(resolve => setTimeout(resolve, 100));
    this.isConnected = true;
    this.log("PostgreSQL connected successfully");
  }

  async disconnect(): Promise<void> {
    this.log("Disconnecting from PostgreSQL");
    this.isConnected = false;
  }

  async query(sql: string): Promise<any[]> {
    if (!this.isConnected) throw new Error("Not connected");
    this.log(`PG Query: ${sql}`);
    return [];
  }

  async execute(sql: string): Promise<number> {
    if (!this.isConnected) throw new Error("Not connected");
    this.log(`PG Execute: ${sql}`);
    return 1;
  }
}

// Usage
async function databaseExample() {
  const connections: DatabaseConnection[] = [
    new MySQLConnection("mysql://localhost:3306/mydb"),
    new PostgreSQLConnection("postgresql://localhost:5432/mydb")
  ];

  for (const conn of connections) {
    await conn.connect();
    await conn.query("SELECT * FROM users");
    console.log(`Status: ${conn.getStatus()}`);
    await conn.disconnect();
  }
}

Interfaces vs Abstract Classes

Understanding when to use interfaces versus abstract classes:

// Interface: Contract for implementation
interface Printable {
  print(): void;
  getPrintData(): string;
}

// Interface: Can extend multiple interfaces
interface Saveable {
  save(): Promise<void>;
  load(): Promise<void>;
}

// A class can implement multiple interfaces
class Document implements Printable, Saveable {
  constructor(
    private content: string,
    private filename: string
  ) {}

  print(): void {
    console.log(this.getPrintData());
  }

  getPrintData(): string {
    return `Document: ${this.filename}\n${this.content}`;
  }

  async save(): Promise<void> {
    console.log(`Saving ${this.filename}...`);
    // Save logic
  }

  async load(): Promise<void> {
    console.log(`Loading ${this.filename}...`);
    // Load logic
  }
}

// Abstract class: Provides base implementation
abstract class Report {
  protected title: string;
  protected generatedAt: Date;

  constructor(title: string) {
    this.title = title;
    this.generatedAt = new Date();
  }

  // Concrete method with implementation
  public getMetadata(): string {
    return `Report: ${this.title} (Generated: ${this.generatedAt.toISOString()})`;
  }

  // Abstract method - must be implemented
  abstract generateContent(): string;
  abstract export(format: string): string;
}

class SalesReport extends Report {
  constructor(
    title: string,
    private salesData: any[]
  ) {
    super(title);
  }

  generateContent(): string {
    return `Sales Report Content: ${this.salesData.length} records`;
  }

  export(format: string): string {
    return `Exporting sales report to ${format}`;
  }
}

// When to use which:
// - Use INTERFACE when you need multiple inheritance or pure contracts
// - Use ABSTRACT CLASS when you have shared implementation and state

Composition over Inheritance

While inheritance is powerful, composition is often preferred for building flexible systems.

// Instead of deep inheritance, use composition
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

class FileLogger implements Logger {
  constructor(private filename: string) {}
  
  log(message: string): void {
    console.log(`[FILE: ${this.filename}] ${message}`);
  }
}

interface Cache {
  get(key: string): any;
  set(key: string, value: any): void;
}

class MemoryCache implements Cache {
  private store = new Map();

  get(key: string): any {
    return this.store.get(key);
  }

  set(key: string, value: any): void {
    this.store.set(key, value);
  }
}

// Using composition
class UserService {
  constructor(
    private logger: Logger,
    private cache: Cache
  ) {}

  async getUser(id: string): Promise<any> {
    this.logger.log(`Fetching user: ${id}`);
    
    // Check cache first
    const cached = this.cache.get(`user_${id}`);
    if (cached) {
      this.logger.log(`User ${id} found in cache`);
      return cached;
    }

    // Fetch from database (simulated)
    const user = { id, name: "John Doe" };
    this.cache.set(`user_${id}`, user);
    this.logger.log(`User ${id} cached`);
    
    return user;
  }
}

// Flexible - can swap implementations easily
const service1 = new UserService(
  new ConsoleLogger(),
  new MemoryCache()
);

const service2 = new UserService(
  new FileLogger("app.log"),
  new MemoryCache()
);
OOP

Best Practices

OOP Best Practices Follow these best practices to write clean, maintainable, and effective object-oriented code. 1. Single Responsibility Principle A class shou

OOP Best Practices

Follow these best practices to write clean, maintainable, and effective object-oriented code.

1. Single Responsibility Principle

A class should have only one reason to change - it should do one thing and do it well.

// ❌ BAD: Class doing too much
class User {
  constructor(
    public name: string,
    public email: string
  ) {}

  save(): void {
    // Database logic - NOT the user's responsibility
    console.log("Saving to database...");
  }

  sendEmail(message: string): void {
    // Email logic - NOT the user's responsibility
    console.log(`Sending email to ${this.email}: ${message}`);
  }

  generateReport(): string {
    // Reporting logic - NOT the user's responsibility
    return `User Report for ${this.name}`;
  }
}

// ✅ GOOD: Separate responsibilities
class User {
  constructor(
    public name: string,
    public email: string
  ) {}

  getFullName(): string {
    return this.name;
  }
}

class UserRepository {
  save(user: User): void {
    console.log(`Saving user: ${user.name}`);
    // Database logic here
  }

  find(email: string): User | null {
    // Find logic here
    return null;
  }
}

class EmailService {
  send(to: string, message: string): void {
    console.log(`Sending email to ${to}: ${message}`);
    // Email logic here
  }
}

class UserReportGenerator {
  generate(user: User): string {
    return `User Report for ${user.name}`;
    // Reporting logic here
  }
}

2. Favor Composition Over Inheritance

Use composition to combine behaviors rather than creating deep inheritance hierarchies.

// ❌ BAD: Deep inheritance
class Animal {}
class Bird extends Animal {}
class FlyingBird extends Bird {}
class SwimmingFlyingBird extends FlyingBird {} // Getting complex!

// ✅ GOOD: Composition
interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Duck implements Flyable, Swimmable {
  fly(): void {
    console.log("Duck flying");
  }

  swim(): void {
    console.log("Duck swimming");
  }
}

class Penguin implements Swimmable {
  swim(): void {
    console.log("Penguin swimming");
  }
}

class Eagle implements Flyable {
  fly(): void {
    console.log("Eagle soaring");
  }
}

3. Program to Interfaces, Not Implementations

Depend on abstractions rather than concrete implementations.

// ❌ BAD: Depending on concrete class
class MySQLDatabase {
  query(sql: string): any[] {
    return [];
  }
}

class UserService {
  private db: MySQLDatabase; // Tightly coupled

  constructor() {
    this.db = new MySQLDatabase();
  }

  getUsers(): any[] {
    return this.db.query("SELECT * FROM users");
  }
}

// ✅ GOOD: Depending on interface
interface Database {
  query(sql: string): any[];
}

class MySQLDatabase implements Database {
  query(sql: string): any[] {
    console.log("MySQL query:", sql);
    return [];
  }
}

class PostgreSQLDatabase implements Database {
  query(sql: string): any[] {
    console.log("PostgreSQL query:", sql);
    return [];
  }
}

class UserService {
  constructor(private db: Database) {} // Flexible!

  getUsers(): any[] {
    return this.db.query("SELECT * FROM users");
  }
}

// Easy to swap implementations
const service1 = new UserService(new MySQLDatabase());
const service2 = new UserService(new PostgreSQLDatabase());

4. Keep Classes Small and Focused

If a class has too many methods or properties, it probably needs to be split.

// ❌ BAD: God class doing everything
class OrderManager {
  createOrder() {}
  updateOrder() {}
  deleteOrder() {}
  calculateTotal() {}
  applyDiscount() {}
  processPayment() {}
  sendConfirmationEmail() {}
  updateInventory() {}
  generateInvoice() {}
  calculateShipping() {}
  // ... too many responsibilities!
}

// ✅ GOOD: Focused classes
class Order {
  constructor(
    public id: string,
    public items: OrderItem[],
    public customerId: string
  ) {}

  calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.getTotal(), 0);
  }
}

class OrderRepository {
  save(order: Order): void {}
  find(id: string): Order | null { return null; }
  delete(id: string): void {}
}

class DiscountService {
  apply(order: Order, code: string): number { return 0; }
}

class PaymentProcessor {
  process(order: Order, method: string): boolean { return true; }
}

class OrderNotificationService {
  sendConfirmation(order: Order): void {}
}

class InventoryService {
  update(order: Order): void {}
}

class InvoiceGenerator {
  generate(order: Order): string { return ""; }
}

class ShippingCalculator {
  calculate(order: Order): number { return 0; }
}

5. Use Meaningful Names

Classes, methods, and properties should have clear, descriptive names.

// ❌ BAD: Unclear names
class Mgr {
  private d: Data[];
  
  proc(): void {}
  get(): Data[] { return this.d; }
}

// ✅ GOOD: Clear names
class UserManager {
  private users: User[];
  
  processUserRegistration(): void {}
  getAllUsers(): User[] { return this.users; }
  findUserByEmail(email: string): User | null { return null; }
}

// Class names should be nouns
class PaymentProcessor {} // Good
class ProcessPayment {}   // Bad (verb)

// Method names should be verbs
getUser()      // Good
createOrder()  // Good
user()         // Bad (noun)
order()        // Bad (noun)

6. Minimize Coupling

Classes should depend on as few other classes as possible.

// ❌ BAD: Tight coupling
class EmailService {
  send(to: string, message: string): void {
    const smtp = new SMTPClient("smtp.gmail.com", 587);
    smtp.connect();
    smtp.send(to, message);
    smtp.disconnect();
  }
}

// ✅ GOOD: Loose coupling through dependency injection
interface MailTransport {
  send(to: string, message: string): void;
}

class SMTPTransport implements MailTransport {
  constructor(
    private host: string,
    private port: number
  ) {}

  send(to: string, message: string): void {
    console.log(`SMTP: Sending to ${to}`);
  }
}

class SendGridTransport implements MailTransport {
  constructor(private apiKey: string) {}

  send(to: string, message: string): void {
    console.log(`SendGrid: Sending to ${to}`);
  }
}

class EmailService {
  constructor(private transport: MailTransport) {}

  send(to: string, message: string): void {
    this.transport.send(to, message);
  }
}

// Easy to test and swap implementations
const emailService = new EmailService(
  new SMTPTransport("smtp.gmail.com", 587)
);

7. Follow the Law of Demeter (Principle of Least Knowledge)

Don't talk to strangers - a method should only call methods on: itself, its parameters, objects it creates, or its direct fields.

// ❌ BAD: Violates Law of Demeter (train wreck)
const userName = order.getCustomer().getAddress().getCity().getName();

// ✅ GOOD: Ask, don't dig
class Order {
  getCustomerCityName(): string {
    return this.customer.getCityName();
  }
}

class Customer {
  getCityName(): string {
    return this.address.getCityName();
  }
}

const userName = order.getCustomerCityName();

Summary

  • Keep classes small and focused on a single responsibility

  • Favor composition over inheritance for flexibility

  • Program to interfaces to reduce coupling

  • Use meaningful, descriptive names

  • Minimize dependencies between classes

  • Encapsulate what varies

  • Follow the Law of Demeter

OOP

Core Principles

Object-Oriented Programming: Core Principles Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" that contain data and

Object-Oriented Programming: Core Principles

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" that contain data and code. It organizes software design around data, or objects, rather than functions and logic.

The Four Pillars of OOP

1. Encapsulation

Encapsulation is the bundling of data and methods that operate on that data within a single unit (class). It restricts direct access to some of an object's components, which is a means of preventing accidental interference and misuse.

class BankAccount {
  private balance: number;
  private accountNumber: string;

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

  // Public method to access private data
  public getBalance(): number {
    return this.balance;
  }

  // Public method to modify private data safely
  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
      console.log(`Deposited $${amount}. New balance: $${this.balance}`);
    } else {
      console.log("Deposit amount must be positive");
    }
  }

  public withdraw(amount: number): boolean {
    if (amount > 0 && amount <= this.balance) {
      this.balance -= amount;
      console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
      return true;
    }
    console.log("Insufficient funds or invalid amount");
    return false;
  }
}

// Usage
const myAccount = new BankAccount("12345", 1000);
myAccount.deposit(500);
console.log(myAccount.getBalance()); // 1500
// myAccount.balance = 10000; // ERROR: Cannot access private property

2. Abstraction

Abstraction means hiding complex implementation details and showing only the necessary features of an object. It helps reduce programming complexity and effort.

// Abstract class defining the interface
abstract class Vehicle {
  protected brand: string;

  constructor(brand: string) {
    this.brand = brand;
  }

  // Abstract method - must be implemented by derived classes
  abstract startEngine(): void;
  abstract stopEngine(): void;

  // Concrete method - shared by all vehicles
  public displayInfo(): void {
    console.log(`This is a ${this.brand} vehicle`);
  }
}

// Concrete implementation
class Car extends Vehicle {
  startEngine(): void {
    console.log(`${this.brand} car engine started with key turn`);
  }

  stopEngine(): void {
    console.log(`${this.brand} car engine stopped`);
  }
}

class ElectricCar extends Vehicle {
  startEngine(): void {
    console.log(`${this.brand} electric car powered on silently`);
  }

  stopEngine(): void {
    console.log(`${this.brand} electric car powered off`);
  }
}

// Usage - we don't need to know implementation details
const myCar: Vehicle = new Car("Toyota");
myCar.startEngine();
myCar.displayInfo();

const myTesla: Vehicle = new ElectricCar("Tesla");
myTesla.startEngine();

3. Inheritance

Inheritance allows a class to inherit properties and methods from another class. It promotes code reusability and establishes a relationship between parent and child classes.

// Base class (Parent)
class Animal {
  protected name: string;
  protected age: number;

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

  public makeSound(): void {
    console.log("Some generic animal sound");
  }

  public eat(): void {
    console.log(`${this.name} is eating`);
  }

  public sleep(): void {
    console.log(`${this.name} is sleeping`);
  }
}

// Derived class (Child)
class Dog extends Animal {
  private breed: string;

  constructor(name: string, age: number, breed: string) {
    super(name, age); // Call parent constructor
    this.breed = breed;
  }

  // Override parent method
  public makeSound(): void {
    console.log(`${this.name} barks: Woof! Woof!`);
  }

  // Additional method specific to Dog
  public fetch(): void {
    console.log(`${this.name} is fetching the ball`);
  }
}

class Cat extends Animal {
  constructor(name: string, age: number) {
    super(name, age);
  }

  // Override parent method
  public makeSound(): void {
    console.log(`${this.name} meows: Meow!`);
  }

  // Additional method specific to Cat
  public scratch(): void {
    console.log(`${this.name} is scratching the furniture`);
  }
}

// Usage
const dog = new Dog("Buddy", 3, "Golden Retriever");
dog.makeSound(); // Buddy barks: Woof! Woof!
dog.eat();       // Buddy is eating
dog.fetch();     // Buddy is fetching the ball

const cat = new Cat("Whiskers", 2);
cat.makeSound(); // Whiskers meows: Meow!
cat.scratch();   // Whiskers is scratching the furniture

4. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common base class. It enables a single interface to represent different underlying forms (data types).

// Interface defining common behavior
interface Shape {
  calculateArea(): number;
  calculatePerimeter(): number;
  draw(): void;
}

// Different implementations
class Circle implements Shape {
  constructor(private radius: number) {}

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

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

  draw(): void {
    console.log(`Drawing a circle with radius ${this.radius}`);
  }
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

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

  calculatePerimeter(): number {
    return 2 * (this.width + this.height);
  }

  draw(): void {
    console.log(`Drawing a rectangle ${this.width}x${this.height}`);
  }
}

class Triangle implements Shape {
  constructor(
    private sideA: number,
    private sideB: number,
    private sideC: number
  ) {}

  calculateArea(): number {
    // Using Heron's formula
    const s = (this.sideA + this.sideB + this.sideC) / 2;
    return Math.sqrt(s * (s - this.sideA) * (s - this.sideB) * (s - this.sideC));
  }

  calculatePerimeter(): number {
    return this.sideA + this.sideB + this.sideC;
  }

  draw(): void {
    console.log(`Drawing a triangle with sides ${this.sideA}, ${this.sideB}, ${this.sideC}`);
  }
}

// Polymorphism in action - same interface, different implementations
function printShapeInfo(shape: Shape): void {
  shape.draw();
  console.log(`Area: ${shape.calculateArea().toFixed(2)}`);
  console.log(`Perimeter: ${shape.calculatePerimeter().toFixed(2)}`);
  console.log("---");
}

// All shapes can be treated uniformly
const shapes: Shape[] = [
  new Circle(5),
  new Rectangle(4, 6),
  new Triangle(3, 4, 5)
];

shapes.forEach(printShapeInfo);

Benefits of OOP

  • Modularity: Code is organized into self-contained objects

  • Reusability: Objects can be reused across different parts of a program or in different programs

  • Maintainability: Changes to one object don't affect others, making debugging easier

  • Scalability: New features can be added with minimal impact on existing code

  • Security: Encapsulation protects data from unauthorized access

OOP

Classes and Objects

Classes and Objects Classes are blueprints for creating objects. Objects are instances of classes that contain actual data and can perform actions. Defining a C

Classes and Objects

Classes are blueprints for creating objects. Objects are instances of classes that contain actual data and can perform actions.

Defining a Class

A class typically contains properties (data) and methods (functions) that operate on that data.

class Person {
  // Properties
  private firstName: string;
  private lastName: string;
  private age: number;
  private email: string;

  // Constructor
  constructor(firstName: string, lastName: string, age: number, email: string) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.email = email;
  }

  // Getter methods
  public getFullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  public getAge(): number {
    return this.age;
  }

  // Method
  public introduce(): void {
    console.log(`Hi, I'm ${this.getFullName()} and I'm ${this.age} years old.`);
  }

  public celebrateBirthday(): void {
    this.age++;
    console.log(`Happy Birthday! I'm now ${this.age} years old.`);
  }

  // Static method (belongs to class, not instance)
  public static compareAges(person1: Person, person2: Person): string {
    if (person1.age > person2.age) {
      return `${person1.getFullName()} is older`;
    } else if (person1.age < person2.age) {
      return `${person2.getFullName()} is older`;
    }
    return "They are the same age";
  }
}

// Creating objects (instances)
const john = new Person("John", "Doe", 30, "john@example.com");
const jane = new Person("Jane", "Smith", 25, "jane@example.com");

john.introduce(); // Hi, I'm John Doe and I'm 30 years old.
jane.introduce(); // Hi, I'm Jane Smith and I'm 25 years old.

console.log(Person.compareAges(john, jane)); // John Doe is older

Access Modifiers

Access modifiers control the visibility of class members:

  • public: Accessible from anywhere

  • private: Only accessible within the class

  • protected: Accessible within the class and its subclasses

class Employee {
  public id: number;           // Accessible everywhere
  private salary: number;       // Only within Employee class
  protected department: string; // Within Employee and subclasses

  constructor(id: number, salary: number, department: string) {
    this.id = id;
    this.salary = salary;
    this.department = department;
  }

  private calculateBonus(): number {
    return this.salary * 0.10;
  }

  public getPaymentInfo(): string {
    return `Salary: $${this.salary}, Bonus: $${this.calculateBonus()}`;
  }
}

class Manager extends Employee {
  private teamSize: number;

  constructor(id: number, salary: number, department: string, teamSize: number) {
    super(id, salary, department);
    this.teamSize = teamSize;
  }

  public getTeamInfo(): string {
    // Can access protected department, but not private salary
    return `Managing ${this.teamSize} people in ${this.department} department`;
  }
}

const emp = new Employee(1, 50000, "IT");
console.log(emp.id);                 // OK: public
console.log(emp.getPaymentInfo());   // OK: public method
// console.log(emp.salary);          // ERROR: private
// console.log(emp.calculateBonus()); // ERROR: private method

Constructors

Constructors are special methods that initialize new objects. They can be overloaded to provide different ways of creating objects.

class Product {
  constructor(
    public readonly id: string,
    public name: string,
    public price: number,
    public inStock: boolean = true
  ) {
    // Shorthand property initialization
  }

  // Factory method pattern for creating products
  static createFromJSON(json: any): Product {
    return new Product(
      json.id,
      json.name,
      json.price,
      json.inStock ?? true
    );
  }

  static createDiscountedProduct(name: string, originalPrice: number, discount: number): Product {
    const discountedPrice = originalPrice * (1 - discount);
    return new Product(
      `DISC-${Date.now()}`,
      `${name} (${discount * 100}% OFF)`,
      discountedPrice
    );
  }
}

// Different ways to create products
const product1 = new Product("P001", "Laptop", 999.99);
const product2 = Product.createFromJSON({
  id: "P002",
  name: "Mouse",
  price: 29.99,
  inStock: true
});
const product3 = Product.createDiscountedProduct("Keyboard", 79.99, 0.20);

console.log(product3.name);  // "Keyboard (20% OFF)"
console.log(product3.price); // 63.992

Getters and Setters

Getters and setters allow controlled access to private properties with validation and computed properties.

class Temperature {
  private celsius: number;

  constructor(celsius: number) {
    this.celsius = celsius;
  }

  // Getter: read like a property
  get fahrenheit(): number {
    return (this.celsius * 9/5) + 32;
  }

  // Setter: write like a property with validation
  set fahrenheit(value: number) {
    if (value < -459.67) {
      throw new Error("Temperature cannot be below absolute zero!");
    }
    this.celsius = (value - 32) * 5/9;
  }

  get kelvin(): number {
    return this.celsius + 273.15;
  }

  set kelvin(value: number) {
    if (value < 0) {
      throw new Error("Kelvin cannot be negative!");
    }
    this.celsius = value - 273.15;
  }

  // Method to display all formats
  display(): void {
    console.log(`${this.celsius.toFixed(2)}°C = ${this.fahrenheit.toFixed(2)}°F = ${this.kelvin.toFixed(2)}K`);
  }
}

const temp = new Temperature(25);
temp.display(); // 25.00°C = 77.00°F = 298.15K

temp.fahrenheit = 98.6; // Set using Fahrenheit
temp.display(); // 37.00°C = 98.60°F = 310.15K

temp.kelvin = 273.15; // Set using Kelvin
temp.display(); // 0.00°C = 32.00°F = 273.15K

Static Members

Static members belong to the class itself rather than to instances of the class. They are useful for utility functions and shared data.

class MathUtils {
  // Static property
  static readonly PI = 3.14159265359;

  // Static method
  static circleArea(radius: number): number {
    return this.PI * radius ** 2;
  }

  static circleCircumference(radius: number): number {
    return 2 * this.PI * radius;
  }

  static degreesToRadians(degrees: number): number {
    return degrees * (this.PI / 180);
  }

  static radiansToDegrees(radians: number): number {
    return radians * (180 / this.PI);
  }
}

// Use without creating an instance
console.log(MathUtils.circleArea(5));           // 78.54
console.log(MathUtils.degreesToRadians(180));   // 3.14

class Counter {
  private static count: number = 0;
  public readonly id: number;

  constructor() {
    Counter.count++;
    this.id = Counter.count;
  }

  static getCount(): number {
    return Counter.count;
  }

  static reset(): void {
    Counter.count = 0;
  }
}

const c1 = new Counter();
const c2 = new Counter();
const c3 = new Counter();
console.log(Counter.getCount()); // 3
console.log(c1.id, c2.id, c3.id); // 1 2 3
OOP

Design Patterns

Common OOP Design Patterns Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time. Single

Common OOP Design Patterns

Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time.

Singleton Pattern

Ensures a class has only one instance and provides a global access point to it.

class Database {
  private static instance: Database;
  private connection: any;

  // Private constructor prevents external instantiation
  private constructor() {
    this.connection = this.createConnection();
    console.log("Database instance created");
  }

  public static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }

  private createConnection(): any {
    return { connected: true, host: "localhost" };
  }

  public query(sql: string): any {
    console.log(`Executing: ${sql}`);
    return [];
  }
}

// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true - same instance

// const db3 = new Database(); // ERROR: Constructor is private

Factory Pattern

Creates objects without specifying the exact class of object that will be created.

interface Notification {
  send(message: string): void;
}

class EmailNotification implements Notification {
  constructor(private recipient: string) {}

  send(message: string): void {
    console.log(`Email to ${this.recipient}: ${message}`);
  }
}

class SMSNotification implements Notification {
  constructor(private phoneNumber: string) {}

  send(message: string): void {
    console.log(`SMS to ${this.phoneNumber}: ${message}`);
  }
}

class PushNotification implements Notification {
  constructor(private deviceId: string) {}

  send(message: string): void {
    console.log(`Push to ${this.deviceId}: ${message}`);
  }
}

// Factory
class NotificationFactory {
  static create(type: string, recipient: string): Notification {
    switch (type) {
      case "email":
        return new EmailNotification(recipient);
      case "sms":
        return new SMSNotification(recipient);
      case "push":
        return new PushNotification(recipient);
      default:
        throw new Error(`Unknown notification type: ${type}`);
    }
  }
}

// Usage
const notifications = [
  NotificationFactory.create("email", "user@example.com"),
  NotificationFactory.create("sms", "+1234567890"),
  NotificationFactory.create("push", "device-123")
];

notifications.forEach(notification => {
  notification.send("Hello!");
});

Observer Pattern

Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.

interface Observer {
  update(data: any): void;
}

class Subject {
  private observers: Observer[] = [];
  private state: any;

  attach(observer: Observer): void {
    this.observers.push(observer);
  }

  detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(): void {
    for (const observer of this.observers) {
      observer.update(this.state);
    }
  }

  setState(state: any): void {
    this.state = state;
    this.notify();
  }
}

class StockMarket extends Subject {
  private stockPrices: Map<string, number> = new Map();

  updatePrice(symbol: string, price: number): void {
    this.stockPrices.set(symbol, price);
    this.setState({ symbol, price });
  }

  getPrice(symbol: string): number | undefined {
    return this.stockPrices.get(symbol);
  }
}

class Investor implements Observer {
  constructor(private name: string) {}

  update(data: { symbol: string; price: number }): void {
    console.log(`${this.name} notified: ${data.symbol} = $${data.price}`);
  }
}

class TradingBot implements Observer {
  constructor(private strategy: string) {}

  update(data: { symbol: string; price: number }): void {
    console.log(`Bot (${this.strategy}) analyzing: ${data.symbol} = $${data.price}`);
    if (data.price < 100) {
      console.log(`  → Executing BUY order`);
    }
  }
}

// Usage
const market = new StockMarket();
const investor1 = new Investor("Alice");
const investor2 = new Investor("Bob");
const bot = new TradingBot("Value Investing");

market.attach(investor1);
market.attach(investor2);
market.attach(bot);

market.updatePrice("AAPL", 150);
market.updatePrice("GOOGL", 95);

Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

interface SortStrategy {
  sort(data: number[]): number[];
}

class BubbleSort implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Using Bubble Sort");
    const arr = [...data];
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}

class QuickSort implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Using Quick Sort");
    if (data.length <= 1) return data;
    
    const pivot = data[Math.floor(data.length / 2)];
    const left = data.filter(x => x < pivot);
    const middle = data.filter(x => x === pivot);
    const right = data.filter(x => x > pivot);
    
    return [...this.sort(left), ...middle, ...this.sort(right)];
  }
}

class MergeSort implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Using Merge Sort");
    if (data.length <= 1) return data;

    const mid = Math.floor(data.length / 2);
    const left = this.sort(data.slice(0, mid));
    const right = this.sort(data.slice(mid));

    return this.merge(left, right);
  }

  private merge(left: number[], right: number[]): number[] {
    const result: number[] = [];
    let i = 0, j = 0;

    while (i < left.length && j < right.length) {
      if (left[i] < right[j]) {
        result.push(left[i++]);
      } else {
        result.push(right[j++]);
      }
    }

    return result.concat(left.slice(i)).concat(right.slice(j));
  }
}

class DataSorter {
  private strategy: SortStrategy;

  constructor(strategy: SortStrategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy: SortStrategy): void {
    this.strategy = strategy;
  }

  sort(data: number[]): number[] {
    return this.strategy.sort(data);
  }
}

// Usage
const data = [64, 34, 25, 12, 22, 11, 90];
const sorter = new DataSorter(new BubbleSort());

console.log("Result:", sorter.sort(data));

// Change strategy at runtime
sorter.setStrategy(new QuickSort());
console.log("Result:", sorter.sort(data));

sorter.setStrategy(new MergeSort());
console.log("Result:", sorter.sort(data));

Decorator Pattern

Attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing.

interface Coffee {
  cost(): number;
  description(): string;
}

class SimpleCoffee implements Coffee {
  cost(): number {
    return 2;
  }

  description(): string {
    return "Simple coffee";
  }
}

// Decorator base class
abstract class CoffeeDecorator implements Coffee {
  constructor(protected coffee: Coffee) {}

  abstract cost(): number;
  abstract description(): string;
}

class MilkDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 0.5;
  }

  description(): string {
    return this.coffee.description() + ", milk";
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 0.2;
  }

  description(): string {
    return this.coffee.description() + ", sugar";
  }
}

class WhippedCreamDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 0.7;
  }

  description(): string {
    return this.coffee.description() + ", whipped cream";
  }
}

// Usage - stack decorators
let coffee: Coffee = new SimpleCoffee();
console.log(`${coffee.description()} = $${coffee.cost()}`);

coffee = new MilkDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);

coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);

coffee = new WhippedCreamDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);

// Or all at once
const fancyCoffee = new WhippedCreamDecorator(
  new SugarDecorator(
    new MilkDecorator(
      new SimpleCoffee()
    )
  )
);
console.log(`${fancyCoffee.description()} = $${fancyCoffee.cost()}`);
OOP

Design Patterns

Common OOP Design Patterns Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time. Single

Common OOP Design Patterns

Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time.

Singleton Pattern

Ensures a class has only one instance and provides a global access point to it.

class Database {
  private static instance: Database;
  private connection: any;

  // Private constructor prevents external instantiation
  private constructor() {
    this.connection = this.createConnection();
    console.log("Database instance created");
  }

  public static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }

  private createConnection(): any {
    return { connected: true, host: "localhost" };
  }

  public query(sql: string): any {
    console.log(`Executing: ${sql}`);
    return [];
  }
}

// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true - same instance

// const db3 = new Database(); // ERROR: Constructor is private

Factory Pattern

Creates objects without specifying the exact class of object that will be created.

interface Notification {
  send(message: string): void;
}

class EmailNotification implements Notification {
  constructor(private recipient: string) {}

  send(message: string): void {
    console.log(`Email to ${this.recipient}: ${message}`);
  }
}

class SMSNotification implements Notification {
  constructor(private phoneNumber: string) {}

  send(message: string): void {
    console.log(`SMS to ${this.phoneNumber}: ${message}`);
  }
}

class PushNotification implements Notification {
  constructor(private deviceId: string) {}

  send(message: string): void {
    console.log(`Push to ${this.deviceId}: ${message}`);
  }
}

// Factory
class NotificationFactory {
  static create(type: string, recipient: string): Notification {
    switch (type) {
      case "email":
        return new EmailNotification(recipient);
      case "sms":
        return new SMSNotification(recipient);
      case "push":
        return new PushNotification(recipient);
      default:
        throw new Error(`Unknown notification type: ${type}`);
    }
  }
}

// Usage
const notifications = [
  NotificationFactory.create("email", "user@example.com"),
  NotificationFactory.create("sms", "+1234567890"),
  NotificationFactory.create("push", "device-123")
];

notifications.forEach(notification => {
  notification.send("Hello!");
});

Observer Pattern

Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.

interface Observer {
  update(data: any): void;
}

class Subject {
  private observers: Observer[] = [];
  private state: any;

  attach(observer: Observer): void {
    this.observers.push(observer);
  }

  detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(): void {
    for (const observer of this.observers) {
      observer.update(this.state);
    }
  }

  setState(state: any): void {
    this.state = state;
    this.notify();
  }
}

class StockMarket extends Subject {
  private stockPrices: Map<string, number> = new Map();

  updatePrice(symbol: string, price: number): void {
    this.stockPrices.set(symbol, price);
    this.setState({ symbol, price });
  }

  getPrice(symbol: string): number | undefined {
    return this.stockPrices.get(symbol);
  }
}

class Investor implements Observer {
  constructor(private name: string) {}

  update(data: { symbol: string; price: number }): void {
    console.log(`${this.name} notified: ${data.symbol} = $${data.price}`);
  }
}

class TradingBot implements Observer {
  constructor(private strategy: string) {}

  update(data: { symbol: string; price: number }): void {
    console.log(`Bot (${this.strategy}) analyzing: ${data.symbol} = $${data.price}`);
    if (data.price < 100) {
      console.log(`  → Executing BUY order`);
    }
  }
}

// Usage
const market = new StockMarket();
const investor1 = new Investor("Alice");
const investor2 = new Investor("Bob");
const bot = new TradingBot("Value Investing");

market.attach(investor1);
market.attach(investor2);
market.attach(bot);

market.updatePrice("AAPL", 150);
market.updatePrice("GOOGL", 95);

Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

interface SortStrategy {
  sort(data: number[]): number[];
}

class BubbleSort implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Using Bubble Sort");
    const arr = [...data];
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}

class QuickSort implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Using Quick Sort");
    if (data.length <= 1) return data;
    
    const pivot = data[Math.floor(data.length / 2)];
    const left = data.filter(x => x < pivot);
    const middle = data.filter(x => x === pivot);
    const right = data.filter(x => x > pivot);
    
    return [...this.sort(left), ...middle, ...this.sort(right)];
  }
}

class MergeSort implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Using Merge Sort");
    if (data.length <= 1) return data;

    const mid = Math.floor(data.length / 2);
    const left = this.sort(data.slice(0, mid));
    const right = this.sort(data.slice(mid));

    return this.merge(left, right);
  }

  private merge(left: number[], right: number[]): number[] {
    const result: number[] = [];
    let i = 0, j = 0;

    while (i < left.length && j < right.length) {
      if (left[i] < right[j]) {
        result.push(left[i++]);
      } else {
        result.push(right[j++]);
      }
    }

    return result.concat(left.slice(i)).concat(right.slice(j));
  }
}

class DataSorter {
  private strategy: SortStrategy;

  constructor(strategy: SortStrategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy: SortStrategy): void {
    this.strategy = strategy;
  }

  sort(data: number[]): number[] {
    return this.strategy.sort(data);
  }
}

// Usage
const data = [64, 34, 25, 12, 22, 11, 90];
const sorter = new DataSorter(new BubbleSort());

console.log("Result:", sorter.sort(data));

// Change strategy at runtime
sorter.setStrategy(new QuickSort());
console.log("Result:", sorter.sort(data));

sorter.setStrategy(new MergeSort());
console.log("Result:", sorter.sort(data));

Decorator Pattern

Attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing.

interface Coffee {
  cost(): number;
  description(): string;
}

class SimpleCoffee implements Coffee {
  cost(): number {
    return 2;
  }

  description(): string {
    return "Simple coffee";
  }
}

// Decorator base class
abstract class CoffeeDecorator implements Coffee {
  constructor(protected coffee: Coffee) {}

  abstract cost(): number;
  abstract description(): string;
}

class MilkDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 0.5;
  }

  description(): string {
    return this.coffee.description() + ", milk";
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 0.2;
  }

  description(): string {
    return this.coffee.description() + ", sugar";
  }
}

class WhippedCreamDecorator extends CoffeeDecorator {
  cost(): number {
    return this.coffee.cost() + 0.7;
  }

  description(): string {
    return this.coffee.description() + ", whipped cream";
  }
}

// Usage - stack decorators
let coffee: Coffee = new SimpleCoffee();
console.log(`${coffee.description()} = $${coffee.cost()}`);

coffee = new MilkDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);

coffee = new SugarDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);

coffee = new WhippedCreamDecorator(coffee);
console.log(`${coffee.description()} = $${coffee.cost()}`);

// Or all at once
const fancyCoffee = new WhippedCreamDecorator(
  new SugarDecorator(
    new MilkDecorator(
      new SimpleCoffee()
    )
  )
);
console.log(`${fancyCoffee.description()} = $${fancyCoffee.cost()}`);
OOP

Inheritance and Polymorphism

Inheritance and Polymorphism Master advanced OOP concepts that enable code reuse and flexible design through inheritance hierarchies and polymorphic behavior. I

Inheritance and Polymorphism

Master advanced OOP concepts that enable code reuse and flexible design through inheritance hierarchies and polymorphic behavior.

Inheritance Hierarchies

Inheritance creates a parent-child relationship between classes, allowing child classes to inherit and extend parent functionality.

// Base class
class User {
  protected id: string;
  protected email: string;
  protected createdAt: Date;

  constructor(id: string, email: string) {
    this.id = id;
    this.email = email;
    this.createdAt = new Date();
  }

  public getProfile(): string {
    return `User: ${this.email} (ID: ${this.id})`;
  }

  public canAccessResource(resourceId: string): boolean {
    return false; // Base users can't access resources
  }
}

// First level inheritance
class RegisteredUser extends User {
  protected username: string;
  protected lastLogin: Date;

  constructor(id: string, email: string, username: string) {
    super(id, email);
    this.username = username;
    this.lastLogin = new Date();
  }

  public override getProfile(): string {
    return `${this.username} (${this.email})`;
  }

  public override canAccessResource(resourceId: string): boolean {
    return true; // Registered users can access basic resources
  }

  public updateLastLogin(): void {
    this.lastLogin = new Date();
  }
}

// Second level inheritance
class PremiumUser extends RegisteredUser {
  private subscriptionEndDate: Date;
  private features: string[];

  constructor(
    id: string,
    email: string,
    username: string,
    subscriptionMonths: number
  ) {
    super(id, email, username);
    this.subscriptionEndDate = new Date();
    this.subscriptionEndDate.setMonth(
      this.subscriptionEndDate.getMonth() + subscriptionMonths
    );
    this.features = ["ad-free", "HD-streaming", "offline-mode"];
  }

  public override getProfile(): string {
    return `⭐ PREMIUM: ${super.getProfile()}`;
  }

  public override canAccessResource(resourceId: string): boolean {
    // Premium users can access all resources
    return this.isSubscriptionActive();
  }

  private isSubscriptionActive(): boolean {
    return new Date() < this.subscriptionEndDate;
  }

  public getFeatures(): string[] {
    return [...this.features];
  }
}

class AdminUser extends RegisteredUser {
  private permissions: Set<string>;

  constructor(id: string, email: string, username: string) {
    super(id, email, username);
    this.permissions = new Set(["read", "write", "delete", "admin"]);
  }

  public override getProfile(): string {
    return `🛡️ ADMIN: ${super.getProfile()}`;
  }

  public override canAccessResource(resourceId: string): boolean {
    return true; // Admins can access everything
  }

  public hasPermission(permission: string): boolean {
    return this.permissions.has(permission);
  }

  public deleteUser(userId: string): void {
    console.log(`Admin deleted user: ${userId}`);
  }
}

// Usage
const users: User[] = [
  new User("1", "guest@example.com"),
  new RegisteredUser("2", "user@example.com", "john_doe"),
  new PremiumUser("3", "premium@example.com", "jane_premium", 12),
  new AdminUser("4", "admin@example.com", "admin_user")
];

users.forEach(user => {
  console.log(user.getProfile());
  console.log(`Can access: ${user.canAccessResource("resource-1")}`);
});

Method Overriding

Child classes can provide their own implementation of methods inherited from parent classes.

class PaymentProcessor {
  protected processingFee: number = 0;

  processPayment(amount: number): number {
    const fee = this.calculateFee(amount);
    const total = amount + fee;
    console.log(`Processing payment: $${amount} + $${fee} fee = $${total}`);
    return total;
  }

  protected calculateFee(amount: number): number {
    return 0; // Base class has no fee
  }
}

class CreditCardProcessor extends PaymentProcessor {
  protected override calculateFee(amount: number): number {
    return amount * 0.029 + 0.30; // 2.9% + $0.30
  }
}

class PayPalProcessor extends PaymentProcessor {
  protected override calculateFee(amount: number): number {
    return amount * 0.034 + 0.49; // 3.4% + $0.49
  }
}

class WireTransferProcessor extends PaymentProcessor {
  protected override calculateFee(amount: number): number {
    return 25.00; // Flat fee
  }
}

// Same method, different behavior based on object type
function processAllPayments(processors: PaymentProcessor[], amount: number): void {
  processors.forEach(processor => {
    processor.processPayment(amount);
  });
}

const processors = [
  new PaymentProcessor(),
  new CreditCardProcessor(),
  new PayPalProcessor(),
  new WireTransferProcessor()
];

processAllPayments(processors, 100);

Abstract Classes

Abstract classes cannot be instantiated directly and are meant to be extended. They can contain abstract methods that must be implemented by child classes.

abstract class DatabaseConnection {
  protected connectionString: string;
  protected isConnected: boolean = false;

  constructor(connectionString: string) {
    this.connectionString = connectionString;
  }

  // Abstract methods - must be implemented by subclasses
  abstract connect(): Promise<void>;
  abstract disconnect(): Promise<void>;
  abstract query(sql: string): Promise<any[]>;
  abstract execute(sql: string): Promise<number>;

  // Concrete method - shared by all connections
  public getStatus(): string {
    return this.isConnected ? "Connected" : "Disconnected";
  }

  protected log(message: string): void {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}

class MySQLConnection extends DatabaseConnection {
  async connect(): Promise<void> {
    this.log(`Connecting to MySQL: ${this.connectionString}`);
    // Simulate connection
    await new Promise(resolve => setTimeout(resolve, 100));
    this.isConnected = true;
    this.log("MySQL connected successfully");
  }

  async disconnect(): Promise<void> {
    this.log("Disconnecting from MySQL");
    this.isConnected = false;
  }

  async query(sql: string): Promise<any[]> {
    if (!this.isConnected) throw new Error("Not connected");
    this.log(`Executing query: ${sql}`);
    return []; // Simulated result
  }

  async execute(sql: string): Promise<number> {
    if (!this.isConnected) throw new Error("Not connected");
    this.log(`Executing command: ${sql}`);
    return 1; // Simulated affected rows
  }
}

class PostgreSQLConnection extends DatabaseConnection {
  async connect(): Promise<void> {
    this.log(`Connecting to PostgreSQL: ${this.connectionString}`);
    await new Promise(resolve => setTimeout(resolve, 100));
    this.isConnected = true;
    this.log("PostgreSQL connected successfully");
  }

  async disconnect(): Promise<void> {
    this.log("Disconnecting from PostgreSQL");
    this.isConnected = false;
  }

  async query(sql: string): Promise<any[]> {
    if (!this.isConnected) throw new Error("Not connected");
    this.log(`PG Query: ${sql}`);
    return [];
  }

  async execute(sql: string): Promise<number> {
    if (!this.isConnected) throw new Error("Not connected");
    this.log(`PG Execute: ${sql}`);
    return 1;
  }
}

// Usage
async function databaseExample() {
  const connections: DatabaseConnection[] = [
    new MySQLConnection("mysql://localhost:3306/mydb"),
    new PostgreSQLConnection("postgresql://localhost:5432/mydb")
  ];

  for (const conn of connections) {
    await conn.connect();
    await conn.query("SELECT * FROM users");
    console.log(`Status: ${conn.getStatus()}`);
    await conn.disconnect();
  }
}

Interfaces vs Abstract Classes

Understanding when to use interfaces versus abstract classes:

// Interface: Contract for implementation
interface Printable {
  print(): void;
  getPrintData(): string;
}

// Interface: Can extend multiple interfaces
interface Saveable {
  save(): Promise<void>;
  load(): Promise<void>;
}

// A class can implement multiple interfaces
class Document implements Printable, Saveable {
  constructor(
    private content: string,
    private filename: string
  ) {}

  print(): void {
    console.log(this.getPrintData());
  }

  getPrintData(): string {
    return `Document: ${this.filename}\n${this.content}`;
  }

  async save(): Promise<void> {
    console.log(`Saving ${this.filename}...`);
    // Save logic
  }

  async load(): Promise<void> {
    console.log(`Loading ${this.filename}...`);
    // Load logic
  }
}

// Abstract class: Provides base implementation
abstract class Report {
  protected title: string;
  protected generatedAt: Date;

  constructor(title: string) {
    this.title = title;
    this.generatedAt = new Date();
  }

  // Concrete method with implementation
  public getMetadata(): string {
    return `Report: ${this.title} (Generated: ${this.generatedAt.toISOString()})`;
  }

  // Abstract method - must be implemented
  abstract generateContent(): string;
  abstract export(format: string): string;
}

class SalesReport extends Report {
  constructor(
    title: string,
    private salesData: any[]
  ) {
    super(title);
  }

  generateContent(): string {
    return `Sales Report Content: ${this.salesData.length} records`;
  }

  export(format: string): string {
    return `Exporting sales report to ${format}`;
  }
}

// When to use which:
// - Use INTERFACE when you need multiple inheritance or pure contracts
// - Use ABSTRACT CLASS when you have shared implementation and state

Composition over Inheritance

While inheritance is powerful, composition is often preferred for building flexible systems.

// Instead of deep inheritance, use composition
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

class FileLogger implements Logger {
  constructor(private filename: string) {}
  
  log(message: string): void {
    console.log(`[FILE: ${this.filename}] ${message}`);
  }
}

interface Cache {
  get(key: string): any;
  set(key: string, value: any): void;
}

class MemoryCache implements Cache {
  private store = new Map();

  get(key: string): any {
    return this.store.get(key);
  }

  set(key: string, value: any): void {
    this.store.set(key, value);
  }
}

// Using composition
class UserService {
  constructor(
    private logger: Logger,
    private cache: Cache
  ) {}

  async getUser(id: string): Promise<any> {
    this.logger.log(`Fetching user: ${id}`);
    
    // Check cache first
    const cached = this.cache.get(`user_${id}`);
    if (cached) {
      this.logger.log(`User ${id} found in cache`);
      return cached;
    }

    // Fetch from database (simulated)
    const user = { id, name: "John Doe" };
    this.cache.set(`user_${id}`, user);
    this.logger.log(`User ${id} cached`);
    
    return user;
  }
}

// Flexible - can swap implementations easily
const service1 = new UserService(
  new ConsoleLogger(),
  new MemoryCache()
);

const service2 = new UserService(
  new FileLogger("app.log"),
  new MemoryCache()
);
OOP

Best Practices

OOP Best Practices Follow these best practices to write clean, maintainable, and effective object-oriented code. 1. Single Responsibility Principle A class shou

OOP Best Practices

Follow these best practices to write clean, maintainable, and effective object-oriented code.

1. Single Responsibility Principle

A class should have only one reason to change - it should do one thing and do it well.

// ❌ BAD: Class doing too much
class User {
  constructor(
    public name: string,
    public email: string
  ) {}

  save(): void {
    // Database logic - NOT the user's responsibility
    console.log("Saving to database...");
  }

  sendEmail(message: string): void {
    // Email logic - NOT the user's responsibility
    console.log(`Sending email to ${this.email}: ${message}`);
  }

  generateReport(): string {
    // Reporting logic - NOT the user's responsibility
    return `User Report for ${this.name}`;
  }
}

// ✅ GOOD: Separate responsibilities
class User {
  constructor(
    public name: string,
    public email: string
  ) {}

  getFullName(): string {
    return this.name;
  }
}

class UserRepository {
  save(user: User): void {
    console.log(`Saving user: ${user.name}`);
    // Database logic here
  }

  find(email: string): User | null {
    // Find logic here
    return null;
  }
}

class EmailService {
  send(to: string, message: string): void {
    console.log(`Sending email to ${to}: ${message}`);
    // Email logic here
  }
}

class UserReportGenerator {
  generate(user: User): string {
    return `User Report for ${user.name}`;
    // Reporting logic here
  }
}

2. Favor Composition Over Inheritance

Use composition to combine behaviors rather than creating deep inheritance hierarchies.

// ❌ BAD: Deep inheritance
class Animal {}
class Bird extends Animal {}
class FlyingBird extends Bird {}
class SwimmingFlyingBird extends FlyingBird {} // Getting complex!

// ✅ GOOD: Composition
interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Duck implements Flyable, Swimmable {
  fly(): void {
    console.log("Duck flying");
  }

  swim(): void {
    console.log("Duck swimming");
  }
}

class Penguin implements Swimmable {
  swim(): void {
    console.log("Penguin swimming");
  }
}

class Eagle implements Flyable {
  fly(): void {
    console.log("Eagle soaring");
  }
}

3. Program to Interfaces, Not Implementations

Depend on abstractions rather than concrete implementations.

// ❌ BAD: Depending on concrete class
class MySQLDatabase {
  query(sql: string): any[] {
    return [];
  }
}

class UserService {
  private db: MySQLDatabase; // Tightly coupled

  constructor() {
    this.db = new MySQLDatabase();
  }

  getUsers(): any[] {
    return this.db.query("SELECT * FROM users");
  }
}

// ✅ GOOD: Depending on interface
interface Database {
  query(sql: string): any[];
}

class MySQLDatabase implements Database {
  query(sql: string): any[] {
    console.log("MySQL query:", sql);
    return [];
  }
}

class PostgreSQLDatabase implements Database {
  query(sql: string): any[] {
    console.log("PostgreSQL query:", sql);
    return [];
  }
}

class UserService {
  constructor(private db: Database) {} // Flexible!

  getUsers(): any[] {
    return this.db.query("SELECT * FROM users");
  }
}

// Easy to swap implementations
const service1 = new UserService(new MySQLDatabase());
const service2 = new UserService(new PostgreSQLDatabase());

4. Keep Classes Small and Focused

If a class has too many methods or properties, it probably needs to be split.

// ❌ BAD: God class doing everything
class OrderManager {
  createOrder() {}
  updateOrder() {}
  deleteOrder() {}
  calculateTotal() {}
  applyDiscount() {}
  processPayment() {}
  sendConfirmationEmail() {}
  updateInventory() {}
  generateInvoice() {}
  calculateShipping() {}
  // ... too many responsibilities!
}

// ✅ GOOD: Focused classes
class Order {
  constructor(
    public id: string,
    public items: OrderItem[],
    public customerId: string
  ) {}

  calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.getTotal(), 0);
  }
}

class OrderRepository {
  save(order: Order): void {}
  find(id: string): Order | null { return null; }
  delete(id: string): void {}
}

class DiscountService {
  apply(order: Order, code: string): number { return 0; }
}

class PaymentProcessor {
  process(order: Order, method: string): boolean { return true; }
}

class OrderNotificationService {
  sendConfirmation(order: Order): void {}
}

class InventoryService {
  update(order: Order): void {}
}

class InvoiceGenerator {
  generate(order: Order): string { return ""; }
}

class ShippingCalculator {
  calculate(order: Order): number { return 0; }
}

5. Use Meaningful Names

Classes, methods, and properties should have clear, descriptive names.

// ❌ BAD: Unclear names
class Mgr {
  private d: Data[];
  
  proc(): void {}
  get(): Data[] { return this.d; }
}

// ✅ GOOD: Clear names
class UserManager {
  private users: User[];
  
  processUserRegistration(): void {}
  getAllUsers(): User[] { return this.users; }
  findUserByEmail(email: string): User | null { return null; }
}

// Class names should be nouns
class PaymentProcessor {} // Good
class ProcessPayment {}   // Bad (verb)

// Method names should be verbs
getUser()      // Good
createOrder()  // Good
user()         // Bad (noun)
order()        // Bad (noun)

6. Minimize Coupling

Classes should depend on as few other classes as possible.

// ❌ BAD: Tight coupling
class EmailService {
  send(to: string, message: string): void {
    const smtp = new SMTPClient("smtp.gmail.com", 587);
    smtp.connect();
    smtp.send(to, message);
    smtp.disconnect();
  }
}

// ✅ GOOD: Loose coupling through dependency injection
interface MailTransport {
  send(to: string, message: string): void;
}

class SMTPTransport implements MailTransport {
  constructor(
    private host: string,
    private port: number
  ) {}

  send(to: string, message: string): void {
    console.log(`SMTP: Sending to ${to}`);
  }
}

class SendGridTransport implements MailTransport {
  constructor(private apiKey: string) {}

  send(to: string, message: string): void {
    console.log(`SendGrid: Sending to ${to}`);
  }
}

class EmailService {
  constructor(private transport: MailTransport) {}

  send(to: string, message: string): void {
    this.transport.send(to, message);
  }
}

// Easy to test and swap implementations
const emailService = new EmailService(
  new SMTPTransport("smtp.gmail.com", 587)
);

7. Follow the Law of Demeter (Principle of Least Knowledge)

Don't talk to strangers - a method should only call methods on: itself, its parameters, objects it creates, or its direct fields.

// ❌ BAD: Violates Law of Demeter (train wreck)
const userName = order.getCustomer().getAddress().getCity().getName();

// ✅ GOOD: Ask, don't dig
class Order {
  getCustomerCityName(): string {
    return this.customer.getCityName();
  }
}

class Customer {
  getCityName(): string {
    return this.address.getCityName();
  }
}

const userName = order.getCustomerCityName();

Summary

  • Keep classes small and focused on a single responsibility

  • Favor composition over inheritance for flexibility

  • Program to interfaces to reduce coupling

  • Use meaningful, descriptive names

  • Minimize dependencies between classes

  • Encapsulate what varies

  • Follow the Law of Demeter

OOP

Core Principles

Object-Oriented Programming: Core Principles Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" that contain data and

Object-Oriented Programming: Core Principles

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects" that contain data and code. It organizes software design around data, or objects, rather than functions and logic.

The Four Pillars of OOP

1. Encapsulation

Encapsulation is the bundling of data and methods that operate on that data within a single unit (class). It restricts direct access to some of an object's components, which is a means of preventing accidental interference and misuse.

class BankAccount {
  private balance: number;
  private accountNumber: string;

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

  // Public method to access private data
  public getBalance(): number {
    return this.balance;
  }

  // Public method to modify private data safely
  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
      console.log(`Deposited $${amount}. New balance: $${this.balance}`);
    } else {
      console.log("Deposit amount must be positive");
    }
  }

  public withdraw(amount: number): boolean {
    if (amount > 0 && amount <= this.balance) {
      this.balance -= amount;
      console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
      return true;
    }
    console.log("Insufficient funds or invalid amount");
    return false;
  }
}

// Usage
const myAccount = new BankAccount("12345", 1000);
myAccount.deposit(500);
console.log(myAccount.getBalance()); // 1500
// myAccount.balance = 10000; // ERROR: Cannot access private property

2. Abstraction

Abstraction means hiding complex implementation details and showing only the necessary features of an object. It helps reduce programming complexity and effort.

// Abstract class defining the interface
abstract class Vehicle {
  protected brand: string;

  constructor(brand: string) {
    this.brand = brand;
  }

  // Abstract method - must be implemented by derived classes
  abstract startEngine(): void;
  abstract stopEngine(): void;

  // Concrete method - shared by all vehicles
  public displayInfo(): void {
    console.log(`This is a ${this.brand} vehicle`);
  }
}

// Concrete implementation
class Car extends Vehicle {
  startEngine(): void {
    console.log(`${this.brand} car engine started with key turn`);
  }

  stopEngine(): void {
    console.log(`${this.brand} car engine stopped`);
  }
}

class ElectricCar extends Vehicle {
  startEngine(): void {
    console.log(`${this.brand} electric car powered on silently`);
  }

  stopEngine(): void {
    console.log(`${this.brand} electric car powered off`);
  }
}

// Usage - we don't need to know implementation details
const myCar: Vehicle = new Car("Toyota");
myCar.startEngine();
myCar.displayInfo();

const myTesla: Vehicle = new ElectricCar("Tesla");
myTesla.startEngine();

3. Inheritance

Inheritance allows a class to inherit properties and methods from another class. It promotes code reusability and establishes a relationship between parent and child classes.

// Base class (Parent)
class Animal {
  protected name: string;
  protected age: number;

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

  public makeSound(): void {
    console.log("Some generic animal sound");
  }

  public eat(): void {
    console.log(`${this.name} is eating`);
  }

  public sleep(): void {
    console.log(`${this.name} is sleeping`);
  }
}

// Derived class (Child)
class Dog extends Animal {
  private breed: string;

  constructor(name: string, age: number, breed: string) {
    super(name, age); // Call parent constructor
    this.breed = breed;
  }

  // Override parent method
  public makeSound(): void {
    console.log(`${this.name} barks: Woof! Woof!`);
  }

  // Additional method specific to Dog
  public fetch(): void {
    console.log(`${this.name} is fetching the ball`);
  }
}

class Cat extends Animal {
  constructor(name: string, age: number) {
    super(name, age);
  }

  // Override parent method
  public makeSound(): void {
    console.log(`${this.name} meows: Meow!`);
  }

  // Additional method specific to Cat
  public scratch(): void {
    console.log(`${this.name} is scratching the furniture`);
  }
}

// Usage
const dog = new Dog("Buddy", 3, "Golden Retriever");
dog.makeSound(); // Buddy barks: Woof! Woof!
dog.eat();       // Buddy is eating
dog.fetch();     // Buddy is fetching the ball

const cat = new Cat("Whiskers", 2);
cat.makeSound(); // Whiskers meows: Meow!
cat.scratch();   // Whiskers is scratching the furniture

4. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common base class. It enables a single interface to represent different underlying forms (data types).

// Interface defining common behavior
interface Shape {
  calculateArea(): number;
  calculatePerimeter(): number;
  draw(): void;
}

// Different implementations
class Circle implements Shape {
  constructor(private radius: number) {}

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

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

  draw(): void {
    console.log(`Drawing a circle with radius ${this.radius}`);
  }
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

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

  calculatePerimeter(): number {
    return 2 * (this.width + this.height);
  }

  draw(): void {
    console.log(`Drawing a rectangle ${this.width}x${this.height}`);
  }
}

class Triangle implements Shape {
  constructor(
    private sideA: number,
    private sideB: number,
    private sideC: number
  ) {}

  calculateArea(): number {
    // Using Heron's formula
    const s = (this.sideA + this.sideB + this.sideC) / 2;
    return Math.sqrt(s * (s - this.sideA) * (s - this.sideB) * (s - this.sideC));
  }

  calculatePerimeter(): number {
    return this.sideA + this.sideB + this.sideC;
  }

  draw(): void {
    console.log(`Drawing a triangle with sides ${this.sideA}, ${this.sideB}, ${this.sideC}`);
  }
}

// Polymorphism in action - same interface, different implementations
function printShapeInfo(shape: Shape): void {
  shape.draw();
  console.log(`Area: ${shape.calculateArea().toFixed(2)}`);
  console.log(`Perimeter: ${shape.calculatePerimeter().toFixed(2)}`);
  console.log("---");
}

// All shapes can be treated uniformly
const shapes: Shape[] = [
  new Circle(5),
  new Rectangle(4, 6),
  new Triangle(3, 4, 5)
];

shapes.forEach(printShapeInfo);

Benefits of OOP

  • Modularity: Code is organized into self-contained objects

  • Reusability: Objects can be reused across different parts of a program or in different programs

  • Maintainability: Changes to one object don't affect others, making debugging easier

  • Scalability: New features can be added with minimal impact on existing code

  • Security: Encapsulation protects data from unauthorized access

OOP

Classes and Objects

Classes and Objects Classes are blueprints for creating objects. Objects are instances of classes that contain actual data and can perform actions. Defining a C

Classes and Objects

Classes are blueprints for creating objects. Objects are instances of classes that contain actual data and can perform actions.

Defining a Class

A class typically contains properties (data) and methods (functions) that operate on that data.

class Person {
  // Properties
  private firstName: string;
  private lastName: string;
  private age: number;
  private email: string;

  // Constructor
  constructor(firstName: string, lastName: string, age: number, email: string) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.email = email;
  }

  // Getter methods
  public getFullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  public getAge(): number {
    return this.age;
  }

  // Method
  public introduce(): void {
    console.log(`Hi, I'm ${this.getFullName()} and I'm ${this.age} years old.`);
  }

  public celebrateBirthday(): void {
    this.age++;
    console.log(`Happy Birthday! I'm now ${this.age} years old.`);
  }

  // Static method (belongs to class, not instance)
  public static compareAges(person1: Person, person2: Person): string {
    if (person1.age > person2.age) {
      return `${person1.getFullName()} is older`;
    } else if (person1.age < person2.age) {
      return `${person2.getFullName()} is older`;
    }
    return "They are the same age";
  }
}

// Creating objects (instances)
const john = new Person("John", "Doe", 30, "john@example.com");
const jane = new Person("Jane", "Smith", 25, "jane@example.com");

john.introduce(); // Hi, I'm John Doe and I'm 30 years old.
jane.introduce(); // Hi, I'm Jane Smith and I'm 25 years old.

console.log(Person.compareAges(john, jane)); // John Doe is older

Access Modifiers

Access modifiers control the visibility of class members:

  • public: Accessible from anywhere

  • private: Only accessible within the class

  • protected: Accessible within the class and its subclasses

class Employee {
  public id: number;           // Accessible everywhere
  private salary: number;       // Only within Employee class
  protected department: string; // Within Employee and subclasses

  constructor(id: number, salary: number, department: string) {
    this.id = id;
    this.salary = salary;
    this.department = department;
  }

  private calculateBonus(): number {
    return this.salary * 0.10;
  }

  public getPaymentInfo(): string {
    return `Salary: $${this.salary}, Bonus: $${this.calculateBonus()}`;
  }
}

class Manager extends Employee {
  private teamSize: number;

  constructor(id: number, salary: number, department: string, teamSize: number) {
    super(id, salary, department);
    this.teamSize = teamSize;
  }

  public getTeamInfo(): string {
    // Can access protected department, but not private salary
    return `Managing ${this.teamSize} people in ${this.department} department`;
  }
}

const emp = new Employee(1, 50000, "IT");
console.log(emp.id);                 // OK: public
console.log(emp.getPaymentInfo());   // OK: public method
// console.log(emp.salary);          // ERROR: private
// console.log(emp.calculateBonus()); // ERROR: private method

Constructors

Constructors are special methods that initialize new objects. They can be overloaded to provide different ways of creating objects.

class Product {
  constructor(
    public readonly id: string,
    public name: string,
    public price: number,
    public inStock: boolean = true
  ) {
    // Shorthand property initialization
  }

  // Factory method pattern for creating products
  static createFromJSON(json: any): Product {
    return new Product(
      json.id,
      json.name,
      json.price,
      json.inStock ?? true
    );
  }

  static createDiscountedProduct(name: string, originalPrice: number, discount: number): Product {
    const discountedPrice = originalPrice * (1 - discount);
    return new Product(
      `DISC-${Date.now()}`,
      `${name} (${discount * 100}% OFF)`,
      discountedPrice
    );
  }
}

// Different ways to create products
const product1 = new Product("P001", "Laptop", 999.99);
const product2 = Product.createFromJSON({
  id: "P002",
  name: "Mouse",
  price: 29.99,
  inStock: true
});
const product3 = Product.createDiscountedProduct("Keyboard", 79.99, 0.20);

console.log(product3.name);  // "Keyboard (20% OFF)"
console.log(product3.price); // 63.992

Getters and Setters

Getters and setters allow controlled access to private properties with validation and computed properties.

class Temperature {
  private celsius: number;

  constructor(celsius: number) {
    this.celsius = celsius;
  }

  // Getter: read like a property
  get fahrenheit(): number {
    return (this.celsius * 9/5) + 32;
  }

  // Setter: write like a property with validation
  set fahrenheit(value: number) {
    if (value < -459.67) {
      throw new Error("Temperature cannot be below absolute zero!");
    }
    this.celsius = (value - 32) * 5/9;
  }

  get kelvin(): number {
    return this.celsius + 273.15;
  }

  set kelvin(value: number) {
    if (value < 0) {
      throw new Error("Kelvin cannot be negative!");
    }
    this.celsius = value - 273.15;
  }

  // Method to display all formats
  display(): void {
    console.log(`${this.celsius.toFixed(2)}°C = ${this.fahrenheit.toFixed(2)}°F = ${this.kelvin.toFixed(2)}K`);
  }
}

const temp = new Temperature(25);
temp.display(); // 25.00°C = 77.00°F = 298.15K

temp.fahrenheit = 98.6; // Set using Fahrenheit
temp.display(); // 37.00°C = 98.60°F = 310.15K

temp.kelvin = 273.15; // Set using Kelvin
temp.display(); // 0.00°C = 32.00°F = 273.15K

Static Members

Static members belong to the class itself rather than to instances of the class. They are useful for utility functions and shared data.

class MathUtils {
  // Static property
  static readonly PI = 3.14159265359;

  // Static method
  static circleArea(radius: number): number {
    return this.PI * radius ** 2;
  }

  static circleCircumference(radius: number): number {
    return 2 * this.PI * radius;
  }

  static degreesToRadians(degrees: number): number {
    return degrees * (this.PI / 180);
  }

  static radiansToDegrees(radians: number): number {
    return radians * (180 / this.PI);
  }
}

// Use without creating an instance
console.log(MathUtils.circleArea(5));           // 78.54
console.log(MathUtils.degreesToRadians(180));   // 3.14

class Counter {
  private static count: number = 0;
  public readonly id: number;

  constructor() {
    Counter.count++;
    this.id = Counter.count;
  }

  static getCount(): number {
    return Counter.count;
  }

  static reset(): void {
    Counter.count = 0;
  }
}

const c1 = new Counter();
const c2 = new Counter();
const c3 = new Counter();
console.log(Counter.getCount()); // 3
console.log(c1.id, c2.id, c3.id); // 1 2 3

Keep your OOP knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever