Advanced OOP

TypeScript Intermediate — 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.