Accueil / Articles / Trois patterns TypeScript qui améliorent l’architecture des applications React

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.

763 mots

É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

  • Quand l’IA écrit votre React App mais ignore les principes du code propre — Découvrez sept habitudes de codage propre — DRY, responsabilité unique, clauses de protection, etc. — que le code React généré par l’IA enfreint souvent, ainsi que les moyens de les corriger.
  • Comprendre les lanes React : comment les bitmasks codent la priorité des mises à jour — Apprenez comment React encode plusieurs priorités de mise à jour en un seul entier à l’aide de bitmasks, et pourquoi les opérations bitaires remplacent les simples flags booléens pour le planification.