# Typescript Design Patterns

> When to activate: Repository pattern, Factory, Builder, Observer, Strategy, dependency injection, inversion of control in TypeScript

- Skill: `mattakushi432/typescript-design-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/typescript-design-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/typescript-design-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/typescript-design-patterns

---


# TypeScript Design Patterns

## Repository Pattern
```ts
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
```ts
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
```ts
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
```ts
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
```ts
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)
```ts
// 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)
  }
}
```

