🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

TypeScript Design Patterns 2026: Singleton, Factory, Observer and Repository

⏱️7 min read  ·  1,358 words

TypeScript design patterns in 2026 leverage the full power of the type system to create self-documenting, safe, and extensible code. This guide covers the most valuable patterns for real-world TypeScript applications, from basic creational patterns to advanced functional techniques.

Singleton Pattern

class DatabasePool {
  private static instance: DatabasePool | null = null;
  private connections: Connection[] = [];

  private constructor(private readonly config: DBConfig) {
    // Private constructor prevents direct instantiation
  }

  static getInstance(config?: DBConfig): DatabasePool {
    if (!DatabasePool.instance) {
      if (!config) throw new Error("Config required for first initialization");
      DatabasePool.instance = new DatabasePool(config);
    }
    return DatabasePool.instance;
  }

  async query<T>(sql: string, params: unknown[]): Promise<T[]> {
    const conn = await this.acquire();
    try {
      return await conn.execute<T>(sql, params);
    } finally {
      this.release(conn);
    }
  }

  private async acquire(): Promise<Connection> {
    // Get available connection from pool
    return this.connections.pop() ?? await createConnection(this.config);
  }

  private release(conn: Connection): void {
    this.connections.push(conn);
  }
}

// Usage
const db = DatabasePool.getInstance({ host: "localhost", port: 5432 });
const users = await db.query<User>("SELECT * FROM users WHERE id = $1", [1]);

Builder Pattern

interface QueryConfig {
  table: string;
  conditions: string[];
  columns: string[];
  limit?: number;
  offset?: number;
  orderBy?: { column: string; direction: "ASC" | "DESC" };
}

class QueryBuilder {
  private config: QueryConfig;

  constructor(table: string) {
    this.config = { table, conditions: [], columns: [] };
  }

  select(...columns: string[]): this {
    this.config.columns.push(...columns);
    return this;
  }

  where(condition: string): this {
    this.config.conditions.push(condition);
    return this;
  }

  orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): this {
    this.config.orderBy = { column, direction };
    return this;
  }

  limit(n: number): this {
    this.config.limit = n;
    return this;
  }

  offset(n: number): this {
    this.config.offset = n;
    return this;
  }

  build(): string {
    const cols = this.config.columns.length ? this.config.columns.join(", ") : "*";
    let query = `SELECT ${cols} FROM ${this.config.table}`;
    if (this.config.conditions.length) {
      query += ` WHERE ${this.config.conditions.join(" AND ")}`;
    }
    if (this.config.orderBy) {
      query += ` ORDER BY ${this.config.orderBy.column} ${this.config.orderBy.direction}`;
    }
    if (this.config.limit) query += ` LIMIT ${this.config.limit}`;
    if (this.config.offset) query += ` OFFSET ${this.config.offset}`;
    return query;
  }
}

const query = new QueryBuilder("users")
  .select("id", "name", "email")
  .where("active = true")
  .where("role = 'admin'")
  .orderBy("created_at", "DESC")
  .limit(10)
  .build();

Factory Pattern

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

class EmailSender implements NotificationSender {
  async send(to: string, message: string): Promise<void> {
    await sendEmail({ to, subject: "Notification", body: message });
  }
}

class SMSSender implements NotificationSender {
  async send(to: string, message: string): Promise<void> {
    await sendSMS({ to, message: message.slice(0, 160) });
  }
}

class PushSender implements NotificationSender {
  async send(to: string, message: string): Promise<void> {
    await sendPushNotification({ userId: to, body: message });
  }
}

type NotificationType = "email" | "sms" | "push";

function createNotificationSender(type: NotificationType): NotificationSender {
  const senders: Record<NotificationType, () => NotificationSender> = {
    email: () => new EmailSender(),
    sms: () => new SMSSender(),
    push: () => new PushSender(),
  };
  const factory = senders[type];
  if (!factory) throw new Error(`Unknown notification type: ${type}`);
  return factory();
}

// Usage
const sender = createNotificationSender("email");
await sender.send("alice@example.com", "Your order has shipped!");

Observer / Event Emitter Pattern

type EventMap = {
  "user:created": { id: number; email: string };
  "order:placed": { orderId: string; total: number; userId: number };
  "payment:failed": { orderId: string; error: string };
};

type EventHandler<T> = (data: T) => void | Promise<void>;

class TypedEventEmitter<Events extends Record<string, unknown>> {
  private handlers: Partial<{
    [K in keyof Events]: EventHandler<Events[K]>[];
  }> = {};

  on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): this {
    if (!this.handlers[event]) {
      (this.handlers[event] as EventHandler<Events[K]>[]) = [];
    }
    (this.handlers[event] as EventHandler<Events[K]>[]).push(handler);
    return this;
  }

  off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): this {
    const handlers = this.handlers[event] as EventHandler<Events[K]>[] | undefined;
    if (handlers) {
      (this.handlers[event] as EventHandler<Events[K]>[]) = handlers.filter(h => h !== handler);
    }
    return this;
  }

  async emit<K extends keyof Events>(event: K, data: Events[K]): Promise<void> {
    const handlers = (this.handlers[event] as EventHandler<Events[K]>[] | undefined) ?? [];
    await Promise.all(handlers.map(h => h(data)));
  }
}

const emitter = new TypedEventEmitter<EventMap>();

emitter.on("user:created", async ({ email }) => {
  await sendWelcomeEmail(email);
});

emitter.on("order:placed", async ({ orderId, userId }) => {
  await sendOrderConfirmation(userId, orderId);
});

// TypeScript enforces correct event data types
await emitter.emit("user:created", { id: 1, email: "alice@example.com" });
// await emitter.emit("user:created", { id: 1 });  // TypeScript error!

Strategy Pattern

interface PaymentStrategy {
  charge(amount: number, currency: string): Promise<PaymentResult>;
  refund(transactionId: string, amount: number): Promise<void>;
}

class StripeStrategy implements PaymentStrategy {
  constructor(private apiKey: string) {}

  async charge(amount: number, currency: string): Promise<PaymentResult> {
    const charge = await stripe.charges.create({ amount, currency });
    return { transactionId: charge.id, status: "success" };
  }

  async refund(transactionId: string, amount: number): Promise<void> {
    await stripe.refunds.create({ charge: transactionId, amount });
  }
}

class PayPalStrategy implements PaymentStrategy {
  async charge(amount: number, currency: string): Promise<PaymentResult> {
    const order = await paypal.orders.create({ amount, currency });
    return { transactionId: order.id, status: "success" };
  }

  async refund(transactionId: string, amount: number): Promise<void> {
    await paypal.captures.refund(transactionId, { amount });
  }
}

class PaymentProcessor {
  constructor(private strategy: PaymentStrategy) {}

  setStrategy(strategy: PaymentStrategy): void {
    this.strategy = strategy;
  }

  async processPayment(amount: number, currency: string): Promise<PaymentResult> {
    return this.strategy.charge(amount, currency);
  }
}

// Switch strategy at runtime
const processor = new PaymentProcessor(new StripeStrategy(process.env.STRIPE_KEY!));
const result = await processor.processPayment(9999, "AUD");

// Switch to PayPal for specific users
processor.setStrategy(new PayPalStrategy());

Repository Pattern with Generics

interface Repository<T, ID = number> {
  findById(id: ID): Promise<T | null>;
  findAll(options?: FindOptions): Promise<T[]>;
  create(data: Omit<T, "id" | "createdAt" | "updatedAt">): Promise<T>;
  update(id: ID, data: Partial<T>): Promise<T>;
  delete(id: ID): Promise<void>;
  count(where?: Partial<T>): Promise<number>;
}

interface FindOptions {
  limit?: number;
  offset?: number;
  orderBy?: string;
  where?: Record<string, unknown>;
}

// Base implementation
abstract class BaseRepository<T extends { id: number }> implements Repository<T> {
  constructor(
    protected readonly db: Database,
    protected readonly tableName: string
  ) {}

  async findById(id: number): Promise<T | null> {
    const result = await this.db.query<T>(
      `SELECT * FROM ${this.tableName} WHERE id = $1`, [id]
    );
    return result[0] ?? null;
  }

  async create(data: Omit<T, "id" | "createdAt" | "updatedAt">): Promise<T> {
    const keys = Object.keys(data);
    const values = Object.values(data);
    const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
    const result = await this.db.query<T>(
      `INSERT INTO ${this.tableName} (${keys.join(", ")}) VALUES (${placeholders}) RETURNING *`,
      values
    );
    return result[0];
  }

  // Other methods...
  async findAll(_opts?: FindOptions): Promise<T[]> { return []; }
  async update(_id: number, _data: Partial<T>): Promise<T> { return {} as T; }
  async delete(_id: number): Promise<void> {}
  async count(_where?: Partial<T>): Promise<number> { return 0; }
}

// Specific implementation
class UserRepository extends BaseRepository<User> {
  constructor(db: Database) { super(db, "users"); }

  async findByEmail(email: string): Promise<User | null> {
    const result = await this.db.query<User>(
      "SELECT * FROM users WHERE email = $1", [email]
    );
    return result[0] ?? null;
  }

  async findActive(): Promise<User[]> {
    return this.db.query<User>("SELECT * FROM users WHERE active = true");
  }
}

TypeScript design patterns in 2026 are powerful precisely because the type system enforces correct usage at compile time — the Observer pattern above catches wrong event data before runtime, the Repository pattern ensures type-safe database access. Apply patterns to solve real problems, not as pattern-for-its-own-sake over-engineering.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা