frontend / typescript
Pattern expert : builder typé
Explication
Ce que vous allez apprendre
- Construire un builder par étapes où chaque méthode retourne un type intermédiaire différent
- Faire en sorte qu'un ordre d'appel incorrect devienne une erreur de compilation, pas une erreur runtime
- Utiliser le type de retour spécial
thispour les méthodes chaînables dans n'importe quel ordre - Construire un générique accumulateur qui trace les clés déjà renseignées d'un objet
- Rendre une méthode de construction inaccessible tant que tous les champs requis ne sont pas fournis
Dans quel contexte ?
Une équipe backend construit des requêtes HTTP sortantes vers des API tierces avec un builder maison. Un développeur oublie d'appeler .avecMethode("POST") avant .construire() : l'objet final se retrouve avec une méthode undefined, et la requête part malgré tout, provoquant une erreur 400 chez le fournisseur tiers, découverte seulement en observant les logs de production le lendemain. Un builder typé comme celui de cette leçon rend cet oubli tout simplement impossible à écrire : le compilateur refuse de compiler le code avant même qu'il ne soit exécuté une seule fois.
Encoder les règles d'usage directement dans les types
Certains objets ne peuvent être construits que dans un ordre précis, ou nécessitent que plusieurs champs obligatoires soient tous renseignés avant utilisation. Le pattern builder classique, une suite d'appels chaînés qui construisent progressivement un objet, existe dans de nombreux langages, mais TypeScript permet d'aller plus loin : faire en sorte que le compilateur lui-même interdise d'appeler les méthodes dans le mauvais ordre, sans écrire la moindre vérification au runtime.
Prérequis
Cette leçon combine des notions déjà vues séparément : les classes, les génériques, et le type de retour this. Si l'une de ces trois notions n'est pas encore solide, il est préférable d'y revenir avant d'aborder ce pattern expert.
Le principe : chaque étape change de type
L'astuce consiste à faire en sorte que chaque méthode du builder ne retourne pas le même type, mais un type intermédiaire différent, qui n'expose que les méthodes encore pertinentes à ce stade de construction. Ainsi, un builder qui n'a pas encore reçu d'URL n'expose tout simplement pas la méthode pour définir la méthode HTTP : tenter d'appeler les méthodes dans le désordre devient une erreur de compilation, détectée avant même d'exécuter le code.
| Étape du builder | Type retourné | Méthodes exposées à ce stade |
|---|---|---|
| Aucune information | BuilderRequeteSansUrl | Seulement .avecUrl() |
| URL fournie | BuilderRequeteSansMethode | Seulement .avecMethode() |
| URL + méthode fournies | BuilderRequetePret | .avecEnTete(), .avecCorps(), .construire() |
Le type this polymorphe
Pour les méthodes qui peuvent être appelées dans n'importe quel ordre, comme ajouter des en-têtes ou un corps de requête, on utilise le type de retour spécial this, qui préserve automatiquement le type exact de l'objet courant, même s'il s'agit d'une sous-classe du builder.
Piège fréquent
Un builder par étapes classique (comme BuilderRequeteSansUrl) protège l'ordre d'appel, mais pas la complétude d'un objet avec de nombreux champs indépendants. C'est justement pour ce second besoin que le pattern accumulateur généralise l'idée un cran plus loin, comme le montre la suite de cette leçon.
Un pattern accumulateur plus avancé
La deuxième partie de la leçon pousse l'idée encore plus loin avec un générique qui accumule, au fil des appels, la liste des clés déjà renseignées. La méthode finale de construction devient alors littéralement inaccessible tant que toutes les clés requises n'ont pas été fournies — une garantie de complétude entièrement vérifiée à la compilation, sans coût à l'exécution.
Commandes & code
Pattern expert : builder typé
interface RequeteHttp {
url: string;
methode: "GET" | "POST";
en_tetes: Record<string, string>;
corps?: unknown;
}
// Builder typé par étapes : chaque méthode retourne un type DIFFÉRENT,
// qui n'expose que les méthodes encore valides à ce stade de construction.
class BuilderRequeteSansUrl {
avecUrl(url: string): BuilderRequeteSansMethode {
return new BuilderRequeteSansMethode(url);
}
}
class BuilderRequeteSansMethode {
constructor(private url: string) {}
avecMethode(methode: "GET" | "POST"): BuilderRequetePret {
return new BuilderRequetePret(this.url, methode);
}
}
class BuilderRequetePret {
private en_tetes: Record<string, string> = {};
private corps?: unknown;
constructor(private url: string, private methode: "GET" | "POST") {}
avecEnTete(cle: string, valeur: string): this {
this.en_tetes[cle] = valeur;
return this; // "this" polymorphe : préserve le type exact même via une sous-classe
}
avecCorps(corps: unknown): this {
this.corps = corps;
return this;
}
construire(): RequeteHttp {
return { url: this.url, methode: this.methode, en_tetes: this.en_tetes, corps: this.corps };
}
}
const requete = new BuilderRequeteSansUrl()
.avecUrl("/api/utilisateurs")
.avecMethode("POST")
.avecEnTete("Content-Type", "application/json")
.avecCorps({ nom: "Ada" })
.construire();
// new BuilderRequeteSansUrl().avecMethode("GET");
// Error : .avecMethode n'existe pas encore, .avecUrl() doit être appelé en premier
// Builder générique type-safe : trace à la compilation quelles clés sont déjà renseignées
class BuilderObjet<T, ClesRenseignees extends keyof T = never> {
private valeurs: Partial<T> = {};
definir<K extends keyof T>(cle: K, valeur: T[K]): BuilderObjet<T, ClesRenseignees | K> {
this.valeurs[cle] = valeur;
return this as BuilderObjet<T, ClesRenseignees | K>;
}
// "construire" n'est utilisable QUE quand toutes les clés de T ont été renseignées
construire(this: BuilderObjet<T, keyof T>): T {
return this.valeurs as T;
}
}
interface Produit {
id: number;
nom: string;
prix: number;
}
const produit = new BuilderObjet<Produit>()
.definir("id", 1)
.definir("nom", "Clavier")
.definir("prix", 49.99)
.construire(); // compile seulement si id, nom ET prix ont été définisRésumé
- Un builder par étapes encode l'ordre d'appel obligatoire dans le SYSTÈME DE TYPES lui-même.
return thisavec le typethispréserve le type exact, y compris pour des sous-classes.- Un générique accumulateur (
ClesRenseignees) rendconstruire()inaccessible tant que toutes les clés requises n'ont pas été renseignées, sans aucune vérification au runtime.
Exercices pratiques
Mission : rendre impossible l'oubli de avecMethode()
Objectif : Transformer un builder monolithique en builder par étapes pour que l'ordre d'appel incorrect devienne une erreur de compilation.
Contexte
Une équipe backend construit des requêtes HTTP avec un builder maison. Un développeur oublie d'appeler .avecMethode("POST") avant .construire() : la requête part malgré tout avec une méthode undefined, provoquant une erreur 400 découverte seulement en production. Ta mission : rendre cet oubli impossible à compiler.