IntermediateTypeScript · Lesson 3 of 9

Classes & Object-Oriented Design

Constructors, access modifiers, interfaces, inheritance and abstract classes.

A class bundles data with the methods that operate on it. TypeScript adds access modifiers: private members are hidden from outside code, protected ones are visible to subclasses, and readonly fields can't change after construction.

Parameter properties (constructor(private balance: number)) declare and assign a field in one step.

A class can implements an interface to promise it has a certain shape — this lets you swap implementations (for example, a real payment gateway vs a fake one in tests). Prefer composition and interfaces over deep inheritance chains.

account.tsTypeScript
interface Notifier {
  send(to: string, message: string): void;
}

class ConsoleNotifier implements Notifier {
  send(to: string, message: string): void {
    console.log("[to " + to + "] " + message);
  }
}

class InsufficientFundsError extends Error {
  constructor(public readonly shortBy: number) {
    super("Insufficient funds: short by " + shortBy);
    this.name = "InsufficientFundsError";
  }
}

class Account {
  private balance = 0;

  constructor(
    public readonly owner: string,
    private readonly notifier: Notifier,
  ) {}

  deposit(amount: number): void {
    if (amount <= 0) throw new Error("Deposit must be positive");
    this.balance += amount;
    this.notifier.send(this.owner, "Deposited " + amount + ". Balance " + this.balance);
  }

  withdraw(amount: number): void {
    if (amount > this.balance) throw new InsufficientFundsError(amount - this.balance);
    this.balance -= amount;
    this.notifier.send(this.owner, "Withdrew " + amount + ". Balance " + this.balance);
  }

  get currentBalance(): number {
    return this.balance;
  }
}

const acc = new Account("Amina", new ConsoleNotifier());
acc.deposit(50000);
acc.withdraw(20000);
console.log(acc.currentBalance); // 30000

Key points

  • Keep state private and expose behaviour through methods.
  • Depend on interfaces (Notifier), not concrete classes, so parts are swappable.
  • Extend Error for domain-specific errors that callers can detect.

Exercise

Build a Library class that manages Book objects: add a book, borrow by ISBN, return a book, and list available books. Throw a custom BookNotAvailableError when borrowing a book that is already out.

Show solution

Try the exercise yourself first — then compare your approach with this one.

Library keeps its books in a private Map keyed by ISBN. Borrowing checks two failure cases — unknown ISBN and already borrowed — and throws a specific error for the second, so callers can tell them apart with instanceof.

library.tsTypeScript
interface Book {
  readonly isbn: string;
  title: string;
  available: boolean;
}

class BookNotAvailableError extends Error {
  constructor(public readonly isbn: string) {
    super("Book " + isbn + " is already borrowed");
    this.name = "BookNotAvailableError";
  }
}

class Library {
  private books = new Map<string, Book>();

  add(isbn: string, title: string): void {
    this.books.set(isbn, { isbn, title, available: true });
  }

  borrow(isbn: string): Book {
    const book = this.books.get(isbn);
    if (!book) throw new Error("No book with ISBN " + isbn);
    if (!book.available) throw new BookNotAvailableError(isbn);
    book.available = false;
    return book;
  }

  giveBack(isbn: string): void {
    const book = this.books.get(isbn);
    if (book) book.available = true;
  }

  availableBooks(): string[] {
    return [...this.books.values()].filter((b) => b.available).map((b) => b.title);
  }
}

const library = new Library();
library.add("978-1", "Things Fall Apart");
library.add("978-2", "Kinjeketile");

library.borrow("978-1");
console.log(library.availableBooks());      // [ 'Kinjeketile' ]

try {
  library.borrow("978-1");
} catch (err) {
  if (err instanceof BookNotAvailableError) console.log("Sorry:", err.message);
}

library.giveBack("978-1");
console.log(library.availableBooks());      // [ 'Things Fall Apart', 'Kinjeketile' ]

Check your understanding

  1. What does constructor(private readonly notifier: Notifier) {} do?

  2. Why depend on an interface such as Notifier instead of a concrete SmsNotifier class?

  3. What is the benefit of a custom error class like InsufficientFundsError?

  4. Which member can be accessed from outside the class?

Ask AI