Advanced OOP

TypeScript Intermediate
Course 2 Β· Chapter 6 Β· Advanced OOP

πŸ›οΈ Advanced OOP

Object-oriented programming in TypeScript goes beyond basic classes. This chapter covers abstract classes (enforce contracts), access modifiers (control visibility), static members (class-level data), and getters/setters (computed properties). These patterns scale code from hundreds to millions of lines.

πŸ”· Abstract Classes: Enforce Contracts

An abstract class can't be instantiated. It defines a contract that subclasses must implement:

abstract class Animal {
  abstract makeSound(): void;  // Subclasses MUST implement

  sleep() {
    console.log("Zzz...");  // Concrete method (optional to override)
  }
}

class Dog extends Animal {
  makeSound() {
    console.log("Woof!");  // βœ… Implements abstract method
  }
}

const animal = new Animal();  // ❌ Error: can't instantiate abstract class
const dog = new Dog();         // βœ… OK
dog.makeSound();           // "Woof!"

Abstract Methods

abstract methodName(); β€” no body. Subclasses must implement.

Abstract Properties

abstract prop: Type; β€” subclasses must define.

Concrete Methods

Regular methods with bodies. Subclasses can override or use as-is.

Use Case

Define a template. Subclasses fill in the details.

πŸ”’ Access Modifiers: Control Visibility

TypeScript provides public, private, and protected to control who can access members:

class BankAccount {
  public accountHolder: string;        // Anyone can read/write
  private balance: number = 0;     // Only this class can access
  protected log: string[] = [];   // This class and subclasses

  constructor(holder: string) {
    this.accountHolder = holder;
  }

  public deposit(amount: number) {
    this.balance += amount;
    this.recordLog(`Deposited ${amount}`);
  }

  private recordLog(message: string) {
    this.log.push(message);
  }

  protected getBalance() {
    return this.balance;  // Only subclasses can call
  }
}

const account = new BankAccount("Alice");
account.deposit(100);              // βœ… public
console.log(account.accountHolder);  // βœ… public
// account.balance;           // ❌ private β€” error
// account.recordLog("...");  // ❌ private β€” error

Access Levels

public (default)
public prop: string;
// Accessible everywhere
private
private prop: string;
// Only inside this class
protected
protected prop: string;
// This class + subclasses
readonly
readonly prop: string;
// Can't reassign after init

πŸ“Œ Static Members: Class-Level Data

Static members belong to the class itself, not instances:

class Counter {
  static count: number = 0;

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

  constructor() {
    Counter.count++;  // Increment class-level counter
  }
}

const c1 = new Counter();
const c2 = new Counter();

console.log(Counter.count);  // 2 β€” shared across all instances
Counter.reset();
console.log(Counter.count);  // 0

Static Properties

static prop: Type; β€” shared by all instances.

Static Methods

static methodName() { } β€” called on the class, not instances.

Access

Use ClassName.property, not instance.property.

Use Cases

Configuration, factories, utility functions, singletons.

🎯 Getters & Setters: Computed Properties

Getters and setters let you use properties while running custom logic:

class User {
  private _age: number = 0;

  get age(): number {
    return this._age;
  }

  set age(value: number) {
    if (value < 0) {
      throw new Error("Age can't be negative");
    }
    this._age = value;
  }
}

const user = new User();
user.age = 25;          // βœ… Calls setter with validation
console.log(user.age);  // βœ… Calls getter
user.age = -5;          // ❌ Error: Age can't be negative

Why use getters/setters?

  • Validation: Check constraints before setting
  • Computed values: Calculate on-the-fly
  • Side effects: Log, notify, update cache
  • Clean API: Property syntax instead of methods

πŸ—οΈ Real-World Pattern: Sealed Class with Private State

Robust Configuration Class

abstract class BaseConfig {
  abstract validate(): boolean;
}

class AppConfig extends BaseConfig {
  private _port: number;
  private _host: string;
  readonly version: string;
  static instance: AppConfig;

  private constructor(port: number, host: string) {
    super();
    this._port = port;
    this._host = host;
    this.version = "1.0.0";
  }

  static create(port: number, host: string): AppConfig {
    if (!AppConfig.instance) {
      AppConfig.instance = new AppConfig(port, host);
    }
    return AppConfig.instance;
  }

  get port(): number {
    return this._port;
  }

  set port(value: number) {
    if (value < 1 || value > 65535) {
      throw new Error("Invalid port");
    }
    this._port = value;
  }

  validate(): boolean {
    return this._port > 0 && this._host.length > 0;
  }
}

// Usage: factory pattern with singleton
const config = AppConfig.create(3000, "localhost");
console.log(config.port);  // 3000

πŸ’» Coding Challenges

Challenge 1: Abstract Class with Multiple Subclasses

Create an abstract Vehicle class with abstract methods. Implement two subclasses (Car, Bike) that satisfy the contract.

Goal: Practice abstract classes and polymorphism.

β†’ Solution

Challenge 2: Access Control & Encapsulation

Create a BankAccount class with private balance, public deposit/withdraw methods, and a getter for balance. Ensure balance can't go negative.

Goal: Understand private/public boundaries and encapsulation.

β†’ Solution

Challenge 3: Static Singleton Pattern

Create a Database class with a static getInstance() method that returns a single instance (singleton). Add static initialization logic.

Goal: Implement the singleton pattern with static members.

β†’ Solution

⚠️ Gotcha: Private at Compile Time Only

TypeScript's private is compile-time only. In the compiled JavaScript, private fields are just regular propertiesβ€”they're not truly inaccessible at runtime. For true privacy, use JavaScript's # private fields. But for most purposes, TypeScript's private is sufficient discipline.

🎯 What's Next

With advanced OOP patterns mastered, we'll explore Type Utilities & Inference β€” leveraging TypeScript's powerful type system to build reusable, composable type utilities.