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

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

  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.