Bonnes pratiques TypeScript : écrire un code plus sûr et plus maintenable
Découvrez les patterns TypeScript les plus importants — mode strict, types utilitaires, génériques, unions discriminées, et comment éviter les échappatoires courantes du système de types.
TypeScript est passé de facultatif à indispensable dans la plupart des projets JavaScript professionnels. Mais il existe un écart considérable entre « techniquement TypeScript » et « TypeScript qui détecte vraiment les bugs ». Cette différence repose sur quelques patterns qui libèrent la véritable puissance du système de types.
Activez le mode strict — toujours
La modification de configuration TypeScript qui apporte le plus de valeur :
// tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
strict active un ensemble de vérifications :
strictNullChecks—nulletundefinedne sont pas assignables à d'autres typesnoImplicitAny— les variables ne peuvent pas avoir implicitement le typeanystrictFunctionTypes— vérification plus stricte des paramètres de fonctionstrictPropertyInitialization— les propriétés de classe doivent être initialisées
Sans strictNullChecks, TypeScript rate la catégorie d'erreurs d'exécution la plus fréquente. Activez-le dès le premier jour — l'ajouter a posteriori sur une grande base de code est particulièrement pénible.
Évitez any — utilisez unknown à la place
any est une échappatoire du système de types qui réduit toutes les erreurs au silence et neutralise l'intérêt même de TypeScript :
// ❌ any désactive toute vérification de type
function processData(data: any) {
data.nonExistentMethod(); // Aucune erreur ! Plante à l'exécution.
}
// ✅ unknown vous oblige à affiner le type avant de l'utiliser
function processData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // Sûr — affiné en string
}
if (data instanceof Error) {
console.log(data.message); // Sûr — affiné en Error
}
}
unknown est la version type-safe de any. Il vous oblige à vérifier ce que vous avez avant de l'utiliser. Utilisez-le pour les réponses d'API, le parsing JSON, et tout ce qui provient de sources externes.
Unions discriminées pour une gestion exhaustive
L'un des patterns les plus puissants de TypeScript :
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState = { status: "error"; message: string };
type ApiState = LoadingState | SuccessState | ErrorState;
function render(state: ApiState) {
switch (state.status) {
case "loading":
return <Spinner />;
case "success":
return <UserList users={state.data} />; // state.data disponible ici
case "error":
return <ErrorMessage msg={state.message} />; // state.message disponible ici
default:
// TypeScript génèrera une erreur ici si vous ajoutez un nouvel état sans le gérer
const _exhaustive: never = state;
throw new Error(`État non géré : ${_exhaustive}`);
}
}
L'assignation à never à la fin est la vérification d'exhaustivité — si vous ajoutez une nouvelle variante à ApiState sans la gérer dans le switch, TypeScript produit une erreur à la compilation. C'est l'un des filets de sécurité les plus précieux de tout le langage.
Types utilitaires
TypeScript embarque des transformateurs de types natifs :
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// Rendre toutes les propriétés optionnelles
type PartialUser = Partial<User>;
// Rendre toutes les propriétés obligatoires
type RequiredUser = Required<User>;
// Rendre toutes les propriétés en lecture seule
type ReadonlyUser = Readonly<User>;
// Sélectionner des propriétés spécifiques
type UserPreview = Pick<User, "id" | "name">;
// Exclure des propriétés spécifiques
type UserWithoutDates = Omit<User, "createdAt">;
// Créer un type à partir des valeurs d'un objet
type UserRole = User["role"]; // "admin" | "editor" | "viewer"
// Rendre les propriétés nullables
type NullableUser = { [K in keyof User]: User[K] | null };
// Type Record : objet avec des types de clé et de valeur spécifiques
type RolePermissions = Record<UserRole, string[]>;
// Extract/Exclude depuis des types union
type AdminOrEditor = Extract<UserRole, "admin" | "editor">; // "admin" | "editor"
type NonAdmin = Exclude<UserRole, "admin">; // "editor" | "viewer"
Les génériques pour un code réutilisable et type-safe
Les génériques vous permettent d'écrire des fonctions et des classes qui fonctionnent avec n'importe quel type tout en restant entièrement type-safe :
// Sans génériques — perd l'information de type
function first(arr: any[]): any {
return arr[0];
}
// Avec génériques — préserve le type
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // type : number | undefined
const str = first(["a", "b"]); // type : string | undefined
Contraindre les génériques
// T doit avoir une propriété 'id'
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// K doit être une clé de T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name"); // type : string
const role = getProperty(user, "role"); // type : string
// getProperty(user, "missing"); // ❌ Erreur TypeScript — "missing" n'est pas une clé
Gardes de type et affinage
// Prédicat de type — garde de type personnalisée
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value
);
}
// Utilisation
const data: unknown = await fetchUser();
if (isUser(data)) {
console.log(data.name); // TypeScript sait que data est de type User ici
}
Pour la validation des réponses d'API à l'exécution, associez les interfaces TypeScript à une validation de schéma à l'exécution avec Zod. Générez des schémas Zod directement depuis votre JSON avec notre convertisseur JSON to Zod — collez un exemple de réponse JSON et obtenez instantanément un schéma type-safe.
Types littéraux de template
TypeScript 4.1+ prend en charge la manipulation de chaînes au niveau des types :
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSKey = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"
Opérateur satisfies (TypeScript 4.9+)
Valide qu'une valeur correspond à un type sans l'élargir :
type ColorMap = Record<string, string>;
// ❌ Sans satisfies — type élargi à Record<string, string>
const colors: ColorMap = {
primary: "#3b82f6",
secondary: "#8b5cf6",
};
colors.primary.toUpperCase(); // Fonctionne mais perd le type string
// ✅ Avec satisfies — valide ET préserve les types littéraux
const colors = {
primary: "#3b82f6",
secondary: "#8b5cf6",
} satisfies ColorMap;
colors.primary.toUpperCase(); // ✅ TypeScript sait que c'est une string
colors.nonexistent; // ❌ Erreur — la clé n'existe pas
Générer des types à partir de données
Au lieu d'écrire des interfaces TypeScript à la main depuis des réponses d'API :
- Collez un exemple de réponse JSON dans notre convertisseur JSON to TypeScript pour obtenir des interfaces instantanément
- Ou générez un schéma Zod avec JSON to Zod — Zod infère automatiquement les types TypeScript via
z.infer<typeof schema>
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
});
// Type inféré automatiquement — aucune duplication
type User = z.infer<typeof UserSchema>;
// Valide à l'exécution ET fournit les types TypeScript
const user = UserSchema.parse(await response.json());
Anti-patterns courants à éviter
| Anti-pattern | Problème | Solution |
|---|---|---|
as any |
Désactive la vérification de type | Utiliser unknown + affinage |
// @ts-ignore |
Réduit les erreurs au silence sans les corriger | Corriger l'erreur de type |
as SomeType (cast non sûr) |
Contourne l'inférence | Utiliser des gardes de type |
interface pour les unions |
Ne fonctionne pas pour les unions discriminées | Utiliser type |
object trop générique |
Aucune structure | Définir une interface appropriée |
Function comme type |
Aucune info sur les paramètres/le retour | Utiliser une signature spécifique |
Configuration TypeScript à connaître
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // array[n] retourne T | undefined
"exactOptionalPropertyTypes": true, // distingue propriété absente vs undefined
"noImplicitReturns": true, // tous les chemins de code doivent retourner une valeur
"noFallthroughCasesInSwitch": true, // pas de fallthrough non intentionnel dans switch
"verbatimModuleSyntax": true // import type explicite pour les imports de types uniquement
}
}
La valeur de TypeScript est proportionnelle à la rigueur avec laquelle vous l'utilisez. Les patterns présentés ici — mode strict, unions discriminées, génériques, vérifications d'exhaustivité — sont ce qui distingue le TypeScript qui détecte les bugs du TypeScript qui ne fait qu'ajouter de la syntaxe.