Tous les articles
·Ingénierie·7 min

Maîtriser les types TypeScript avancés dans un projet Next.js

Branded types, opérateur satisfies et types conditionnels : trois techniques TypeScript pour renforcer la sûreté du code dans Next.js.


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.

Code TypeScript dans un éditeur de code moderne
Un typage rigoureux réduit les bugs avant même l'exécution.

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 :

ApprocheValidation de conformitéInférence des littéraux
Annotation : TOuiNon
Assertion as TNonNon
Opérateur satisfies TOuiOui

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 dd raisonnable d'une chaîne infer en fonction du nombre de cas nn couverts :

dlog2(n)d \leq \lfloor \log_2(n) \rfloor

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 :

TechniqueCas d'usage principalEmplacement recommandé
Branded typesDistinguer des primitives structurellement identiquesRepository, Server Actions, mappers
satisfiesConfigs, variantes, metadatametadata.ts, fichiers de config Tailwind
infer + types conditionnelsDériver depuis des fonctions existantestypes/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.

Sources

  1. TypeScript Documentation - Conditional Types
  2. TypeScript 4.9 Release Notes - satisfies operator
  3. Branded Types - Learning TypeScript