TypeScript Design Patterns
Repository Pattern
interface Repository<T, ID = string> {
findById(id: ID): Promise<T | null>
findAll(filter?: Partial<T>): Promise<T[]>
create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T>
update(id: ID, data: Partial<T>): Promise<T>
delete(id: ID): Promise<void>
}
class PrismaUserRepository implements Repository<User> {
async findById(id: string) {
return prisma.user.findUnique({ where: { id } })
}
async findAll(filter?: Partial<User>) {
return prisma.user.findMany({ where: filter })
}
async create(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>) {
return prisma.user.create({ data })
}
async update(id: string, data: Partial<User>) {
return prisma.user.update({ where: { id }, data })
}
async delete(id: string) {
await prisma.user.delete({ where: { id } })
}
}
// Easy to swap for testing
class InMemoryUserRepository implements Repository<User> {
private store = new Map<string, User>()
async findById(id: string) { return this.store.get(id) ?? null }
async findAll() { return [...this.store.values()] }
async create(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>) {
const user = { id: crypto.randomUUID(), createdAt: new Date(), updatedAt: new Date(), ...data }
this.store.set(user.id, user)
return user
}
async update(id: string, data: Partial<User>) {
const user = { ...this.store.get(id)!, ...data, updatedAt: new Date() }
this.store.set(id, user)
return user
}
async delete(id: string) { this.store.delete(id) }
}
Factory Pattern
interface Notification { send(to: string, message: string): Promise<void> }
class EmailNotification implements Notification {
async send(to: string, message: string) {
await emailClient.send({ to, body: message })
}
}
class SlackNotification implements Notification {
async send(to: string, message: string) {
await slack.postMessage({ channel: to, text: message })
}
}
class SMSNotification implements Notification {
async send(to: string, message: string) {
await twilio.messages.create({ to, body: message })
}
}
type Channel = 'email' | 'slack' | 'sms'
function createNotification(channel: Channel): Notification {
switch (channel) {
case 'email': return new EmailNotification()
case 'slack': return new SlackNotification()
case 'sms': return new SMSNotification()
}
}
Builder Pattern
class QueryBuilder<T> {
private _table = ''
private _conditions: string[] = []
private _limit?: number
private _offset?: number
private _orderBy?: string
table(name: string) { this._table = name; return this }
where(cond: string) { this._conditions.push(cond); return this }
limit(n: number) { this._limit = n; return this }
offset(n: number) { this._offset = n; return this }
orderBy(col: string) { this._orderBy = col; return this }
build(): string {
let query = `SELECT * FROM ${this._table}`
if (this._conditions.length) query += ` WHERE ${this._conditions.join(' AND ')}`
if (this._orderBy) query += ` ORDER BY ${this._orderBy}`
if (this._limit) query += ` LIMIT ${this._limit}`
if (this._offset) query += ` OFFSET ${this._offset}`
return query
}
}
// Usage
const sql = new QueryBuilder()
.table('users')
.where('active = true')
.where('role = 'admin'')
.orderBy('created_at DESC')
.limit(20)
.offset(40)
.build()
Observer / Event Bus
type Handler<T> = (event: T) => void | Promise<void>
class EventBus<Events extends Record<string, unknown>> {
private handlers = new Map<keyof Events, Set<Handler<any>>>()
on<K extends keyof Events>(event: K, handler: Handler<Events[K]>) {
if (!this.handlers.has(event)) this.handlers.set(event, new Set())
this.handlers.get(event)!.add(handler)
return () => this.off(event, handler)
}
off<K extends keyof Events>(event: K, handler: Handler<Events[K]>) {
this.handlers.get(event)?.delete(handler)
}
async emit<K extends keyof Events>(event: K, data: Events[K]) {
const handlers = this.handlers.get(event) ?? []
await Promise.all([...handlers].map(h => h(data)))
}
}
// Usage
interface AppEvents {
'user:created': { userId: string; email: string }
'order:placed': { orderId: string; total: number }
}
const bus = new EventBus<AppEvents>()
const unsubscribe = bus.on('user:created', async ({ userId }) => {
await sendWelcomeEmail(userId)
})
await bus.emit('user:created', { userId: '123', email: 'alice@example.com' })
Strategy Pattern
interface SortStrategy<T> {
sort(items: T[]): T[]
}
class QuickSort<T> implements SortStrategy<T> {
constructor(private compareFn: (a: T, b: T) => number) {}
sort(items: T[]) { return [...items].sort(this.compareFn) }
}
class Sorter<T> {
constructor(private strategy: SortStrategy<T>) {}
setStrategy(s: SortStrategy<T>) { this.strategy = s }
sort(items: T[]) { return this.strategy.sort(items) }
}
const sorter = new Sorter(new QuickSort<User>((a, b) => a.name.localeCompare(b.name)))
const sorted = sorter.sort(users)
Dependency Injection (manual)
// Container
class Container {
private bindings = new Map<string, () => unknown>()
bind<T>(token: string, factory: () => T): void {
this.bindings.set(token, factory)
}
resolve<T>(token: string): T {
const factory = this.bindings.get(token)
if (!factory) throw new Error(`No binding for ${token}`)
return factory() as T
}
}
// Wiring
const container = new Container()
container.bind('UserRepository', () => new PrismaUserRepository())
container.bind('UserService', () => new UserService(container.resolve('UserRepository')))
// Service with injected dependency
class UserService {
constructor(private userRepo: Repository<User>) {}
async getActiveUsers() {
return this.userRepo.findAll({ active: true } as any)
}
}