Design patterns are proven solutions to common programming problems. JavaScript's flexible nature lets you implement classic patterns cleanly, often more concisely than in typed languages. Here are the most useful patterns for modern JavaScript/TypeScript development.
Observer Pattern (Pub/Sub)
Components communicate without knowing about each other:
class EventEmitter {
#listeners = new Map();
on(event, listener) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(listener);
return () => this.off(event, listener); // Returns unsubscribe function
}
off(event, listener) {
this.#listeners.get(event)?.delete(listener);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(fn => fn(...args));
}
once(event, listener) {
const unsubscribe = this.on(event, (...args) => {
listener(...args);
unsubscribe();
});
}
}
// Usage
const bus = new EventEmitter();
const unsubscribe = bus.on('user:login', (user) => {
console.log('User logged in:', user.name);
});
bus.emit('user:login', { id: 1, name: 'Alice' });
unsubscribe(); // Clean up
Strategy Pattern
Swap algorithms/behaviors at runtime:
// Different sort strategies
const strategies = {
alphabetical: (a, b) => a.name.localeCompare(b.name),
byDate: (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
byScore: (a, b) => b.score - a.score,
};
class DataTable {
#data;
#sortStrategy;
constructor(data, strategy = 'alphabetical') {
this.#data = data;
this.#sortStrategy = strategies[strategy];
}
setStrategy(name) {
this.#sortStrategy = strategies[name];
return this;
}
getSorted() {
return [...this.#data].sort(this.#sortStrategy);
}
}
const table = new DataTable(data);
table.setStrategy('byDate').getSorted();
table.setStrategy('byScore').getSorted();
Proxy Pattern
Intercept property access, mutations, and function calls:
function createValidator(target, rules) {
return new Proxy(target, {
set(obj, key, value) {
const rule = rules[key];
if (rule && !rule.validate(value)) {
throw new TypeError(rule.message(key, value));
}
obj[key] = value;
return true;
}
});
}
const rules = {
age: {
validate: v => typeof v === 'number' && v >= 0 && v <= 150,
message: (k, v) => `${k} must be a number between 0-150, got ${v}`,
},
email: {
validate: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
message: (k, v) => `${k} must be a valid email, got "${v}"`,
},
};
const user = createValidator({}, rules);
user.age = 30; // OK
user.age = -5; // TypeError: age must be a number between 0-150...
user.email = 'alice@example.com'; // OK
user.email = 'notanemail'; // TypeError
Builder Pattern
Construct complex objects step by step:
class QueryBuilder {
#table = '';
#conditions = [];
#columns = ['*'];
#limit = null;
#order = null;
from(table) {
this.#table = table;
return this; // Return this for chaining
}
select(...columns) {
this.#columns = columns;
return this;
}
where(condition) {
this.#conditions.push(condition);
return this;
}
orderBy(column, direction = 'ASC') {
this.#order = `${column} ${direction}`;
return this;
}
limit(n) {
this.#limit = n;
return this;
}
build() {
let query = `SELECT ${this.#columns.join(', ')} FROM ${this.#table}`;
if (this.#conditions.length) {
query += ` WHERE ${this.#conditions.join(' AND ')}`;
}
if (this.#order) query += ` ORDER BY ${this.#order}`;
if (this.#limit) query += ` LIMIT ${this.#limit}`;
return query;
}
}
const query = new QueryBuilder()
.from('users')
.select('id', 'name', 'email')
.where('active = true')
.where('age > 18')
.orderBy('name')
.limit(50)
.build();
// SELECT id, name, email FROM users WHERE active = true AND age > 18 ORDER BY name ASC LIMIT 50
Decorator Pattern
Add behavior to objects/functions without modifying them:
// Function decorator
function withLogging(fn, name = fn.name) {
return async function(...args) {
console.log(`${name} called with:`, args);
const start = performance.now();
try {
const result = await fn.apply(this, args);
console.log(`${name} returned:`, result, `(${(performance.now() - start).toFixed(2)}ms)`);
return result;
} catch (error) {
console.error(`${name} threw:`, error);
throw error;
}
};
}
function withCache(fn, ttl = 60000) {
const cache = new Map();
return async function(...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value;
}
const value = await fn.apply(this, args);
cache.set(key, { value, timestamp: Date.now() });
return value;
};
}
// Compose decorators
let fetchUser = async (id) => {
const resp = await fetch(`/api/users/${id}`);
return resp.json();
};
fetchUser = withLogging(fetchUser);
fetchUser = withCache(fetchUser, 30000); // Cache for 30s
Singleton Pattern
Ensure only one instance exists:
class DatabaseConnection {
static #instance = null;
constructor(connectionString) {
if (DatabaseConnection.#instance) {
return DatabaseConnection.#instance;
}
this.connectionString = connectionString;
this.connected = false;
DatabaseConnection.#instance = this;
}
static getInstance() {
if (!DatabaseConnection.#instance) {
throw new Error('Database not initialized. Call new DatabaseConnection(url) first.');
}
return DatabaseConnection.#instance;
}
static reset() { // For testing
DatabaseConnection.#instance = null;
}
}
// ES Module singleton (simpler): just export an instance
// db.js
export const db = new DatabaseConnection(process.env.DB_URL);
Command Pattern
Encapsulate operations as objects — enables undo/redo:
class CommandHistory {
#history = [];
#undoStack = [];
execute(command) {
command.execute();
this.#history.push(command);
this.#undoStack = []; // Clear redo stack on new command
}
undo() {
const command = this.#history.pop();
if (command) {
command.undo();
this.#undoStack.push(command);
}
}
redo() {
const command = this.#undoStack.pop();
if (command) {
command.execute();
this.#history.push(command);
}
}
}
// A specific command
class AddTextCommand {
constructor(editor, text, position) {
this.editor = editor;
this.text = text;
this.position = position;
}
execute() {
this.editor.insertAt(this.text, this.position);
}
undo() {
this.editor.deleteAt(this.position, this.text.length);
}
}
These patterns aren't just academic — they appear in React (state management as observers), React Query (decorator for fetch), form libraries (builder for field configurations), and text editors (command pattern for undo/redo).
Top comments (0)