title: "Maîtriser les types TypeScript avancés dans un projet Next.js" description: "Branded types, opérateur satisfies et types conditionnels : trois techniques TypeScript pour renforcer la sûreté du code dans vos applications Next.js." date: "2026-06-09" category: "Ingénierie" readingTime: "7 min" author: "Oliwer Fortin, Tech Lead chez Kreio"
Le système de types structurel de TypeScript est puissant, mais il présente des angles morts que les projets Next.js en production finissent tôt ou tard par rencontrer. Trois techniques permettent d'y remédier : les branded types, l'opérateur satisfies et les types conditionnels avec infer.
Pourquoi le typage structurel peut trahir vos intentions
TypeScript compare les types par leur structure, pas par leur nom. Conséquence directe : UserId et ProductId, tous deux des string, sont interchangeables aux yeux du compilateur. Ce comportement entraîne des bugs silencieux dans les projets qui manipulent plusieurs identifiants distincts.
type UserId = string;
type ProductId = string;
function fetchUser(id: UserId) { /* ... */ }
const productId: ProductId = "prod_123";
fetchUser(productId); // Aucune erreur TypeScript, pourtant incorrect
Ce problème n'est pas théorique. Une confusion entre identifiants peut provoquer des requêtes croisées en production, dans des contextes où Prisma retourne des string sans distinction sémantique. La solution est d'ajouter une dimension nominale au système de types, là où TypeScript est par défaut purement structurel.
Les branded types pour différencier les primitives
Un branded type (type marqué) attache une propriété fantôme à un type primitif. Cette propriété est invisible à l'exécution, mais le compilateur la prend en compte pour distinguer des types autrement identiques.
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
// Fonctions de construction : seul endroit légitime pour un cast
function asUserId(id: string): UserId {
return id as UserId;
}
function fetchUser(id: UserId) {
console.log("Fetching user", id);
}
const userId = asUserId("usr_456");
fetchUser(userId); // OK
// fetchUser("prod_123"); // Erreur de compilation : string n'est pas UserId
La bibliothèque Zod intègre les branded types via .brand(), ce qui permet de combiner validation à l'exécution et typage fort à la compilation. C'est particulièrement utile dans les Server Actions Next.js où les données entrantes n'ont aucune garantie de type côté serveur.
L'opérateur satisfies pour valider sans perdre l'inférence
Introduit dans TypeScript 4.9, l'opérateur satisfies résout un dilemme classique : valider qu'un objet correspond à un type tout en conservant les types littéraux inférés par le compilateur.
Sans satisfies, une annotation classique élargit les types et supprime l'information précise :
type Theme = {
colors: Record<string, string>;
};
const theme: Theme = {
colors: {
primary: "#3B82F6",
danger: "#EF4444",
},
};
// theme.colors.primary est de type string, l'information "#3B82F6" est perdue
Avec satisfies, le compilateur valide la conformité et préserve les types littéraux :
const theme = {
colors: {
primary: "#3B82F6",
danger: "#EF4444",
},
} satisfies Theme;
// theme.colors.primary est de type "#3B82F6" : autocomplétion précise conservée
Dans un projet Next.js, cette technique s'applique aux exports metadata, aux fichiers de configuration de routes et aux variantes de composants Tailwind. Le tableau suivant résume les différences entre les trois approches de typage d'objet :
| Approche | Validation de conformité | Inférence des littéraux |
|---|---|---|
Annotation : T | Oui | Non |
Assertion as T | Non | Non |
Opérateur satisfies T | Oui | Oui |
Types conditionnels et infer pour dériver des types automatiquement
Les types conditionnels permettent d'écrire des conditions au niveau du système de types. La syntaxe T extends U ? X : Y agit comme un ternaire pour les types. Le mot-clé infer, documenté dans la référence officielle TypeScript, pousse cette logique plus loin en extrayant automatiquement un sous-type depuis une structure connue.
// Extraire le type résolu d'une Server Action async
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type UserAction = () => Promise<{ id: string; name: string }>;
type UserResult = UnwrapPromise<ReturnType<UserAction>>;
// Résultat : { id: string; name: string }
// Typer les props d'un Server Component avec paramètre dynamique
type PageProps<T extends string> = {
params: { [K in T]: string };
searchParams: Record<string, string | string[] | undefined>;
};
// Utilisation pour une page /blog/[slug]
type BlogPageProps = PageProps<"slug">;
Ces utilitaires évitent de répéter des définitions identiques dans chaque fichier de page. Ils centralisent la logique de dérivation et restent cohérents si les types sources évoluent.
On peut modéliser la profondeur d'imbrication raisonnable d'une chaîne infer en fonction du nombre de cas couverts :
Au-delà de ce seuil, décomposer en types nommés intermédiaires améliore la lisibilité sans réduire la sûreté du typage.
Organiser ces patterns dans une architecture Next.js
Ces trois techniques s'intègrent naturellement par couche dans un projet App Router. Une organisation recommandée :
src/
types/
brands.ts # UserId, OrderId, ProductId et fonctions de construction
utils.ts # UnwrapPromise, PageProps et autres utilitaires infer
config.ts # Objets satisfies pour les configurations typées
lib/
prisma/
mappers.ts # Seul endroit autorisé pour les casts "as BrandedType"
Le tableau ci-dessous synthétise les cas d'usage recommandés pour chaque pattern :
| Technique | Cas d'usage principal | Emplacement recommandé |
|---|---|---|
| Branded types | Distinguer des primitives structurellement identiques | Repository, Server Actions, mappers |
satisfies | Configs, variantes, metadata | metadata.ts, fichiers de config Tailwind |
infer + types conditionnels | Dériver depuis des fonctions existantes | types/utils.ts, hooks partagés |
Ce qu'on en retient
Le typage avancé TypeScript n'est pas une fin en soi : c'est un investissement en lisibilité et en robustesse du code. Les branded types empêchent les confusions d'identifiants, l'opérateur satisfies préserve l'inférence tout en validant la conformité, et les types conditionnels avec infer permettent de dériver des types depuis les fonctions existantes sans les redéfinir manuellement.
Appliqués avec méthode dans les couches repository, configuration et Server Actions d'un projet Next.js, ces patterns réduisent la surface des erreurs silencieuses : celles que les tests unitaires attrapent rarement parce qu'elles n'ont aucun impact visible à l'exécution.