Advanced TypeScript Design Patterns for Enterprise Applications


Design patterns are essential tools for building maintainable and scalable applications. TypeScript's strong type system and object-oriented features make it perfect for implementing these patterns in a type-safe way.

In this comprehensive guide, we'll explore advanced TypeScript design patterns and architectural approaches for enterprise applications.


Key Topics

  1. Creational Patterns: Factory, Singleton, Builder
  2. Structural Patterns: Decorator, Adapter, Facade
  3. Behavioral Patterns: Observer, Strategy, Command
  4. Architectural Patterns: Repository, Unit of Work
  5. Advanced TypeScript Features: Generics, Decorators

1. Creational Patterns

Patterns for object creation and instantiation.

Factory Pattern

// Product interface
interface IProduct {
  name: string;
  price: number;
  getInfo(): string;
}

// Concrete products
class PhysicalProduct implements IProduct {
  constructor(
    public name: string,
    public price: number,
    private weight: number
  ) {}

  getInfo(): string {
    return `${this.name} - $${this.price} (${this.weight}kg)`;
  }
}

class DigitalProduct implements IProduct {
  constructor(
    public name: string,
    public price: number,
    private downloadSize: number
  ) {}

  getInfo(): string {
    return `${this.name} - $${this.price} (${this.downloadSize}MB)`;
  }
}

// Factory class
class ProductFactory {
  static createProduct(type: 'physical' | 'digital', data: {
    name: string;
    price: number;
    weight?: number;
    downloadSize?: number;
  }): IProduct {
    switch (type) {
      case 'physical':
        return new PhysicalProduct(data.name, data.price, data.weight!);
      case 'digital':
        return new DigitalProduct(data.name, data.price, data.downloadSize!);
      default:
        throw new Error('Invalid product type');
    }
  }
}

// Usage
const book = ProductFactory.createProduct('physical', {
  name: 'TypeScript Design Patterns',
  price: 29.99,
  weight: 0.5
});

const ebook = ProductFactory.createProduct('digital', {
  name: 'TypeScript Design Patterns - PDF',
  price: 19.99,
  downloadSize: 15
});

Singleton Pattern with Dependency Injection

// Singleton service
class DatabaseConnection {
  private static instance: DatabaseConnection;
  private constructor(private connectionString: string) {}

  static getInstance(connectionString: string): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(connectionString);
    }
    return DatabaseConnection.instance;
  }

  query(sql: string): Promise<any> {
    // Implementation
    return Promise.resolve([]);
  }
}

// Dependency injection container
class Container {
  private static services: Map<string, any> = new Map();

  static register<T>(token: string, instance: T): void {
    Container.services.set(token, instance);
  }

  static resolve<T>(token: string): T {
    const service = Container.services.get(token);
    if (!service) {
      throw new Error(`Service ${token} not found`);
    }
    return service;
  }
}

// Register services
Container.register('db', DatabaseConnection.getInstance('connection_string'));

// Usage with dependency injection
class UserService {
  private db: DatabaseConnection;

  constructor() {
    this.db = Container.resolve('db');
  }

  async getUsers(): Promise<any[]> {
    return this.db.query('SELECT * FROM users');
  }
}

2. Structural Patterns

Patterns for composing objects and classes.

Decorator Pattern

// Method decorator
function log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;

  descriptor.value = async function(...args: any[]) {
    console.log(`Calling ${propertyKey} with args:`, args);
    const start = performance.now();
    
    try {
      const result = await original.apply(this, args);
      const end = performance.now();
      console.log(`${propertyKey} completed in ${end - start}ms`);
      return result;
    } catch (error) {
      console.error(`${propertyKey} failed:`, error);
      throw error;
    }
  };

  return descriptor;
}

// Class decorator
function injectable() {
  return function(constructor: Function) {
    // Register in DI container
    Container.register(constructor.name, new constructor());
  };
}

// Property decorator
function required(target: any, propertyKey: string) {
  let value: any;
  
  const getter = function() {
    if (value === undefined) {
      throw new Error(`${propertyKey} is required`);
    }
    return value;
  };
  
  const setter = function(newVal: any) {
    value = newVal;
  };
  
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

// Usage
@injectable()
class UserController {
  @required
  private userService: UserService;

  @log
  async getUsers(): Promise<any[]> {
    return this.userService.getUsers();
  }
}

Adapter Pattern

// Third-party payment service
interface ILegacyPayment {
  processPayment(amount: number): Promise<boolean>;
}

class LegacyPaymentService implements ILegacyPayment {
  async processPayment(amount: number): Promise<boolean> {
    // Legacy implementation
    return true;
  }
}

// Modern payment interface
interface IModernPayment {
  pay(amount: number, currency: string): Promise<{
    success: boolean;
    transactionId: string;
  }>;
}

// Adapter
class PaymentAdapter implements IModernPayment {
  constructor(private legacyService: ILegacyPayment) {}

  async pay(amount: number, currency: string): Promise<{
    success: boolean;
    transactionId: string;
  }> {
    const success = await this.legacyService.processPayment(amount);
    return {
      success,
      transactionId: Date.now().toString()
    };
  }
}

// Usage
const legacyService = new LegacyPaymentService();
const modernPayment = new PaymentAdapter(legacyService);

3. Behavioral Patterns

Patterns for communication between objects.

Observer Pattern

interface IObserver<T> {
  update(data: T): void;
}

class Observable<T> {
  private observers: IObserver<T>[] = [];

  subscribe(observer: IObserver<T>): void {
    this.observers.push(observer);
  }

  unsubscribe(observer: IObserver<T>): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(data: T): void {
    this.observers.forEach(observer => observer.update(data));
  }
}

// Example implementation
interface IStockUpdate {
  symbol: string;
  price: number;
}

class StockMarket extends Observable<IStockUpdate> {
  private prices: Map<string, number> = new Map();

  updateStock(symbol: string, price: number): void {
    this.prices.set(symbol, price);
    this.notify({ symbol, price });
  }
}

class StockTrader implements IObserver<IStockUpdate> {
  constructor(private name: string) {}

  update(data: IStockUpdate): void {
    console.log(
      `${this.name} received update: ${data.symbol} = $${data.price}`
    );
  }
}

// Usage
const market = new StockMarket();
const trader1 = new StockTrader('Trader 1');
const trader2 = new StockTrader('Trader 2');

market.subscribe(trader1);
market.subscribe(trader2);
market.updateStock('AAPL', 150.50);

Strategy Pattern

interface IDiscountStrategy {
  calculate(amount: number): number;
}

class PercentageDiscount implements IDiscountStrategy {
  constructor(private percentage: number) {}

  calculate(amount: number): number {
    return amount * (1 - this.percentage / 100);
  }
}

class FixedDiscount implements IDiscountStrategy {
  constructor(private amount: number) {}

  calculate(amount: number): number {
    return Math.max(0, amount - this.amount);
  }
}

class ShoppingCart {
  private items: { name: string; price: number }[] = [];
  private discountStrategy: IDiscountStrategy;

  setDiscountStrategy(strategy: IDiscountStrategy): void {
    this.discountStrategy = strategy;
  }

  addItem(name: string, price: number): void {
    this.items.push({ name, price });
  }

  getTotal(): number {
    const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
    return this.discountStrategy
      ? this.discountStrategy.calculate(subtotal)
      : subtotal;
  }
}

// Usage
const cart = new ShoppingCart();
cart.addItem('Item 1', 100);
cart.addItem('Item 2', 50);

// Apply 20% discount
cart.setDiscountStrategy(new PercentageDiscount(20));
console.log(cart.getTotal());  // 120

// Apply $30 fixed discount
cart.setDiscountStrategy(new FixedDiscount(30));
console.log(cart.getTotal());  // 120

4. Architectural Patterns

Patterns for organizing application architecture.

Repository Pattern

// Entity interface
interface IEntity {
  id: number;
}

// Generic repository interface
interface IRepository<T extends IEntity> {
  getById(id: number): Promise<T | null>;
  getAll(): Promise<T[]>;
  create(entity: Omit<T, 'id'>): Promise<T>;
  update(id: number, entity: Partial<T>): Promise<T>;
  delete(id: number): Promise<boolean>;
}

// Generic base repository implementation
abstract class BaseRepository<T extends IEntity> implements IRepository<T> {
  constructor(protected tableName: string) {}

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

  async getAll(): Promise<T[]> {
    const db = Container.resolve<DatabaseConnection>('db');
    return db.query(`SELECT * FROM ${this.tableName}`);
  }

  async create(entity: Omit<T, 'id'>): Promise<T> {
    const db = Container.resolve<DatabaseConnection>('db');
    const result = await db.query(
      `INSERT INTO ${this.tableName} SET ?`,
      [entity]
    );
    return { ...entity, id: result.insertId } as T;
  }

  async update(id: number, entity: Partial<T>): Promise<T> {
    const db = Container.resolve<DatabaseConnection>('db');
    await db.query(
      `UPDATE ${this.tableName} SET ? WHERE id = ?`,
      [entity, id]
    );
    return this.getById(id) as Promise<T>;
  }

  async delete(id: number): Promise<boolean> {
    const db = Container.resolve<DatabaseConnection>('db');
    const result = await db.query(
      `DELETE FROM ${this.tableName} WHERE id = ?`,
      [id]
    );
    return result.affectedRows > 0;
  }
}

// Example implementation
interface IUser extends IEntity {
  id: number;
  name: string;
  email: string;
}

class UserRepository extends BaseRepository<IUser> {
  constructor() {
    super('users');
  }

  // Additional user-specific methods
  async findByEmail(email: string): Promise<IUser | null> {
    const db = Container.resolve<DatabaseConnection>('db');
    const result = await db.query(
      `SELECT * FROM ${this.tableName} WHERE email = ?`,
      [email]
    );
    return result[0] || null;
  }
}

5. Advanced TypeScript Features

Leverage TypeScript's type system for better patterns.

Advanced Generics

// Type-safe event emitter
type EventMap = {
  'user:created': { id: number; name: string };
  'user:updated': { id: number; changes: Partial<IUser> };
  'user:deleted': { id: number };
}

class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: Partial<{
    [K in keyof T]: ((data: T[K]) => void)[];
  }> = {};

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    if (!this.listeners[event]) return;
    this.listeners[event]!.forEach(listener => listener(data));
  }
}

// Usage
const emitter = new TypedEventEmitter<EventMap>();

emitter.on('user:created', data => {
  console.log(`User created: ${data.name}`);
});

emitter.emit('user:created', {
  id: 1,
  name: 'John Doe'
});

Utility Types

// Deep partial type
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? DeepPartial<T[P]>
    : T[P];
};

// Immutable type
type Immutable<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? Immutable<T[P]>
    : T[P];
};

// Function type utilities
type AsyncFunction<T extends (...args: any[]) => any> = (
  ...args: Parameters<T>
) => Promise<ReturnType<T>>;

type Memoized<T extends (...args: any[]) => any> = {
  (...args: Parameters<T>): ReturnType<T>;
  clear(): void;
};

// Implementation example
function memoize<T extends (...args: any[]) => any>(
  fn: T
): Memoized<T> {
  const cache = new Map<string, ReturnType<T>>();

  const memoized = (...args: Parameters<T>): ReturnType<T> => {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key)!;
    }
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };

  memoized.clear = () => cache.clear();

  return memoized;
}

// Usage
interface IConfig {
  api: {
    url: string;
    key: string;
  };
  cache: {
    ttl: number;
  };
}

const updateConfig = (config: DeepPartial<IConfig>): void => {
  // Implementation
};

const getConfig = memoize(() => ({
  api: {
    url: 'https://api.example.com',
    key: 'secret'
  },
  cache: {
    ttl: 3600
  }
}));

Best Practices

  1. Pattern Selection

    • Choose patterns based on requirements
    • Consider maintainability
    • Avoid over-engineering
    • Document pattern usage
  2. Type Safety

    • Use strict TypeScript configuration
    • Leverage type inference
    • Create custom type guards
    • Use utility types
  3. Code Organization

    • Follow SOLID principles
    • Use dependency injection
    • Implement proper error handling
    • Write unit tests
  4. Performance

    • Consider memory usage
    • Optimize for runtime
    • Use lazy loading
    • Implement caching

Conclusion

TypeScript design patterns provide powerful tools for building maintainable and scalable applications. By understanding and applying these patterns, you can:

  • Write more maintainable code
  • Improve code reusability
  • Ensure type safety
  • Create flexible architectures

Remember to choose patterns that fit your specific needs and avoid over-complicating your codebase. Focus on writing clean, readable code that follows TypeScript best practices.