Vous modélisez vos états avec des unions discriminées et verrouillez leur exhaustivité grâce au type never.
Ouvrir cette leçon dans Kodokonnever est le type vide : aucune valeur ne l'habite. Il apparaît quand le compilateur prouve qu'un chemin est impossible - après avoir épuisé tous les membres d'une union, par exemple. Cette propriété en fait un outil de conception : si vous parvenez à faire dire never au compilateur dans la branche par défaut d'un switch, vous avez la preuve statique que tous les cas sont traités. C'est le fondement du contrôle d'exhaustivité.
Vous avez découvert les unions discriminées dans le module précédent ; il s'agit maintenant d'en faire un véritable outil de conception. Comparée à une interface unique truffée de champs optionnels, une union discriminée rend les états impossibles irreprésentables : impossible d'avoir à la fois data et error, ou un succès sans données. C'est le pattern de référence pour les états de chargement, les résultats d'opération et les messages d'événements.
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function render(state: FetchState<string[]>): string {
switch (state.status) {
case "idle": return "En attente";
case "loading": return "Chargement…";
case "success": return state.data.join(", ");
case "error": return state.error.message;
}
}Vous avez déjà croisé la garde assertNever appelée dans la branche default ; décortiquons maintenant sa mécanique exacte. Tant que tous les cas sont couverts, le paramètre y est de type never et tout compile. Ajoutez une variante à l'union sans mettre à jour le switch, et la variante oubliée ne s'assigne plus à never : erreur de compilation, avec le nom du cas manquant dans le message. Vous transformez un bug de production en refus de build.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; width: number; height: number };
function assertNever(value: never): never {
throw new Error("Cas non traité : " + String(value));
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "rect": return shape.width * shape.height;
default: return assertNever(shape);
}
}Deux subtilités avancées méritent votre attention. D'abord, le narrowing du discriminant se propage aux structures imbriquées uniquement si vous testez l'objet porteur directement ; déstructurer le discriminant seul casse le lien (sauf cas gérés depuis TypeScript 4.6 pour la déstructuration conjointe). Ensuite, never est absorbant dans les unions (string | never vaut string) : c'est ce qui permet à des types conditionnels de filtrer des variantes, comme le fait Exclude.
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string };
type EventOf<T extends Event["type"]> =
Extract<Event, { type: T }>;
type ClickEvent = EventOf<"click">;
function handle<T extends Event["type"]>(
type: T,
handler: (event: EventOf<T>) => void
): void {
/* enregistrement du handler */
}
handle("click", (e) => console.log(e.x, e.y));Shape sans modifier le switch contenant assertNever ?string | never ?neverstringunknownstring | undefined