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.
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); // 30000Key points
- Keep state
privateand expose behaviour through methods. - Depend on interfaces (
Notifier), not concrete classes, so parts are swappable. - Extend
Errorfor 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.
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' ]