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
- Dependency Injection: Type-safe IoC containers
- Factory Patterns: Abstract factories and builders
- Decorators: Method and class decorators
- Advanced Generics: Conditional and mapped types
- Repository Pattern: Type-safe data access
1. Dependency Injection Pattern
Implement a type-safe dependency injection container.
IoC Container Implementation
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
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
import 'reflect-metadata';
// 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
// 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
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
-
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 }
-
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
Pattern | Use Case | Benefits |
---|---|---|
DI Container | Service management | Loose coupling |
Factory | Object creation | Encapsulation |
Repository | Data access | Type safety |
Decorator | Cross-cutting concerns | Code reuse |
Generic Types | Type transformations | Type 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.