TypeScript Intermediate
Course 2 · Chapter 9 · Design Patterns in TypeScript
🏗️ Design Patterns in TypeScript
Design patterns are proven solutions to common problems. This final chapter brings together everything you've learned—generics, decorators, async, error handling, type utilities—to build real-world patterns with TypeScript's type system. Master these patterns and you'll write professional-grade code.
👤 Singleton: One Instance
Ensure only one instance of a class exists globally:
class Logger {
private static instance: Logger | null = null;
private logs: string[] = [];
private constructor() {}
static getInstance(): Logger {
if (Logger.instance === null) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string) {
this.logs.push(message);
console.log(`[${new Date().toISOString()}] ${message}`);
}
getLogs(): string[] {
return [...this.logs];
}
}
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1 === logger2;
logger1.log("Hello");
console.log(logger2.getLogs());
🏭 Factory: Flexible Creation
Create objects without exposing creation logic. Useful for supporting multiple implementations:
interface Database {
query(sql: string): Promise<any[]>;
close(): Promise<void>;
}
class PostgreSQL implements Database {
async query(sql: string) {
console.log(`PostgreSQL: ${sql}`);
return [];
}
async close() {}
}
class MySQL implements Database {
async query(sql: string) {
console.log(`MySQL: ${sql}`);
return [];
}
async close() {}
}
function createDatabase(type: "postgres" | "mysql"): Database {
if (type === "postgres") {
return new PostgreSQL();
} else {
return new MySQL();
}
}
const db: Database = createDatabase("postgres");
await db.query("SELECT * FROM users");
👁️ Observer: Reactive Updates
Notify multiple objects when state changes (event-driven architecture):
interface Observer {
update(data: unknown): void;
}
class Subject {
private observers: Observer[] = [];
attach(observer: Observer) {
this.observers.push(observer);
}
detach(observer: Observer) {
this.observers = this.observers.filter(o => o !== observer);
}
notify(data: unknown) {
this.observers.forEach(o => o.update(data));
}
}
class Logger implements Observer {
update(data: unknown) {
console.log(`[Log] ${JSON.stringify(data)}`);
}
}
class EmailNotifier implements Observer {
update(data: unknown) {
console.log(`[Email] Sending notification for ${data}`);
}
}
const subject = new Subject();
subject.attach(new Logger());
subject.attach(new EmailNotifier());
subject.notify({ event: "user.created", userId: 1 });
⚙️ Strategy: Swappable Algorithms
Encapsulate algorithms so they're interchangeable at runtime:
interface SortStrategy {
sort(arr: number[]): number[];
}
class BubbleSort implements SortStrategy {
sort(arr: number[]): number[] {
console.log("Sorting with Bubble Sort");
return [...arr].sort();
}
}
class QuickSort implements SortStrategy {
sort(arr: number[]): number[] {
console.log("Sorting with Quick Sort");
return [...arr].sort();
}
}
class Sorter {
constructor(private strategy: SortStrategy) {}
execute(arr: number[]): number[] {
return this.strategy.sort(arr);
}
}
const sorter1 = new Sorter(new BubbleSort());
console.log(sorter1.execute([3, 1, 2]));
const sorter2 = new Sorter(new QuickSort());
console.log(sorter2.execute([3, 1, 2]));
💉 Dependency Injection: Loose Coupling
Pass dependencies explicitly instead of hardcoding. Makes code testable and flexible:
class UserService_Bad {
private db = new PostgreSQL();
}
class UserService {
constructor(private db: Database) {}
async getUser(id: number) {
return await this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
const service = new UserService(new PostgreSQL());
await service.getUser(1);
class MockDB implements Database {
async query() { return [{ id: 1, name: "Test User" }]; }
async close() {}
}
const testService = new UserService(new MockDB());
💻 Coding Challenges
Challenge 1: Singleton Logger
Implement a Logger singleton with methods to log at different levels (info, warn, error). Ensure only one instance exists across the app.
Goal: Practice singleton pattern with application-level state.
→ Solution
Challenge 2: Factory with Type Safety
Create a factory that builds different notification strategies (Email, SMS, Push). Use generics to ensure type safety across implementations.
Goal: Combine factory pattern with generics.
→ Solution
Challenge 3: Observer with Type-Safe Events
Implement an event bus with typed events (discriminated union). Add/remove listeners for specific events. Emit with type safety.
Goal: Combine Observer pattern with type utilities.
→ Solution
💡 Choose Patterns Carefully
Not every problem needs a design pattern. Start simple, refactor into patterns when complexity demands it. The best code is often the simplest code that solves the problem. Patterns are tools for specific situations, not dogma.
🎯 Course Complete!
You've mastered TypeScript Intermediate. You now understand advanced types, generics, decorators, async patterns, modules, OOP, type utilities, error handling, and design patterns. You're ready to build professional-grade TypeScript applications. Consider exploring Advanced TypeScript (Course 3) for even deeper mastery—or apply these skills to real projects, where the best learning happens.