Patterns TypeScript avancés : Architecture moderne et type safety

TypeScript révolutionne l'écosystème JavaScript en apportant un système de types statiques sophistiqué qui améliore drastiquement la robustesse et la maintenabilité du code. Les patterns avancés exploitent pleinement la puissance du compilateur pour créer des architectures type-safe et expressives.

Types conditionnels et manipulation de types avancée Les types conditionnels constituent l'un des m

écanismes les plus puissants de TypeScript, permettant la création de types dynamiques basés sur des conditions logiques. Cette fonctionnalité ouvre la voie à des patterns d'architecture sophistiqués et type-safe.

// Types conditionnels avancés pour l'architecture moderne type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? T[P] extends Function ? T[P] : DeepReadonly<T[P]> : T[P]; }; type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? T[P] extends Function ? T[P] : DeepPartial<T[P]> : T[P]; }; // Extraction de types avec pattern matching avancé type ExtractRouteParams<T extends string> = T extends ${string}:${infer Param}/${infer Rest} ? { [K in Param | keyof ExtractRouteParams<Rest>]: string } : T extends ${string}:${infer Param} ? { [K in Param]: string } : {}; // Exemple d'utilisation avec routes typées type UserRouteParams = ExtractRouteParams<"/users/:id/posts/:postId">; // Résultat: { id: string; postId: string } // Types distributifs pour les unions complexes type NonNullable<T> = T extends null | undefined ? never : T; type FlattenArray<T> = T extends ReadonlyArray<infer U> ? U extends ReadonlyArray<any> ? FlattenArray<U> : U : T; // Pattern de validation de forme avec types conditionnels type ValidateShape<T, Shape> = { [K in keyof Shape]: K extends keyof T ? Shape[K] extends object ? ValidateShape<T[K], Shape[K]> : T[K] extends Shape[K] ? T[K] : never : never; }; // Système de permissions type-safe type Permission = 'read' | 'write' | 'delete' | 'admin'; type Resource = 'user' | 'post' | 'comment' | 'system'; type PermissionMatrix<P extends Permission, R extends Resource> = { [K in R]: { [J in P]?: boolean; }; }; // Implémentation d'un système d'autorisation type-safe class TypeSafeAuthorization< TUser extends { permissions: Permission[] }, TResource extends Resource > { private permissions: PermissionMatrix<Permission, TResource>; constructor(permissions: PermissionMatrix<Permission, TResource>) { this.permissions = permissions; } // Vérification type-safe des permissions hasPermission<P extends Permission>( user: TUser, resource: TResource, permission: P ): boolean { const resourcePermissions = this.permissions[resource]; const hasResourcePermission = resourcePermissions?.[permission] === true; const hasUserPermission = user.permissions.includes(permission); return hasResourcePermission && hasUserPermission; } // Guard type-safe pour les permissions requirePermission<P extends Permission>( user: TUser, resource: TResource, permission: P ): asserts user is TUser & { permissions: (Permission | P)[] } { if (!this.hasPermission(user, resource, permission)) { throw new Error(Permission denied: ${permission} on ${resource}); } } // Filtrage type-safe basé sur les permissions filterByPermissions<T extends { resource: TResource }>( user: TUser, items: T[], requiredPermission: Permission ): T[] { return items.filter(item => this.hasPermission(user, item.resource, requiredPermission) ); } } // Pattern de builder type-safe avec validation interface DatabaseConfig { host: string; port: number; database: string; username: string; password: string; ssl?: boolean; poolSize?: number; timeout?: number; } type RequiredFields<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K]; }; type OptionalFields<T> = { [K in keyof T as T[K] extends undefined ? K : never]?: T[K]; }; class TypeSafeConfigBuilder<T extends Partial<DatabaseConfig>> { private config: T = {} as T; host<S extends string>(host: S): TypeSafeConfigBuilder<T & { host: S }> { return new TypeSafeConfigBuilder<T & { host: S }>().merge({ host } as any); } port<P extends number>(port: P): TypeSafeConfigBuilder<T & { port: P }> { return new TypeSafeConfigBuilder<T & { port: P }>().merge({ port } as any); } database<D extends string>(database: D): TypeSafeConfigBuilder<T & { database: D }> { return new TypeSafeConfigBuilder<T & { database: D }>().merge({ database } as any); } credentials<U extends string, P extends string>( username: U, password: P ): TypeSafeConfigBuilder<T & { username: U; password: P }> { return new TypeSafeConfigBuilder<T & { username: U; password: P }>() .merge({ username, password } as any); } ssl(enabled: boolean): TypeSafeConfigBuilder<T & { ssl: boolean }> { return new TypeSafeConfigBuilder<T & { ssl: boolean }>().merge({ ssl: enabled } as any); } private merge<U>(additional: U): TypeSafeConfigBuilder<T & U> { const newBuilder = new TypeSafeConfigBuilder<T & U>(); newBuilder.config = { ...this.config, ...additional } as T & U; return newBuilder; } // Construction finale seulement si tous les champs requis sont présents build< TRequired = RequiredFields<DatabaseConfig>, TComplete = T & TRequired >(this: TypeSafeConfigBuilder<TComplete>): DatabaseConfig { const requiredFields: (keyof RequiredFields<DatabaseConfig>)[] = [ 'host', 'port', 'database', 'username', 'password' ]; for (const field of requiredFields) { if (!(field in this.config) || this.config[field] === undefined) { throw new Error(Missing required field: ${String(field)}); } } return this.config as DatabaseConfig; } } // Exemple d'utilisation du builder type-safe const dbConfig = new TypeSafeConfigBuilder() .host('localhost') .port(5432) .database('myapp') .credentials('admin', 'secret123') .ssl(true) .build(); // ✅ Compilation réussie // const invalidConfig = new TypeSafeConfigBuilder() // .host('localhost') // .port(5432) // .build(); // ❌ Erreur de compilation : champs manquants

Patterns d'architecture avec Branded Types et Phantom Types Les Branded Types et Phantom Types perme

ttent de créer des types distincts au niveau de TypeScript tout en conservant la même représentation runtime, garantissant la type safety dans des domaines complexes.

// Branded Types pour la sécurité des types métier declare const brand: unique symbol; type Brand<T, TBrand> = T & { [brand]: TBrand }; // Types métier avec validation intégrée type UserId = Brand<string, 'UserId'>; type Email = Brand<string, 'Email'>; type HashedPassword = Brand<string, 'HashedPassword'>; type JWT = Brand<string, 'JWT'>; type Timestamp = Brand<number, 'Timestamp'>; // Système de validation et création type-safe namespace TypeSafeFactories { export function createUserId(value: string): UserId { if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value)) { throw new Error('Invalid UUID format for UserId'); } return value as UserId; } export function createEmail(value: string): Email { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { throw new Error('Invalid email format'); } return value as Email; } export function createHashedPassword(plaintext: string): HashedPassword { // Simulation de hachage (utiliser bcrypt en vrai) const hashed = $2b$10$${btoa(plaintext).slice(0, 22)}; return hashed as HashedPassword; } export function createTimestamp(): Timestamp { return Date.now() as Timestamp; } export function createJWT(payload: object, secret: string): JWT { // Simulation de JWT (utiliser une vraie librairie) const token = btoa(JSON.stringify({ ...payload, exp: Date.now() + 3600000 })); return token as JWT; } } // Entités métier type-safe avec branded types interface User { readonly id: UserId; readonly email: Email; readonly passwordHash: HashedPassword; readonly createdAt: Timestamp; readonly updatedAt: Timestamp; readonly isActive: boolean; } interface Session { readonly id: UserId; readonly userId: UserId; readonly token: JWT; readonly expiresAt: Timestamp; readonly createdAt: Timestamp; } // Repository pattern avec type safety complète interface Repository<TEntity, TId> { findById(id: TId): Promise<TEntity | null>; save(entity: TEntity): Promise<TEntity>; delete(id: TId): Promise<void>; findMany(criteria: Partial<TEntity>): Promise<TEntity[]>; } class TypeSafeUserRepository implements Repository<User, UserId> { private users = new Map<UserId, User>(); async findById(id: UserId): Promise<User | null> { return this.users.get(id) || null; } async save(user: User): Promise<User> { const updatedUser: User = { ...user, updatedAt: TypeSafeFactories.createTimestamp() }; this.users.set(user.id, updatedUser); return updatedUser; } async delete(id: UserId): Promise<void> { this.users.delete(id); } async findMany(criteria: Partial<User>): Promise<User[]> { return Array.from(this.users.values()).filter(user => Object.entries(criteria).every(([key, value]) => user[key as keyof User] === value ) ); } // Méthodes métier type-safe async findByEmail(email: Email): Promise<User | null> { const users = await this.findMany({ email }); return users[0] || null; } async updatePassword(userId: UserId, newPassword: HashedPassword): Promise<User> { const user = await this.findById(userId); if (!user) { throw new Error('User not found'); } return this.save({ ...user, passwordHash: newPassword }); } } // Service Layer avec injection de dépendances type-safe interface UserService { createUser(email: string, password: string): Promise<User>; authenticateUser(email: string, password: string): Promise<Session | null>; getUserById(id: UserId): Promise<User | null>; } class TypeSafeUserService implements UserService { constructor( private userRepository: Repository<User, UserId>, private sessionRepository: Repository<Session, UserId> ) {} async createUser(email: string, password: string): Promise<User> { const validEmail = TypeSafeFactories.createEmail(email); const hashedPassword = TypeSafeFactories.createHashedPassword(password); const userId = TypeSafeFactories.createUserId(crypto.randomUUID()); const timestamp = TypeSafeFactories.createTimestamp(); const newUser: User = { id: userId, email: validEmail, passwordHash: hashedPassword, createdAt: timestamp, updatedAt: timestamp, isActive: true }; return this.userRepository.save(newUser); } async authenticateUser(email: string, password: string): Promise<Session | null> { const validEmail = TypeSafeFactories.createEmail(email); const user = await this.userRepository.findMany({ email: validEmail }); if (user.length === 0 || !user[0].isActive) { return null; } const targetUser = user[0]; const providedHash = TypeSafeFactories.createHashedPassword(password); // Vérification du mot de passe (simulation) if (targetUser.passwordHash !== providedHash) { return null; } // Création de session const sessionId = TypeSafeFactories.createUserId(crypto.randomUUID()); const token = TypeSafeFactories.createJWT({ userId: targetUser.id }, 'secret'); const createdAt = TypeSafeFactories.createTimestamp(); const expiresAt = (createdAt + 3600000) as Timestamp; // 1 heure const session: Session = { id: sessionId, userId: targetUser.id, token, expiresAt, createdAt }; return this.sessionRepository.save(session); } async getUserById(id: UserId): Promise<User | null> { return this.userRepository.findById(id); } } // Phantom Types pour les états métier type Draft = { readonly state: 'draft' }; type Published = { readonly state: 'published' }; type Archived = { readonly state: 'archived' }; type Article<TState = Draft> = { readonly id: string; readonly title: string; readonly content: string; readonly authorId: UserId; readonly createdAt: Timestamp; readonly updatedAt: Timestamp; } & TState; // Machine à états type-safe avec Phantom Types class ArticleStateMachine { // Création d'un brouillon static createDraft( title: string, content: string, authorId: UserId ): Article<Draft> { const timestamp = TypeSafeFactories.createTimestamp(); return { id: crypto.randomUUID(), title, content, authorId, createdAt: timestamp, updatedAt: timestamp, state: 'draft' }; } // Transition : Draft -> Published static publish(article: Article<Draft>): Article<Published> { if (!article.title.trim() || !article.content.trim()) { throw new Error('Cannot publish article without title and content'); } return { ...article, updatedAt: TypeSafeFactories.createTimestamp(), state: 'published' }; } // Transition : Published -> Archived static archive(article: Article<Published>): Article<Archived> { return { ...article, updatedAt: TypeSafeFactories.createTimestamp(), state: 'archived' }; } // Transition : Draft -> Published -> Archived n'est possible qu'avec les bons types static publishAndArchive(draft: Article<Draft>): Article<Archived> { const published = this.publish(draft); return this.archive(published); } // Les transitions invalides sont impossibles au niveau du type // static invalidTransition(draft: Article<Draft>): Article<Archived> { // return this.archive(draft); // ❌ Erreur de compilation // } } // Service de gestion d'articles avec states type-safe class ArticleService { constructor( private draftRepository: Repository<Article<Draft>, string>, private publishedRepository: Repository<Article<Published>, string>, private archivedRepository: Repository<Article<Archived>, string> ) {} async createDraft(title: string, content: string, authorId: UserId): Promise<Article<Draft>> { const draft = ArticleStateMachine.createDraft(title, content, authorId); return this.draftRepository.save(draft); } async publishDraft(draftId: string): Promise<Article<Published>> { const draft = await this.draftRepository.findById(draftId); if (!draft) { throw new Error('Draft not found'); } const published = ArticleStateMachine.publish(draft); // Suppression du brouillon et sauvegarde de l'article publié await this.draftRepository.delete(draftId); return this.publishedRepository.save(published); } async archivePublished(publishedId: string): Promise<Article<Archived>> { const published = await this.publishedRepository.findById(publishedId); if (!published) { throw new Error('Published article not found'); } const archived = ArticleStateMachine.archive(published); // Suppression de l'article publié et sauvegarde de l'archivé await this.publishedRepository.delete(publishedId); return this.archivedRepository.save(archived); } // Recherche type-safe par état async getDraftsByAuthor(authorId: UserId): Promise<Article<Draft>[]> { return this.draftRepository.findMany({ authorId }); } async getPublishedByAuthor(authorId: UserId): Promise<Article<Published>[]> { return this.publishedRepository.findMany({ authorId }); } }

Patterns avancés pour l'API et la sérialisation TypeScript offre des possibilités sophistiquées

pour créer des APIs type-safe avec validation automatique et sérialisation intelligente, garantissant la cohérence entre runtime et types.

// Système de validation et sérialisation type-safe interface TypeValidator<T> { validate(value: unknown): value is T; serialize(value: T): string; deserialize(data: string): T; schema: TypeSchema<T>; } type TypeSchema<T> = { [K in keyof T]: { type: string; required: boolean; validator?: (value: any) => boolean; transformer?: (value: any) => T[K]; }; }; // Factory pour créer des validateurs type-safe class ValidatorFactory { static string(options: { minLength?: number; maxLength?: number; pattern?: RegExp } = {}): TypeValidator<string> { return { validate: (value: unknown): value is string => { if (typeof value !== 'string') return false; if (options.minLength && value.length < options.minLength) return false; if (options.maxLength && value.length > options.maxLength) return false; if (options.pattern && !options.pattern.test(value)) return false; return true; }, serialize: (value: string) => JSON.stringify(value), deserialize: (data: string) => JSON.parse(data), schema: {} as any // Simplifié pour l'exemple }; } static number(options: { min?: number; max?: number; integer?: boolean } = {}): TypeValidator<number> { return { validate: (value: unknown): value is number => { if (typeof value !== 'number' || !Number.isFinite(value)) return false; if (options.min !== undefined && value < options.min) return false; if (options.max !== undefined && value > options.max) return false; if (options.integer && !Number.isInteger(value)) return false; return true; }, serialize: (value: number) => JSON.stringify(value), deserialize: (data: string) => JSON.parse(data), schema: {} as any }; } static array<T>(itemValidator: TypeValidator<T>): TypeValidator<T[]> { return { validate: (value: unknown): value is T[] => { if (!Array.isArray(value)) return false; return value.every(item => itemValidator.validate(item)); }, serialize: (value: T[]) => JSON.stringify(value.map(itemValidator.serialize)), deserialize: (data: string) => { const parsed = JSON.parse(data); return parsed.map((item: string) => itemValidator.deserialize(item)); }, schema: {} as any }; } static object<T extends Record<string, any>>( validators: { [K in keyof T]: TypeValidator<T[K]> } ): TypeValidator<T> { return { validate: (value: unknown): value is T => { if (typeof value !== 'object' || value === null) return false; return Object.entries(validators).every(([key, validator]) => { const fieldValue = (value as any)[key]; return validator.validate(fieldValue); }); }, serialize: (value: T) => { const serialized: Record<string, string> = {}; for (const [key, validator] of Object.entries(validators)) { serialized[key] = (validator as TypeValidator<any>).serialize(value[key]); } return JSON.stringify(serialized); }, deserialize: (data: string) => { const parsed = JSON.parse(data); const result: Partial<T> = {}; for (const [key, validator] of Object.entries(validators)) { result[key as keyof T] = (validator as TypeValidator<any>).deserialize(parsed[key]); } return result as T; }, schema: {} as any }; } } // DTO avec validation automatique interface CreateUserDTO { email: string; password: string; firstName: string; lastName: string; age?: number; } interface UserResponseDTO { id: string; email: string; firstName: string; lastName: string; age?: number; createdAt: string; isActive: boolean; } // Validateurs pour les DTOs const createUserValidator = ValidatorFactory.object<CreateUserDTO>({ email: ValidatorFactory.string({ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }), password: ValidatorFactory.string({ minLength: 8, maxLength: 128 }), firstName: ValidatorFactory.string({ minLength: 1, maxLength: 50 }), lastName: ValidatorFactory.string({ minLength: 1, maxLength: 50 }), age: ValidatorFactory.number({ min: 13, max: 120, integer: true }) }); const userResponseValidator = ValidatorFactory.object<UserResponseDTO>({ id: ValidatorFactory.string(), email: ValidatorFactory.string(), firstName: ValidatorFactory.string(), lastName: ValidatorFactory.string(), age: ValidatorFactory.number(), createdAt: ValidatorFactory.string(), isActive: ValidatorFactory.object({ validate: (v): v is boolean => typeof v === 'boolean' } as any) }); // API Controller avec validation automatique class TypeSafeApiController { constructor(private userService: UserService) {} // Décorateur pour validation automatique des paramètres private validateBody<T>(validator: TypeValidator<T>) { return (target: any, propertyName: string, descriptor: PropertyDescriptor) => { const method = descriptor.value; descriptor.value = async function(request: any, ...args: any[]) { const body = await request.json(); if (!validator.validate(body)) { throw new ValidationError('Invalid request body'); } return method.call(this, request, body, ...args); }; }; } @TypeSafeApiController.prototype.validateBody(createUserValidator) async createUser(request: Request, body: CreateUserDTO): Promise<UserResponseDTO> { try { const user = await this.userService.createUser(body.email, body.password); const response: UserResponseDTO = { id: user.id, email: user.email, firstName: body.firstName, lastName: body.lastName, age: body.age, createdAt: new Date(user.createdAt).toISOString(), isActive: user.isActive }; // Validation de la réponse avant envoi if (!userResponseValidator.validate(response)) { throw new Error('Response validation failed'); } return response; } catch (error) { throw new ApiError('Failed to create user', 500, error); } } // Pattern de transformation type-safe private transform<TInput, TOutput>( input: TInput, transformer: (input: TInput) => TOutput, validator: TypeValidator<TOutput> ): TOutput { const result = transformer(input); if (!validator.validate(result)) { throw new TransformationError('Transformation validation failed'); } return result; } async getUserById(userId: string): Promise<UserResponseDTO> { const user = await this.userService.getUserById( TypeSafeFactories.createUserId(userId) ); if (!user) { throw new NotFoundError('User not found'); } return this.transform( user, (u): UserResponseDTO => ({ id: u.id, email: u.email, firstName: 'John', // Simulation - à récupérer depuis la base lastName: 'Doe', createdAt: new Date(u.createdAt).toISOString(), isActive: u.isActive }), userResponseValidator ); } } // Système de gestion d'erreurs type-safe abstract class TypedError extends Error { abstract readonly type: string; abstract readonly statusCode: number; abstract readonly isOperational: boolean; } class ValidationError extends TypedError { readonly type = 'VALIDATIONERROR'; readonly statusCode = 400; readonly isOperational = true; constructor(message: string, public readonly fields?: string[]) { super(message); this.name = 'ValidationError'; } } class NotFoundError extends TypedError { readonly type = 'NOTFOUND'; readonly statusCode = 404; readonly isOperational = true; constructor(message: string, public readonly resource?: string) { super(message); this.name = 'NotFoundError'; } } class ApiError extends TypedError { readonly type = 'APIERROR'; readonly isOperational = true; constructor( message: string, public readonly statusCode: number, public readonly cause?: Error ) { super(message); this.name = 'ApiError'; } } class TransformationError extends TypedError { readonly type = 'TRANSFORMATIONERROR'; readonly statusCode = 500; readonly isOperational = false; constructor(message: string) { super(message); this.name = 'TransformationError'; } } // Gestionnaire d'erreurs global type-safe class ErrorHandler { static handle(error: unknown): { message: string; statusCode: number; type: string; details?: any; } { if (error instanceof TypedError) { return { message: error.message, statusCode: error.statusCode, type: error.type, details: this.getErrorDetails(error) }; } if (error instanceof Error) { return { message: 'Internal server error', statusCode: 500, type: 'UNKNOWNERROR', details: process.env.NODEENV === 'development' ? error.stack : undefined }; } return { message: 'Unknown error occurred', statusCode: 500, type: 'UNKNOWNERROR' }; } private static getErrorDetails(error: TypedError): any { switch (error.type) { case 'VALIDATIONERROR': return { fields: (error as ValidationError).fields }; case 'NOTFOUND': return { resource: (error as NotFoundError).resource }; case 'APIERROR': return { cause: (error as ApiError).cause?.message }; default: return undefined; } } }

Patterns de performance et optimisation TypeScript permet d'implémenter des patterns d'optimisation

sophistiqués avec des garanties de type au moment de la compilation, améliorant à la fois la performance runtime et la sûreté du code.

// Lazy Loading avec types préservés type LazyLoadable<T> = { readonly loaded: boolean; readonly loading: boolean; readonly error: Error | null; readonly data: T | null; load(): Promise<T>; reload(): Promise<T>; reset(): void; }; class LazyLoader<T> implements LazyLoadable<T> { private loaded = false; private loading = false; private error: Error | null = null; private data: T | null = null; private loadPromise: Promise<T> | null = null; constructor(private loader: () => Promise<T>) {} get loaded(): boolean { return this.loaded; } get loading(): boolean { return this.loading; } get error(): Error | null { return this.error; } get data(): T | null { return this.data; } async load(): Promise<T> { if (this.loaded && this.data !== null) { return this.data; } if (this.loading && this.loadPromise) { return this.loadPromise; } this.loading = true; this.error = null; this.loadPromise = this.loader() .then(data => { this.data = data; this.loaded = true; this.loading = false; return data; }) .catch(error => { this.error = error; this.loading = false; throw error; }); return this.loadPromise; } async reload(): Promise<T> { this.reset(); return this.load(); } reset(): void { this.loaded = false; this.loading = false; this.error = null; this.data = null; this._loadPromise = null; } } // Cache type-safe avec TTL et invalidation intelligente interface CacheEntry<T> { value: T; createdAt: number; ttl: number; accessCount: number; lastAccessed: number; } interface CacheStrategy { maxSize: number; defaultTtl: number; cleanupInterval: number; evictionPolicy: 'lru' | 'lfu' | 'ttl'; } class TypeSafeCache<K extends string | number, V> { private cache = new Map<K, CacheEntry<V>>(); private timers = new Map<K, NodeJS.Timeout>(); constructor(private strategy: CacheStrategy) { this.startCleanupProcess(); } set(key: K, value: V, ttl?: number): void { const effectiveTtl = ttl ?? this.strategy.defaultTtl; const now = Date.now(); // Éviction si cache plein if (this.cache.size >= this.strategy.maxSize && !this.cache.has(key)) { this.evictOne(); } // Nettoyage du timer existant const existingTimer = this.timers.get(key); if (existingTimer) { clearTimeout(existingTimer); } // Création de l'entrée const entry: CacheEntry<V> = { value, createdAt: now, ttl: effectiveTtl, accessCount: 0, lastAccessed: now }; this.cache.set(key, entry); // Timer d'expiration const timer = setTimeout(() => { this.delete(key); }, effectiveTtl); this.timers.set(key, timer); } get(key: K): V | undefined { const entry = this.cache.get(key); if (!entry) { return undefined; } const now = Date.now(); // Vérification TTL if (now - entry.createdAt > entry.ttl) { this.delete(key); return undefined; } // Mise à jour des statistiques d'accès entry.accessCount++; entry.lastAccessed = now; return entry.value; } delete(key: K): boolean { const timer = this.timers.get(key); if (timer) { clearTimeout(timer); this.timers.delete(key); } return this.cache.delete(key); } clear(): void { for (const timer of this.timers.values()) { clearTimeout(timer); } this.timers.clear(); this.cache.clear(); } // Méthodes d'éviction selon la stratégie private evictOne(): void { let keyToEvict: K | null = null; switch (this.strategy.evictionPolicy) { case 'lru': keyToEvict = this.findLRUKey(); break; case 'lfu': keyToEvict = this.findLFUKey(); break; case 'ttl': keyToEvict = this.findEarliestExpiryKey(); break; } if (keyToEvict) { this.delete(keyToEvict); } } private findLRUKey(): K | null { let oldestKey: K | null = null; let oldestTime = Infinity; for (const [key, entry] of this.cache) { if (entry.lastAccessed < oldestTime) { oldestTime = entry.lastAccessed; oldestKey = key; } } return oldestKey; } private findLFUKey(): K | null { let leastUsedKey: K | null = null; let lowestCount = Infinity; for (const [key, entry] of this.cache) { if (entry.accessCount < lowestCount) { lowestCount = entry.accessCount; leastUsedKey = key; } } return leastUsedKey; } private findEarliestExpiryKey(): K | null { let earliestKey: K | null = null; let earliestExpiry = Infinity; for (const [key, entry] of this.cache) { const expiryTime = entry.createdAt + entry.ttl; if (expiryTime < earliestExpiry) { earliestExpiry = expiryTime; earliestKey = key; } } return earliestKey; } private startCleanupProcess(): void { setInterval(() => { this.cleanup(); }, this.strategy.cleanupInterval); } private cleanup(): void { const now = Date.now(); const expiredKeys: K[] = []; for (const [key, entry] of this.cache) { if (now - entry.createdAt > entry.ttl) { expiredKeys.push(key); } } for (const key of expiredKeys) { this.delete(key); } } // Métriques et statistiques getStats() { const now = Date.now(); let totalAccesses = 0; let totalAge = 0; for (const entry of this.cache.values()) { totalAccesses += entry.accessCount; totalAge += now - entry.createdAt; } return { size: this.cache.size, maxSize: this.strategy.maxSize, utilizationRate: this.cache.size / this.strategy.maxSize, averageAccesses: totalAccesses / this.cache.size || 0, averageAge: totalAge / this.cache.size || 0 }; } } // Service de données avec cache intelligent et type safety interface DataService<T> { get(id: string): Promise<T | null>; getMany(ids: string[]): Promise<T[]>; invalidate(id: string): void; invalidateAll(): void; } class CachedDataService<T> implements DataService<T> { private cache: TypeSafeCache<string, LazyLoadable<T>>; private batchLoader: LazyLoader<Map<string, T>>; constructor( private fetcher: (id: string) => Promise<T>, private batchFetcher: (ids: string[]) => Promise<T[]>, cacheStrategy: CacheStrategy ) { this.cache = new TypeSafeCache<string, LazyLoadable<T>>(cacheStrategy); this.batchLoader = new LazyLoader(async () => { const allIds = Array.from(this.cache['cache'].keys()); const results = await this.batchFetcher(allIds); return new Map(results.map(item => [(item as any).id, item])); }); } async get(id: string): Promise<T | null> { // Vérification du cache local let lazyLoadable = this.cache.get(id); if (!lazyLoadable) { // Création d'un lazy loader pour cet élément lazyLoadable = new LazyLoader(() => this.fetcher(id)); this.cache.set(id, lazyLoadable, 300000); // 5 minutes TTL } try { const result = await lazyLoadable.load(); return result; } catch (error) { console.error(Failed to load data for id ${id}:, error); return null; } } async getMany(ids: string[]): Promise<T[]> { const results: T[] = []; const missingIds: string[] = []; // Vérification du cache pour chaque ID for (const id of ids) { const cached = this.cache.get(id); if (cached && cached.loaded && cached.data) { results.push(cached.data); } else { missingIds.push(id); } } // Chargement par batch pour les IDs manquants if (missingIds.length > 0) { try { const batchResults = await this.batchFetcher(missingIds); // Mise en cache des résultats for (const item of batchResults) { const id = (item as any).id; const lazyLoadable = new LazyLoader(() => Promise.resolve(item)); lazyLoadable['data'] = item; lazyLoadable['loaded'] = true; this.cache.set(id, lazyLoadable); results.push(item); } } catch (error) { console.error('Batch loading failed:', error); } } return results; } invalidate(id: string): void { this.cache.delete(id); } invalidateAll(): void { this.cache.clear(); this.batchLoader.reset(); } // Métriques de performance getPerformanceMetrics() { return { cache: this.cache.getStats(), batchLoader: { loaded: this.batchLoader.loaded, loading: this.batchLoader.loading, error: this.batchLoader.error?.message } }; } } // Exemple d'utilisation avec types stricts interface Product { id: string; name: string; price: number; categoryId: string; createdAt: number; } const productService = new CachedDataService<Product>( // Fetcher individuel async (id: string): Promise<Product> => { const response = await fetch(/api/products/${id}); return response.json(); }, // Fetcher par batch async (ids: string[]): Promise<Product[]> => { const response = await fetch('/api/products/batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids }) }); return response.json(); }, // Configuration du cache { maxSize: 1000, defaultTtl: 300000, // 5 minutes cleanupInterval: 60000, // 1 minute evictionPolicy: 'lru' } ); // Utilisation type-safe async function example() { // Chargement individuel avec cache const product = await productService.get('123'); // Chargement par batch avec optimisation const products = await productService.getMany(['123', '456', '789']); // Invalidation ciblée productService.invalidate('123'); // Métriques de performance const metrics = productService.getPerformanceMetrics(); console.log('Performance metrics:', metrics); }

Testing et qualité avec TypeScript TypeScript transforme l'approche du testing en permettant des tests type-safe avec des mocks intelligents et une validation compile-time des scénarios de test.

// Framework de testing type-safe interface TestCase<TInput, TExpected> { name: string; input: TInput; expected: TExpected; setup?: () => void | Promise<void>; cleanup?: () => void | Promise<void>; } interface MockConfig<T> { [K in keyof T]?: T[K] extends (...args: any[]) => any ? jest.MockedFunction<T[K]> : T[K]; } // Classe de base pour tests type-safe abstract class TypeSafeTestSuite<TSubject> { protected subject!: TSubject; protected mocks: Partial<MockConfig<any>> = {}; abstract createSubject(): TSubject; abstract setupMocks(): void; beforeEach(): void { this.setupMocks(); this.subject = this.createSubject(); } afterEach(): void { jest.clearAllMocks(); } // Méthode helper pour les tests paramétrés protected runTestCases<TInput, TExpected>( testCases: TestCase<TInput, TExpected>[], executor: (input: TInput) => TExpected | Promise<TExpected> ): void { testCases.forEach(({ name, input, expected, setup, cleanup }) => { test(name, async () => { if (setup) await setup(); try { const result = await executor(input); expect(result).toEqual(expected); } finally { if (cleanup) await cleanup(); } }); }); } // Helper pour vérifier les appels de méthodes protected expectMethodCalled<T, K extends keyof T>( mock: MockConfig<T>, method: K, times: number = 1, args?: any[] ): void { const mockedMethod = mock[method] as jest.MockedFunction<any>; expect(mockedMethod).toHaveBeenCalledTimes(times); if (args) { expect(mockedMethod).toHaveBeenCalledWith(...args); } } } // Tests pour le UserService class UserServiceTestSuite extends TypeSafeTestSuite<TypeSafeUserService> { private mockUserRepository!: MockConfig<Repository<User, UserId>>; private mockSessionRepository!: MockConfig<Repository<Session, UserId>>; createSubject(): TypeSafeUserService { return new TypeSafeUserService( this.mockUserRepository as Repository<User, UserId>, this.mockSessionRepository as Repository<Session, UserId> ); } setupMocks(): void { this.mockUserRepository = { findById: jest.fn(), save: jest.fn(), delete: jest.fn(), findMany: jest.fn() }; this.mockSessionRepository = { findById: jest.fn(), save: jest.fn(), delete: jest.fn(), findMany: jest.fn() }; } testCreateUser(): void { interface CreateUserInput { email: string; password: string; } const testCases: TestCase<CreateUserInput, Partial<User>>[] = [ { name: 'should create user with valid data', input: { email: '[email protected]', password: 'password123' }, expected: { email: TypeSafeFactories.createEmail('[email protected]'), isActive: true } }, { name: 'should handle email validation', input: { email: 'invalid-email', password: 'password123' }, expected: null // Attendu une erreur } ]; this.runTestCases(testCases, async (input) => { // Configuration du mock const savedUser: User = { id: TypeSafeFactories.createUserId(crypto.randomUUID()), email: TypeSafeFactories.createEmail(input.email), passwordHash: TypeSafeFactories.createHashedPassword(input.password), createdAt: TypeSafeFactories.createTimestamp(), updatedAt: TypeSafeFactories.createTimestamp(), isActive: true }; this.mockUserRepository.save!.mockResolvedValue(savedUser); try { const result = await this.subject.createUser(input.email, input.password); return { email: result.email, isActive: result.isActive }; } catch (error) { return null; } }); } testAuthenticateUser(): void { describe('authenticateUser', () => { test('should authenticate valid user', async () => { const email = '[email protected]'; const password = 'password123'; const existingUser: User = { id: TypeSafeFactories.createUserId(crypto.randomUUID()), email: TypeSafeFactories.createEmail(email), passwordHash: TypeSafeFactories.createHashedPassword(password), createdAt: TypeSafeFactories.createTimestamp(), updatedAt: TypeSafeFactories.createTimestamp(), isActive: true }; const expectedSession: Session = { id: TypeSafeFactories.createUserId(crypto.randomUUID()), userId: existingUser.id, token: TypeSafeFactories.createJWT({ userId: existingUser.id }, 'secret'), expiresAt: (Date.now() + 3600000) as Timestamp, createdAt: TypeSafeFactories.createTimestamp() }; // Configuration des mocks this.mockUserRepository.findMany!.mockResolvedValue([existingUser]); this.mockSessionRepository.save!.mockResolvedValue(expectedSession); // Exécution du test const result = await this.subject.authenticateUser(email, password); // Vérifications expect(result).not.toBeNull(); expect(result?.userId).toBe(existingUser.id); this.expectMethodCalled(this.mockUserRepository, 'findMany', 1); this.expectMethodCalled(this.mockSessionRepository, 'save', 1); }); test('should reject invalid credentials', async () => { this.mockUserRepository.findMany!.mockResolvedValue([]); const result = await this.subject.authenticateUser('[email protected]', 'wrong'); expect(result).toBeNull(); }); }); } } // Tests d'intégration avec types préservés class IntegrationTestSuite { private app: any; // Application Express/Fastify private request: any; // Supertest instance async setup(): Promise<void> { // Configuration de l'application de test this.app = await this.createTestApp(); this.request = require('supertest')(this.app); } async teardown(): Promise<void> { // Nettoyage des ressources await this.app.close(); } async testApiEndpoints(): Promise<void> { interface ApiTestCase { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: string; body?: any; expectedStatus: number; expectedBody?: any; headers?: Record<string, string>; } const testCases: ApiTestCase[] = [ { method: 'POST', path: '/api/users', body: { email: '[email protected]', password: 'password123', firstName: 'Test', lastName: 'User' }, expectedStatus: 201, expectedBody: { id: expect.any(String), email: '[email protected]', firstName: 'Test', lastName: 'User', isActive: true } }, { method: 'GET', path: '/api/users/invalid-id', expectedStatus: 404, expectedBody: { error: 'User not found' } } ]; for (const testCase of testCases) { const { method, path, body, expectedStatus, expectedBody, headers } = testCase; test(${method} ${path} should return ${expectedStatus}, async () => { let request = this.requestmethod.toLowerCase(); if (headers) { Object.entries(headers).forEach(([key, value]) => { request = request.set(key, value); }); } if (body) { request = request.send(body); } const response = await request.expect(expectedStatus); if (expectedBody) { expect(response.body).toMatchObject(expectedBody); } }); } } private async createTestApp(): Promise<any> { // Configuration d'une application de test // avec base de données en mémoire, mocks, etc. return { close: jest.fn() }; } } // Property-based testing avec TypeScript interface PropertyTestGenerator<T> { generate(): T; shrink(value: T): T[]; } class StringGenerator implements PropertyTestGenerator<string> { constructor( private minLength: number = 0, private maxLength: number = 100 ) {} generate(): string { const length = Math.floor(Math.random()  (this.maxLength - this.minLength + 1)) + this.minLength; return Array.from({ length }, () => String.fromCharCode(Math.floor(Math.random()  26) + 97) ).join(''); } shrink(value: string): string[] { if (value.length <= this.minLength) return []; const shrunk: string[] = []; // Réduction de la longueur shrunk.push(value.slice(0, Math.floor(value.length / 2))); // Suppression de caractères for (let i = 0; i < value.length; i++) { shrunk.push(value.slice(0, i) + value.slice(i + 1)); } return shrunk.filter(s => s.length >= this.minLength); } } // Framework de property testing class PropertyTester { static forAll<T>( generator: PropertyTestGenerator<T>, property: (value: T) => boolean, options: { iterations?: number } = {} ): void { const iterations = options.iterations ?? 100; for (let i = 0; i < iterations; i++) { const value = generator.generate(); if (!property(value)) { // Tentative de shrinking pour trouver le cas minimal const shrunk = this.shrinkUntilMinimal(generator, property, value); throw new Error(Property failed for input: ${JSON.stringify(shrunk)}); } } } private static shrinkUntilMinimal<T>( generator: PropertyTestGenerator<T>, property: (value: T) => boolean, initialValue: T ): T { let current = initialValue; let shrinkAttempts = 0; const maxShrinkAttempts = 100; while (shrinkAttempts < maxShrinkAttempts) { const candidates = generator.shrink(current); let foundSmallerFailure = false; for (const candidate of candidates) { if (!property(candidate)) { current = candidate; foundSmallerFailure = true; break; } } if (!foundSmallerFailure) break; shrinkAttempts++; } return current; } } // Exemples de property tests describe('Property-based tests', () => { test('email validation property', () => { const emailGenerator = new StringGenerator(5, 50); PropertyTester.forAll(emailGenerator, (email) => { // La propriété : tout email valide doit contenir @ et . if (TypeSafeFactories.createEmail(email)) { return email.includes('@') && email.includes('.'); } return true; // Les emails invalides sont OK pour ce test }); }); }); // Exécution des suites de tests describe('User Service', () => { const testSuite = new UserServiceTestSuite(); beforeEach(() => testSuite.beforeEach()); afterEach(() => testSuite.afterEach()); testSuite.testCreateUser(); testSuite.testAuthenticateUser(); });

Outils et écosystème recommandés L'écosystème TypeScript mature continuellement avec des outils

spécialisés optimisant le développement, la compilation et la maintenance des applications type-safe complexes. - tsc : Compilateur TypeScript officiel avec optimisations avancées - ESBuild : Compilateur ultra-rapide avec support TypeScript natif - SWC : Compilateur Rust pour TypeScript avec performances exceptionnelles - TypeDoc : Génération de documentation automatique depuis les types - ts-node : Exécution directe TypeScript pour le développement L'adoption des patterns TypeScript avancés transforme fondamentalement l'approche de développement JavaScript en apportant la robustesse des systèmes de types statiques tout en préservant la flexibilité et l'expressivité du langage. Ces techniques constituent un investissement stratégique pour les applications complexes nécessitant une maintenabilité et une fiabilité maximales.