Trois patterns TypeScript qui améliorent l’architecture des applications React
Découvrez comment les patterns Repository, Observer et Builder utilisent le système de types de TypeScript pour créer des bases de code React et Next.js plus propres et plus faciles à maintenir.
Écrire en TypeScript ne signifie pas forcément l’utiliser correctement
De nombreux développeurs ajoutent simplement des annotations de type à leurs variables et s’en contentent. Mais la véritable force de TypeScript réside ailleurs — dans les modèles structuraux qui permettent de distinguer un code fragile d’un code capable de se développer harmonieusement avec le temps.
Ayant passé du temps à créer des applications en production avec React et Next.js, quelques modèles se distinguent comme véritables changements de paradigme. Il ne s’agit pas d’exercices abstraits tirés de manuels — ce sont des solutions aux problèmes que vous rencontrerez réellement.
1. Modèle de repository — Séparez la récupération des données de tout le reste
Lorsque vos composants appellent directement fetch(), vous vous exposez à une réécriture fastidieuse dès que l’API change de structure.
Le Modèle de repository cache l’accès aux données derrière une interface claire :
// repositories/userRepository.ts
interface UserRepository {
getById(id: string): Promise<User>;
getAll(): Promise<User[]>;
}
export class ApiUserRepository implements UserRepository {
async getById(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async getAll(): Promise<User[]> {
const res = await fetch('/api/users');
return res.json();
}
}
Avec cela en place, vos composants dépendent d’une abstraction plutôt que d’une implémentation concrète. Cela signifie que vous pouvez remplacer ApiUserRepository par une version simulée dans vos tests sans toucher au moindre code d’interface utilisateur.
2. Pattern Observateur — État réactif sans la surcharge de Redux
Redux remplit sa fonction, mais pour des états qui ne sont pas particulièrement complexes, cela peut ressembler à l’utilisation d’une masse pour une tâche mineure. Une implémentation légère basée sur des classes, inspirée d’un émetteur d’événements typé, offre une alternative plus simple :
// utils/eventBus.ts
type EventMap = {
'user:loggedIn': { userId: string };
'cart:updated': { itemCount: number };
};
class TypedEventBus {
private listeners: Partial<{
[K in keyof EventMap]: ((payload: EventMap[K]) => void)[]
}> = {};
on<K extends keyof EventMap>(event: K, cb: (payload: EventMap[K]) => void) {
(this.listeners[event] ??= []).push(cb);
}
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) {
this.listeners[event]?.forEach(cb => cb(payload));
}
}
export const eventBus = new TypedEventBus();
Tout ici est fortement typé — aucun type any caché dans l’ombre. Cela signifie qu’il n’y a aucune ambiguïté quant à la forme du payload que chaque écouteur doit attendre.
3. Pattern Constructeur — Mettre de l’ordre dans la création d’objets complexes
Si vous avez déjà eu du mal à créer des objets de filtre, des configurations de requêtes API ou des schémas de formulaires remplis de conditions imbriquées, le pattern Builder apporte une clarté immédiate :
// builders/queryBuilder.ts
class QueryBuilder {
private params: Record<string, string> = {};
withPage(page: number) {
this.params['page'] = String(page);
return this;
}
withLimit(limit: number) {
this.params['limit'] = String(limit);
return this;
}
withSearch(term: string) {
if (term.trim()) this.params['q'] = term;
return this;
}
build(): string {
return new URLSearchParams(this.params).toString();
}
}
// Usage
const query = new QueryBuilder()
.withPage(1)
.withLimit(20)
.withSearch('typescript')
.build();
// → "page=1&limit=20&q=typescript"
Le code résultant se lit naturellement, les étapes s’enchaînent proprement, et il devient structurellement impossible d’appeler les étapes dans le mauvais ordre.
Points clés
- Pattern Repository : sépare la couche d’accès aux données de la logique de l’interface utilisateur, en suivant le principe d’inversion des dépendances
- Pattern Observer : permet une communication réactive et légère entre les différentes parties de votre application, sans code générique encombrant
- Pattern Builder : rend la création d’objets complexes à la fois lisible et sécurisée
- Tous ces trois patterns bénéficient considérablement des interfaces et des génériques de TypeScript, ce qui les rend nettement plus propres que leurs équivalents en JavaScript pur
« TypeScript ne se limite pas à l’ajout de types — il crée un dialogue entre chaque couche de votre application. » — Dan Vanderkam, auteur de Effective TypeScript
Que tester ensuite
Sélectionnez un modèle présent dans cet article et appliquez-le à un module réel de votre codebase cette semaine. Travaillez sur une partie à la fois, et n’oubliez pas que ces modèles sont des outils, pas des dogmes.
Les meilleures bases de code ne se définissent pas par le nombre de types qu’elles contiennent, même dans un projet TypeScript. Elles se définissent par leur intentionnalité — chaque modèle y est présent parce qu’il a mérité sa place.
Lectures complémentaires
- Le compilateur Go de TypeScript et l’exécution native : un guide de migration — Découvrez comment le compilateur basé sur Go de TypeScript et l’exécution native Node.js affecteront les bases de code React et Next.js, ainsi que ce qu’il convient de corriger dans votre tsconfig dès maintenant.
- Générer automatiquement un client API type-safe Next.js à partir de NestJS Swagger — Apprenez comment éliminer les types API redondants en utilisant NestJS Swagger et Orval pour générer automatiquement des hooks React Query type-safe pour Next.js.