Historical archive
Common Design Patterns in TypeScript
TypeScript examples of the singleton, factory, proxy, observer, and strategy patterns, with notes on when each pattern is useful.
Singleton
Use a singleton when an application should initialize one shared instance rather than create several independent instances. Examples include a WebSocket connection manager or a global sound controller.
class WebSocketController {
private static instance: WebSocketController
// Prevent callers from constructing an instance directly.
private constructor() {}
static getInstance() {
if (!WebSocketController.instance) {
WebSocketController.instance = new WebSocketController()
}
return WebSocketController.instance
}
}
const socket = WebSocketController.getInstance()
Factory
A factory encapsulates object creation so callers choose a product type without knowing the construction details.
enum CarType {
BMW,
Audi,
Benz,
}
class Car {}
class Bmw extends Car {}
class Audi extends Car {}
class Benz extends Car {}
class CarFactory {
static create(type: CarType): Car {
switch (type) {
case CarType.BMW:
return new Bmw()
case CarType.Audi:
return new Audi()
case CarType.Benz:
return new Benz()
}
}
}
const bmw = CarFactory.create(CarType.BMW)
Proxy
A proxy adds control or behavior around a target without changing the target object. This separates cross-cutting behavior such as logging, authorization, or reactive tracking.
Proxy and decorator patterns both wrap behavior. A decorator primarily extends or enhances an object, while a proxy emphasizes access, indirection, or control. Vue 3 uses JavaScript proxies in its reactivity system.
class Person {
constructor(public name: string) {}
doSomething() {
console.log('do something')
}
}
const person = new Proxy(new Person('Stu'), {
get(target, property) {
console.log('Run an additional proxy task')
return Reflect.get(target, property)
},
})
person.doSomething()
Observer
When one object’s state changes, an observer pattern notifies all registered dependents. This is a one-to-many relationship.
interface Message {
event: string
payload: string
}
interface StateObserver {
update(message: Message): void
}
class Person {
private observers: StateObserver[] = []
private currentState: Record<string, string> = {}
addObserver(observer: StateObserver) {
this.observers.push(observer)
}
removeObserver(observer: StateObserver) {
const index = this.observers.indexOf(observer)
if (index > -1) {
this.observers.splice(index, 1)
}
}
private notifyObservers(message: Message) {
this.observers.forEach(observer => observer.update(message))
}
setState(event: string, payload: string) {
this.currentState[event] = payload
this.notifyObservers({ event, payload })
}
get state() {
return this.currentState
}
}
class ConsoleObserver implements StateObserver {
constructor(private name: string) {}
update(message: Message) {
console.log(
`${this.name} saw ${message.event} change to ${message.payload}`
)
}
}
const person = new Person()
const observer1 = new ConsoleObserver('Observer 1')
const observer2 = new ConsoleObserver('Observer 2')
const observer3 = new ConsoleObserver('Observer 3')
person.addObserver(observer1)
person.addObserver(observer2)
person.addObserver(observer3)
person.setState('weather', 'cloudy')
person.removeObserver(observer2)
person.setState('weather', 'light rain')
Strategy
The strategy pattern separates an algorithm from the context that uses it. A caller can switch strategies freely, avoid a large conditional, and add a new strategy by implementing the same interface.
interface Strategy {
calculate(left: number, right: number): number
}
class AddStrategy implements Strategy {
calculate(left: number, right: number) {
return left + right
}
}
class SubtractStrategy implements Strategy {
calculate(left: number, right: number) {
return left - right
}
}
class Calculator {
private strategy: Strategy
setStrategy(strategy: Strategy) {
this.strategy = strategy
}
calculate(left: number, right: number) {
return this.strategy.calculate(left, right)
}
}
const calculator = new Calculator()
calculator.setStrategy(new AddStrategy())
console.log(calculator.calculate(3, 4)) // 7
calculator.setStrategy(new SubtractStrategy())
console.log(calculator.calculate(3, 4)) // -1