Skip to content

Advanced TypeScript Design Patterns for Enterprise Applications

TypeScript has become the go-to language for building large-scale enterprise applications, thanks to its robust type system and modern JavaScript features. However, leveraging TypeScript effectively requires understanding and implementing advanced design patterns that promote code reusability, maintainability, and type safety.

In this guide, we’ll explore advanced TypeScript design patterns and architectural approaches that can help you build more robust and maintainable applications.


Key Design Patterns

  1. Dependency Injection: Type-safe IoC containers
  2. Factory Patterns: Abstract factories and builders
  3. Decorators: Method and class decorators
  4. Advanced Generics: Conditional and mapped types
  5. Repository Pattern: Type-safe data access

1. Dependency Injection Pattern

Implement a type-safe dependency injection container.

IoC Container Implementation

// @filename: index.ts
type Constructor<T = any> = new (...args: any[]) => T

class Container {
  private services: Map<string, any> = new Map()

  register<T>(
    token: string,
    constructor: Constructor<T>,
    dependencies: string[] = []
  ): void {
    const inject = (...args: any[]) => new constructor(...args)
    this.services.set(token, {
      constructor,
      dependencies,
      inject,
    })
  }

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

    const dependencies = service.dependencies.map((dep: string) =>
      this.resolve(dep)
    )
    return service.inject(...dependencies)
  }
}

// Usage example
interface ILogger {
  log(message: string): void
}

class Logger implements ILogger {
  log(message: string): void {
    console.log(message)
  }
}

class UserService {
  constructor(private logger: ILogger) {}

  createUser(name: string): void {
    this.logger.log(`Creating user: ${name}`)
  }
}

const container = new Container()
container.register<ILogger>('logger', Logger)
container.register<UserService>('userService', UserService, ['logger'])

const userService = container.resolve<UserService>('userService')

2. Factory Pattern with Generic Constraints

Implement type-safe factory patterns using generic constraints.

Generic Factory Implementation

// @filename: index.ts
interface Entity {
  id: string
  createdAt: Date
}

interface EntityFactory<T extends Entity> {
  create(props: Omit<T, keyof Entity>): T
}

class GenericEntityFactory<T extends Entity> implements EntityFactory<T> {
  constructor(private type: new () => T) {}

  create(props: Omit<T, keyof Entity>): T {
    const entity = new this.type()
    Object.assign(entity, props, {
      id: crypto.randomUUID(),
      createdAt: new Date(),
    })
    return entity
  }
}

// Usage example
interface User extends Entity {
  name: string
  email: string
}

class UserImpl implements User {
  id!: string
  createdAt!: Date
  name!: string
  email!: string
}

const userFactory = new GenericEntityFactory<User>(UserImpl)
const user = userFactory.create({
  name: 'John Doe',
  email: 'john@example.com',
})

3. Advanced Decorator Pattern

Implement method decorators with metadata reflection.

Method Decorator Implementation

// @filename: index.ts


// Validation decorator
function validate<T>(schema: { [K in keyof T]?: (val: T[K]) => boolean }) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value

    descriptor.value = function (...args: any[]) {
      const paramTypes = Reflect.getMetadata(
        'design:paramtypes',
        target,
        propertyKey
      )

      args.forEach((arg, index) => {
        const paramType = paramTypes[index]
        const validator = schema[arg.constructor.name]
        if (validator && !validator(arg)) {
          throw new Error(`Validation failed for parameter ${index}`)
        }
      })

      return originalMethod.apply(this, args)
    }
  }
}

// Usage example
class UserValidator {
  @validate<User>({
    name: (name) => typeof name === 'string' && name.length > 0,
    email: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
  })
  createUser(user: User): void {
    // Implementation
  }
}

4. Advanced Generic Types

Implement complex type transformations using conditional and mapped types.

Advanced Type Utilities

// @filename: index.ts
// Deep Partial type
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

// Readonly recursive type
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

// Pick deep type
type DeepPick<T, K extends keyof T> = {
  [P in K]: T[P] extends object ? DeepPick<T[P], keyof T[P]> : T[P]
}

// Usage example
interface DeepNestedType {
  id: string
  user: {
    name: string
    settings: {
      theme: string
      notifications: boolean
    }
  }
}

type PartialNested = DeepPartial<DeepNestedType>
type ReadonlyNested = DeepReadonly<DeepNestedType>
type PickedNested = DeepPick<DeepNestedType, 'user'>

5. Repository Pattern with Type Safety

Implement a type-safe repository pattern with generics.

Generic Repository Implementation

// @filename: index.ts
interface Repository<T extends Entity> {
  find(id: string): Promise<T | null>
  findAll(): Promise<T[]>
  create(entity: Omit<T, keyof Entity>): Promise<T>
  update(id: string, entity: Partial<T>): Promise<T>
  delete(id: string): Promise<void>
}

class GenericRepository<T extends Entity> implements Repository<T> {
  constructor(
    private collection: string,
    private factory: EntityFactory<T>
  ) {}

  async find(id: string): Promise<T | null> {
    // Implementation
    return null
  }

  async findAll(): Promise<T[]> {
    // Implementation
    return []
  }

  async create(props: Omit<T, keyof Entity>): Promise<T> {
    const entity = this.factory.create(props)
    // Save to database
    return entity
  }

  async update(id: string, props: Partial<T>): Promise<T> {
    // Implementation
    return {} as T
  }

  async delete(id: string): Promise<void> {
    // Implementation
  }
}

// Usage example
class UserRepository extends GenericRepository<User> {
  constructor() {
    super('users', new GenericEntityFactory<User>(UserImpl))
  }

  // Add user-specific methods
  async findByEmail(email: string): Promise<User | null> {
    // Implementation
    return null
  }
}

Advanced Pattern Combinations

  1. Service Layer Pattern

    interface Service<T extends Entity> {
      create(props: Omit<T, keyof Entity>): Promise<T>
      update(id: string, props: Partial<T>): Promise<T>
      delete(id: string): Promise<void>
    }
    
    class GenericService<T extends Entity> implements Service<T> {
      constructor(
        private repository: Repository<T>,
        private validator: Validator<T>
      ) {}
    
      async create(props: Omit<T, keyof Entity>): Promise<T> {
        await this.validator.validate(props)
        return this.repository.create(props)
      }
    
      // Other methods
    }
  2. Unit of Work Pattern

    class UnitOfWork {
      private transactions: Array<() => Promise<void>> = []
    
      register(operation: () => Promise<void>): void {
        this.transactions.push(operation)
      }
    
      async commit(): Promise<void> {
        for (const transaction of this.transactions) {
          await transaction()
        }
        this.transactions = []
      }
    
      async rollback(): Promise<void> {
        this.transactions = []
      }
    }

Best Practices Summary

PatternUse CaseBenefits
DI ContainerService managementLoose coupling
FactoryObject creationEncapsulation
RepositoryData accessType safety
DecoratorCross-cutting concernsCode reuse
Generic TypesType transformationsType safety

Conclusion

Advanced TypeScript design patterns provide powerful tools for building maintainable and type-safe enterprise applications. By leveraging these patterns effectively, you can create more robust and scalable codebases that are easier to maintain and extend.

Remember that patterns should be applied judiciously based on your specific needs. Start with simpler patterns and gradually introduce more complex ones as your application’s requirements grow.

TypeScript JavaScript Type Safety Advanced Scalability
Share:

Continue Reading

TypeScript Namespaces and Modules: Organizing Large Codebases

As TypeScript projects grow, organizing code in a scalable and maintainable way becomes essential. Namespaces and modules are two techniques that help you structure code, manage dependencies, and prevent naming conflicts in large TypeScript codebases. In this guide, we will explore the differences between namespaces and modules, when to use each, and practical examples for organizing code in a TypeScript project.

Read article
TypeScriptJavaScriptType Safety

TypeScript Advanced Types: Mastering Union, Intersection, and Conditional Types

As you progress in TypeScript, understanding advanced types such as union types, intersection types, and conditional types can enhance the flexibility, readability, and type safety of your code. These types allow for precise type definitions, dynamic type combinations, and even conditional logic within types, making them invaluable tools in TypeScript development.

Read article
TypeScriptJavaScriptType Safety

Getting Started with TypeScript: A Beginner\

JavaScript is a flexible, powerful language, but its dynamic typing can sometimes lead to unexpected bugs and runtime errors. TypeScript is a superset of JavaScript that introduces static typing, helping developers catch errors early in the development process and improving code quality. In this guide, we’ll explore the basics of TypeScript, including how to set up a project, use types, and leverage TypeScript’s powerful features to create reliable, scalable applications.

Read article
TypeScriptJavaScriptType Safety

AI-Assisted Content

This article includes AI-assisted content that has been reviewed for accuracy. Always test code snippets before use.