TypeScript : 5 patterns avancés à connaître
Generics, discriminated unions, template literals — maîtrisez TypeScript comme un pro.
TypeScript : au-delà des bases
TypeScript, c'est un peu comme passer du vélo au scooter électrique — au début tu pédales pareil, et puis tu réalises que tu peux aller beaucoup plus loin avec beaucoup moins d'efforts. La plupart des devs s'arrêtent aux types de base : string, number, interface. C'est déjà bien. Mais les vrais pouvoirs de TypeScript, ceux qui te sauvent d'une nuit blanche à chasser un bug invisible, sont cachés dans ses fonctionnalités avancées.
Dans cet article, on couvre 5 patterns que tu peux intégrer dès aujourd'hui dans tes projets React et Next.js. Pas de théorie abstraite — que du concret, avec des cas d'usage réels.
1. Discriminated Unions — modéliser les états d'une requête
Le problème : dans une app React, t'as souvent un composant qui affiche des données chargées depuis une API. Il peut être en chargement, en erreur, ou afficher un résultat. La tentation, c'est de tout mettre dans un seul objet avec des champs optionnels :
// Avant — le chaos
interface State {
loading: boolean;
data?: User;
error?: string;
}
Le souci ? Rien ne t'empêche d'avoir loading: true ET data rempli en même temps. TypeScript ne peut pas t'aider à naviguer ces combinaisons impossibles.
La solution — Discriminated Unions :
// Après — chaque état est explicite
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
type UserState = RequestState<User>;
Et dans ton composant, TypeScript sait exactement quels champs sont disponibles selon le status :
function UserCard({ state }: { state: UserState }) {
if (state.status === 'loading') return <Spinner />;
if (state.status === 'error') return <p>{state.message}</p>;
if (state.status === 'success') return <p>{state.data.name}</p>;
return null;
}
Le compilateur te garantit que tu n'accèdes à state.data que quand il existe vraiment. C'est ça, du code défensif élégant.
2. Generic Constraints — du fetching de données type-safe
Le problème : t'as une fonction utilitaire fetchData générique. Sans contraintes, les generics sont trop permissifs — tu peux lui passer n'importe quoi, même des types qui n'ont rien à voir avec une réponse API.
// Avant — trop permissif
async function fetchData<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json();
}
La solution — Generic Constraints avec extends :
// On définit ce qu'une "entité API" doit avoir
interface ApiEntity {
id: string | number;
createdAt: string;
}
// T doit obligatoirement respecter la forme d'ApiEntity
async function fetchEntity<T extends ApiEntity>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as T;
}
// Usage — TypeScript vérifie que User respecte ApiEntity
const user = await fetchEntity<User>('/api/users/1');
console.log(user.id); // garanti par la contrainte
Tu peux pousser encore plus loin en combinant plusieurs contraintes :
// Fonction de tri générique — T doit avoir un champ `sortOrder`
function sortByOrder<T extends { sortOrder: number }>(items: T[]): T[] {
return [...items].sort((a, b) => a.sortOrder - b.sortOrder);
}
Un seul pattern, zéro duplication de code, typage complet. C'est exactement ce qu'on utilise dans nos projets Next.js pour gérer le tri des projets et articles en base de données.
3. Template Literal Types — des systèmes de routes et d'événements type-safe
Le problème : dans une grosse app, t'as des chaînes de caractères qui suivent une convention (/admin/projets, on:click, GET:/api/users). Sans typage, n'importe quelle string passe. Une faute de frappe en prod, et bonne chance pour retrouver le bug.
La solution — Template Literal Types :
// Définir les segments valides
type AdminSection = 'projets' | 'articles' | 'clients' | 'factures';
type AdminRoute = `/admin/${AdminSection}`;
// TypeScript accepte seulement les routes valides
function navigateTo(route: AdminRoute) {
window.location.href = route;
}
navigateTo('/admin/projets'); // ✓
navigateTo('/admin/unknown'); // ✗ Erreur TypeScript
Même logique pour un système d'événements React :
type EventName = 'click' | 'hover' | 'focus';
type DOMElement = 'button' | 'input' | 'link';
type EventHandler = `on${Capitalize<EventName>}${Capitalize<DOMElement>}`;
// Produit : "onClickButton" | "onHoverButton" | ... (6 combinaisons auto-générées)
Ou pour typer les clés de traduction dans une app multilingue :
type Lang = 'fr' | 'en';
type TranslationKey = `${Lang}.${string}`;
function t(key: TranslationKey): string { /* ... */ }
t('fr.homepage.title'); // ✓
t('homepage.title'); // ✗ Doit commencer par une langue
Les Template Literal Types éliminent toute une catégorie de bugs liés aux chaînes de caractères magiques. Plus de console.log pour vérifier l'orthographe d'une route.
4. Mapped Types avec clause as — transformer des objets en profondeur
Le problème : t'as un objet avec des champs de données, et tu veux en dériver automatiquement un autre type — par exemple, transformer tous les champs en getters, ou préfixer toutes les clés. Sans Mapped Types, tu dois maintenir deux interfaces en parallèle. Synchronisation garantie ? Jamais.
La solution — Mapped Types avec as :
// Type de base
interface UserForm {
name: string;
email: string;
age: number;
}
// Générer automatiquement un type de validation
type FormErrors<T> = {
[K in keyof T as `${string & K}Error`]?: string;
};
type UserFormErrors = FormErrors<UserForm>;
// Résultat : { nameError?: string; emailError?: string; ageError?: string; }
Cas d'usage concret dans un hook de formulaire :
// Rendre tous les champs optionnels pour les updates partielles (PATCH)
type PartialUpdate<T> = {
[K in keyof T]?: T[K] | null;
};
// Extraire seulement les champs string d'un type
type StringFields<T> = {
[K in keyof T as T[K] extends string ? K : never]: string;
};
type UserStringFields = StringFields<UserForm>;
// Résultat : { name: string; email: string; } — age est exclu
La magie : si tu modifies UserForm, tous les types dérivés se mettent à jour automatiquement. Zéro maintenance manuelle.
On utilise ce pattern pour générer les types de filtres dans nos tables admin — une seule source de vérité, partout.
5. Branded Types — ne plus jamais confondre un UserId avec un PostId
Le problème : c'est probablement le bug le plus classique dans une app avec plusieurs entités. Tu as des fonctions qui prennent des IDs :
// Avant — danger silencieux
function getUser(userId: string) { /* ... */ }
function getPost(postId: string) { /* ... */ }
const postId = '123';
getUser(postId); // TypeScript ne dit rien. Mais c'est FAUX.
TypeScript ne peut pas distinguer deux string même si elles représentent des choses différentes. C'est ce qu'on appelle le structural typing — et dans ce cas, ça nous joue des tours.
La solution — Branded Types (aussi appelés Opaque Types) :
// On "marque" les types avec un tag invisible au runtime
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;
type ProjectId = Brand<number, 'ProjectId'>;
// Fonctions helper pour créer des IDs typés
const asUserId = (id: string): UserId => id as UserId;
const asPostId = (id: string): PostId => id as PostId;
Maintenant TypeScript protège :
function getUser(userId: UserId) { /* ... */ }
function getPost(postId: PostId) { /* ... */ }
const userId = asUserId('abc-123');
const postId = asPostId('xyz-456');
getUser(userId); // ✓
getUser(postId); // ✗ Erreur TypeScript — PostId n'est pas un UserId
Au runtime, les Branded Types sont de simples strings ou numbers — aucun overhead. C'est 100% compile-time. Toute la sécurité, aucun coût.
Dans une app Next.js avec plusieurs entités (projets, articles, clients, factures...), ce pattern évite des bugs qui se produiraient silencieusement en production et seraient quasi impossibles à tracer.
Conclusion
Ces 5 patterns, c'est ce qui sépare "du TypeScript qui compile" de "du TypeScript qui protège". Résumé rapide :
- Discriminated Unions → modéliser des états complexes sans champs optionnels ambigus
- Generic Constraints → réutiliser du code sans sacrifier le typage
- Template Literal Types → typer les chaînes de caractères qui suivent une convention
- Mapped Types avec
as→ dériver des types automatiquement depuis une source de vérité unique - Branded Types → distinguer des primitives identiques avec du sens métier
L'idée derrière tout ça, c'est de faire travailler le compilateur pour toi. TypeScript n'est pas là pour t'embêter — il est là pour détecter les bugs avant qu'ils atteignent les utilisateurs. Plus tu lui donnes d'informations sur tes intentions, mieux il peut t'aider.
On applique tous ces patterns au quotidien sur nos projets. Si tu veux voir à quoi ça ressemble dans une vraie codebase Next.js, jette un oeil à nos réalisations — ou parle-nous de ton projet, on sera ravis d'en discuter.
Le guide complet du SEO technique en 2026
Core Web Vitals, structured data, crawl budget — tout ce qu'il faut savoir pour ranker.