正在加载,请稍候…

Software Design Patterns in JavaScript and TypeScript: A Practical Guide

Learn the most useful software design patterns with practical JavaScript/TypeScript examples: Singleton, Factory, Observer, Strategy, Decorator, Repository, and more.

Which Design Patterns Actually Matter in Modern JavaScript?

The original GoF "Design Patterns" book (1994) described 23 patterns in C++. Many of them are solving problems that JavaScript handles differently — dynamic typing, first-class functions, and prototype-based objects change the calculus significantly.

This guide focuses on the patterns that genuinely improve real codebases in 2026: the ones you'll encounter in popular frameworks, in job interviews, and in the codebases you'll actually maintain.

Creational Patterns

Singleton

Ensure a class has only one instance:

class DatabaseConnection {
  private static instance: DatabaseConnection | null = null;
  private pool: Pool;
  
  private constructor() {
    this.pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 20,
    });
  }
  
  // The only way to get the instance
  static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }
  
  async query(sql: string, params?: unknown[]) {
    return this.pool.query(sql, params);
  }
}

// Usage — always gets the same instance
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true

// Modern alternative: module-level singleton (simpler)
// db.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export default pool; // ES modules are singletons by default!

Factory Method

Create objects without specifying the exact class:

interface Notifier {
  send(to: string, message: string): Promise<void>;
}

class EmailNotifier implements Notifier {
  async send(to: string, message: string) {
    await sendEmail({ to, subject: 'Notification', body: message });
  }
}

class SMSNotifier implements Notifier {
  async send(to: string, message: string) {
    await sendSMS({ phone: to, text: message });
  }
}

class SlackNotifier implements Notifier {
  async send(to: string, message: string) {
    await postToSlack({ channel: to, text: message });
  }
}

// Factory
class NotifierFactory {
  static create(type: 'email' | 'sms' | 'slack'): Notifier {
    const map = {
      email: EmailNotifier,
      sms: SMSNotifier,
      slack: SlackNotifier,
    };
    
    const NotifierClass = map[type];
    if (!NotifierClass) throw new Error(`Unknown notifier type: ${type}`);
    
    return new NotifierClass();
  }
}

// Usage — client doesn't know which class was created
const notifier = NotifierFactory.create(process.env.NOTIFIER_TYPE as 'email');
await notifier.send('alice@example.com', 'Your order is ready!');

Builder

Construct complex objects step by step:

class QueryBuilder {
  private table = '';
  private conditions: string[] = [];
  private columns = '*';
  private limitVal?: number;
  private orderByCol?: string;
  private orderByDir = 'ASC';
  private params: unknown[] = [];

  from(table: string) {
    this.table = table;
    return this; // Method chaining
  }
  
  select(...columns: string[]) {
    this.columns = columns.join(', ');
    return this;
  }
  
  where(condition: string, ...params: unknown[]) {
    this.params.push(...params);
    this.conditions.push(condition.replace('?', `${this.params.length}`));
    return this;
  }
  
  limit(n: number) {
    this.limitVal = n;
    return this;
  }
  
  orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC') {
    this.orderByCol = column;
    this.orderByDir = direction;
    return this;
  }
  
  build(): { sql: string; params: unknown[] } {
    let sql = `SELECT ${this.columns} FROM ${this.table}`;
    
    if (this.conditions.length > 0) {
      sql += ` WHERE ${this.conditions.join(' AND ')}`;
    }
    if (this.orderByCol) {
      sql += ` ORDER BY ${this.orderByCol} ${this.orderByDir}`;
    }
    if (this.limitVal !== undefined) {
      sql += ` LIMIT ${this.limitVal}`;
    }
    
    return { sql, params: this.params };
  }
}

// Clean, readable query construction
const { sql, params } = new QueryBuilder()
  .from('users')
  .select('id', 'name', 'email')
  .where('country = ?', 'US')
  .where('created_at > ?', new Date('2026-01-01'))
  .orderBy('name')
  .limit(20)
  .build();

Structural Patterns

Decorator

Add behavior to objects without modifying them:

interface UserService {
  findById(id: string): Promise<User | null>;
  create(data: CreateUserDto): Promise<User>;
}

// Base implementation
class UserServiceImpl implements UserService {
  async findById(id: string) {
    return db.query('SELECT * FROM users WHERE id = $1', [id]);
  }
  async create(data: CreateUserDto) {
    return db.query('INSERT INTO users ...', [data.name, data.email]);
  }
}

// Caching decorator (wraps the service)
class CachedUserService implements UserService {
  constructor(
    private service: UserService,
    private cache: Redis
  ) {}
  
  async findById(id: string) {
    const cached = await this.cache.get(`user:${id}`);
    if (cached) return JSON.parse(cached);
    
    const user = await this.service.findById(id);
    if (user) await this.cache.setEx(`user:${id}`, 3600, JSON.stringify(user));
    return user;
  }
  
  async create(data: CreateUserDto) {
    const user = await this.service.create(data);
    await this.cache.setEx(`user:${user.id}`, 3600, JSON.stringify(user));
    return user;
  }
}

// Logging decorator
class LoggingUserService implements UserService {
  constructor(private service: UserService, private logger: Logger) {}
  
  async findById(id: string) {
    const start = Date.now();
    const user = await this.service.findById(id);
    this.logger.info({ id, duration: Date.now() - start }, 'findById');
    return user;
  }
  
  // ... same for create
}

// Compose: Logging(Caching(Base))
const userService: UserService = 
  new LoggingUserService(
    new CachedUserService(
      new UserServiceImpl(),
      redis
    ),
    logger
  );

Behavioral Patterns

Observer (Event Emitter)

type EventMap = {
  'user.created': { userId: string; email: string };
  'order.placed': { orderId: string; total: number };
  'payment.failed': { orderId: string; reason: string };
};

class TypedEventBus {
  private listeners = new Map<string, Set<Function>>();
  
  on<K extends keyof EventMap>(
    event: K,
    listener: (data: EventMap[K]) => void
  ): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
    
    // Return unsubscribe function
    return () => this.listeners.get(event)?.delete(listener);
  }
  
  emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
    this.listeners.get(event)?.forEach(listener => {
      listener(data);
    });
  }
}

const bus = new TypedEventBus();

// Subscribe (type-safe!)
const unsubscribe = bus.on('user.created', ({ userId, email }) => {
  sendWelcomeEmail(email);
});

// Emit
bus.emit('user.created', { userId: '123', email: 'alice@example.com' });

// Clean up
unsubscribe();

Strategy

Define a family of algorithms, make them interchangeable:

interface SortStrategy<T> {
  sort(data: T[]): T[];
}

class QuickSort<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    return [...data].sort(); // Simplified
  }
}

class MergeSort<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    // Merge sort implementation
    return [...data].sort();
  }
}

class DataProcessor<T> {
  constructor(private strategy: SortStrategy<T>) {}
  
  setStrategy(strategy: SortStrategy<T>) {
    this.strategy = strategy;
  }
  
  process(data: T[]): T[] {
    return this.strategy.sort(data);
  }
}

// Switch algorithms at runtime
const processor = new DataProcessor(new QuickSort<number>());
processor.process([3, 1, 4, 1, 5]);

// For large datasets, switch to merge sort
if (data.length > 10000) {
  processor.setStrategy(new MergeSort<number>());
}

Repository Pattern

Abstract data access behind an interface:

interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<User>;
  delete(id: string): Promise<void>;
}

// PostgreSQL implementation
class PgUserRepository implements UserRepository {
  constructor(private pool: Pool) {}
  
  async findById(id: string) {
    const result = await this.pool.query(
      'SELECT * FROM users WHERE id = $1', [id]
    );
    return result.rows[0] ?? null;
  }
  
  async findByEmail(email: string) {
    const result = await this.pool.query(
      'SELECT * FROM users WHERE email = $1', [email]
    );
    return result.rows[0] ?? null;
  }
  
  async save(user: User) {
    const result = await this.pool.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [user.name, user.email]
    );
    return result.rows[0];
  }
  
  async delete(id: string) {
    await this.pool.query('DELETE FROM users WHERE id = $1', [id]);
  }
}

// In-memory implementation for tests!
class InMemoryUserRepository implements UserRepository {
  private users = new Map<string, User>();
  
  async findById(id: string) {
    return this.users.get(id) ?? null;
  }
  
  async findByEmail(email: string) {
    return [...this.users.values()].find(u => u.email === email) ?? null;
  }
  
  async save(user: User) {
    const saved = { ...user, id: user.id ?? crypto.randomUUID() };
    this.users.set(saved.id, saved);
    return saved;
  }
  
  async delete(id: string) {
    this.users.delete(id);
  }
}

// Service uses the interface — doesn't know which implementation
class UserService {
  constructor(private users: UserRepository) {}
  
  async register(email: string, password: string): Promise<User> {
    const existing = await this.users.findByEmail(email);
    if (existing) throw new Error('Email already registered');
    
    return this.users.save({ email, passwordHash: hashPassword(password) });
  }
}

// Production
const userService = new UserService(new PgUserRepository(pool));

// Tests — fast, no database needed
const userService = new UserService(new InMemoryUserRepository());

Anti-Patterns to Avoid

// ❌ God Object — one class does everything
class Application {
  handleLogin() { ... }
  sendEmail() { ... }
  processPayment() { ... }
  generatePDF() { ... }
  // 50 more methods...
}

// ✅ Single Responsibility — each class has one job

// ❌ Anemic Domain Model — objects are just data bags
class User {
  name: string;
  email: string;
  // No behavior — all logic in UserService
}

// ✅ Rich Domain Model — behavior lives with data
class User {
  private passwordHash: string;
  
  verifyPassword(password: string): boolean {
    return bcrypt.compare(password, this.passwordHash);
  }
  
  changeEmail(newEmail: string): void {
    if (!isValidEmail(newEmail)) throw new Error('Invalid email');
    this.email = newEmail;
    this.emit('email.changed', { userId: this.id });
  }
}

→ Visualize and inspect complex data structures with the JSON Viewer.