TypeScript n’est pas lent : ce sont les types sur-conçus qui le sont.
La soupe générique, le DRY prématuré sur les types, l’état de drapeau optionnel et l’ingéniosité au niveau des types ralentissent le développement. Préférez des interfaces simples, des unions discriminées et une évaluation mesurée des coûts avec TSC.
Comment les soupes génériques, la gymnastique des types conditionnels et l’abstraction prématurée ralentissent considérablement la vitesse de livraison.
Lors d’une rétrospective après sprint, un ingénieur junior admet qu’ajouter un champ optionnel au payload d’une API existante a pris quatre heures. L’IDE révèle pourquoi : une interface construite à partir de conditions imbriquées.
type ExtractNestedPayload<
T,
K extends keyof T,
U extends boolean = false
> =
T[K] extends (...args: any[]) => infer R
? R extends Promise<infer P>
? U extends true ? NonNullable<P> : P
: R
: T[K] extends Array<infer Item>
? Item
: never;
Quatre niveaux de conditions imbriquées, trois paramètres génériques, ainsi qu’une chaîne ternaire qui nécessite un tableau blanc. Les erreurs de compilateur apparaissent sous forme d’indicateurs rouges longs :
Type 'ExtractNestedPayload<ApiResponses["users"], "data", true>'
is not assignable to type 'UserDTO'.
Types of property 'meta' are incompatible.Type 'unknown' is not assignable to type
'{ pagination: PaginationMeta }'.
La plainte habituelle revient : TypeScript ralentirait l’équipe. En général, ce n’est pas le cas ; ce sont plutôt les abstractions surdimensionnées qui posent problème. TypeScript a été conçu comme un garde-fou pragmatique — moins d’erreurs en temps de exécution, un complétion automatique améliorée. Au fil du temps, de nombreux projets en ont fait un véritable casse-tête : des heures passées à créer des génériques mathématiquement « parfaits » pour éviter quelques lignes de déclarations simples. Plus le graphe des types devient complexe, moins il aide les développeurs qui mettent en œuvre les fonctionnalités. Un type doit exprimer l’intention de son auteur. Si sa lecture nécessite des types conditionnels, des particularités d’inférence, des types mappés, des unions distributives, des génériques récursifs et une série d’outils spécialisés, l’intention initiale disparaît et il faut déboguer un autre système. Ci-dessous sont présentés des schémas qui ont des effets négatifs en production, ainsi que des alternatives plus simples permettant de retrouver l’efficacité.
1. L’anti-pattern de la soupe de génériques
Les génériques sous-tendent Promise<T>, Array<T> et Map<K, V>. Les problèmes commencent lorsque la flexibilité devient un symbole de seniorité. Plus un élément est générique, ce n’est pas automatiquement mieux. Prenons l’exemple d’un composant de tableau :
// The Generic Soup Nightmare
interface TableProps<
TData,
TKey extends keyof TData,
TColumn extends ColumnDef<TData, any>,
TFilter extends Record<string, any> = Record<string, any>
> {
data: TData[];
keyExtractor: (item: TData) => TData[TKey];
columns: TColumn[];
initialFilter?: TFilter;
onRowClick?: (row: TData) => void;
}
Il semble pouvoir être réutilisé à l’infini : structure des données, clé de ligne, colonnes, filtres. Chaque abstraction entraîne un coût cognitif. Les points d’appel obligent le compilateur à déduire plusieurs paramètres interdépendants. Un petit désaccord dans les propriétés transmises ne cible pas nécessairement la propriété en cause ; la déduction peut échouer dans l’ensemble du graphe. Un nouveau collègue doit comprendre pourquoi TKey existe, pourquoi il étend keyof TData, comment les génériques des colonnes et des filtres interagissent, ainsi que ce que le compilateur a réellement déduit. C’est un coût élevé pour un simple tableau.
Cette taxe se traduit par des revues de code plus lentes, un processus d’intégration plus long, ainsi qu’une culture où une ou deux personnes seulement osent modifier les primitives d’interface partagées. La perte de vitesse est à la fois sociale et technique : les collègues cessent de proposer de petites améliorations par crainte des conséquences. Lorsqu’une abstraction de tableau nécessite un document de conception pour expliquer ses généricités, cette abstraction a dépassé le problème qu’elle était censée résoudre.
Les symptômes en production sont ennuyeux et coûteux. Un simple changement de nom d’une propriété déclenche des diagnostics mentionnant des paramètres de type non liés. La fonction de complétion automatique ralentit tandis que le service linguistique réévalue le graphe des généricités. Les minutes consacrées aux vérifications de type en CI augmentent sans que personne ne propose un modèle de domaine plus clair. Aucun de ces coûts n’apparaît dans une comparaison « TypeScript vs JavaScript » ; ils se manifestent sous forme de temps passé.
L’alternative concrète
Préférez un paramètre de données unique et des interfaces d’assistance simples :
// Simple, readable, and instant compiler diagnostics
interface TableColumn<T> {
header: string;
accessor: (item: T) => React.ReactNode;
width?: string;
}
interface DataTableProps<T> {
data: T[];
columns: TableColumn<T>[];
rowKey: (item: T) => string;
}
Le composant reste réutilisable sans devenir une source d’information sur les types. Être réutilisable ne signifie pas être infiniment générique.
Être réutilisable ne signifie pas être infiniment générique.
Les équipes craignent parfois que la simplification des concepts génériques ne force à recopier-coller du code. En pratique, deux ou trois variantes de table bien définies avec des propriétés claires valent mieux qu’un composant universel que personne ne peut initialiser sans essais et erreurs. Partagez les outils de rendu et le CSS ; gardez les propriétés publiques simples. Le compilateur indiquera alors précisément le champ qui ne correspond pas, au lieu de fusionner quatre variables d’inférence dans un gouffre inconnu.
2. DRY prématuré dans les définitions de types
« Ne te répète pas » est utile pour le code en temps d’exécution mais dangereux lorsqu’il est appliqué aveuglément aux types. En voyant des champs similaires, les équipes dérivent un type à partir d’un autre :
// Over-abstracted type derivation
type RegisterFormValues =
Omit<
UserProfile,
'id' | 'createdAt' | 'updatedAt' | 'role'
> & {
passwordConfirmation: string;
termsAccepted: boolean;
};
Plus tard, l’entité change :
interface UserProfile {
// ...
phoneNumber: string; // now required!
}
Le type de formulaire dérivé hérite silencieusement d’un champ phoneNumber obligatoire que le flux de registration ne souhaitait pas du tout. D’autres modifications s’ensuivent :
type RegisterFormValues =
Omit<
UserProfile,
'id' |
'createdAt' |
'updatedAt' |
'role' |
'phoneNumber'
> & {
phoneNumber?: string;
passwordConfirmation: string;
termsAccepted: boolean;
};
Chaque omission augmente l’opacité. Le formulaire et l’entité de base de données changent pour des raisons différentes ; les lier crée des pannes inattendues.
La duplication est moins coûteuse qu’une abstraction erronée
Rédigez les contrats séparément :
// Database Entity Contract
export interface UserProfile {
id: string;
email: string;
fullName: string;
phoneNumber: string;
createdAt: string;
}
// Registration Form Contract
export interface RegisterFormValues {
email: string;
fullName: string;
phoneNumber?: string;
password: string;
passwordConfirmation: string;
termsAccepted: boolean;
}
Quelques champs dupliqués coûtent moins qu’un graphe de dérivation fragile. Si deux types changent pour des raisons différentes, ils ne devraient probablement pas être liés.
Si deux types évoluent pour des raisons différentes, ils ne devraient probablement pas être couplés.
Un test olfactif utile : un chef de projet décrirait-il ces éléments comme relevant du même concept ? Une ligne de profil utilisateur dans le stockage et un formulaire d’inscription sur une page marketing partagent rarement le même cycle de vie, les mêmes règles de validation ou le même propriétaire. Lorsqu’ils s’éloignent l’un de l’autre, les types dérivés amplifient cette divergence en erreurs de compilation éloignées de la modification qui les a causées. Les interfaces explicites rendent cette divergence visible et localisée. Les outils de mappage — de petites fonctions qui transforment les entités en valeurs par défaut pour les formulaires — assurent une conversion fiable en temps de exécution sans lier définitivement les identités des types.
3. Les unions discriminées valent mieux que les propriétés optionnelles en pagaille
L’état d’une interface utilisateur asynchrone ressemble souvent à ceci :
// The "Optional Flag" Anti-Pattern
interface RequestState<T> {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
data?: T;
error?: Error;
}
Les utilisateurs inventent alors des combinaisons illégales — isSuccess avec un data manquant, ou isLoading avec une erreur toujours active :
if (state.isSuccess && state.data) {
return <div>{state.data.name}</div>;
}
Les flags optionnels ne codent pas une machine à états ; ils expriment de l’espoir.
Le pouvoir des unions discriminées
Rendre le statut explicite :
export type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
Le rendu devient exhaustif et sûr :
function RenderProfile({
state
}: {
state: AsyncState<UserProfile>;
}) {
switch (state.status) {
case 'idle':
return <div>Ready to load profile.</div>;
case 'loading':
return <LoadingSpinner />; case 'error':
return <ErrorMessage error={state.error} />; case 'success':
// TypeScript guarantees state.data exists here!
return <h1>Welcome, {state.data.fullName}</h1>;
}
}
Les états impossibles disparaissent du type, ce qui fait disparaître de nombreuses vérifications de sécurité de l’interface utilisateur.
Les états impossibles disparaissent du type, ce qui fait disparaître de nombreuses vérifications de sécurité de l’interface utilisateur.
Les modèles utilisant des flags optionnels confondent également les fonctionnalités d’analyse et de journalisation. La requête est-elle réussie si isSuccess vaut vrai mais que data n’est pas défini ? Les unions discriminées obligent à répondre à cette question lors de la construction de l’état, et non lorsque un ingénieur junior tente une supposition dans JSX. Les réducteurs et les enveloppes asynchrones deviennent également plus clairs : chaque transition renvoie une variante complète au lieu de basculer entre des booléens qui peuvent être contradictoires.
4. L’erreur any vs unknown
Utiliser any pour faire taire le compilateur annule les avantages de TypeScript aux frontières du code. Préférez unknown et restreignez les types à l’aide de gardes lors de la lecture des données d’API ou de localStorage.
Remplacer any par unknown + gardes de type
// Safe parsing of unknown API or localStorage data
function parseStoredPreferences(
raw: unknown
): UserPreferences {
if (
typeof raw === 'object' &&
raw !== null &&
'theme' in raw &&
(raw.theme === 'light' || raw.theme === 'dark')
) {
return {
theme: raw.theme,
fontSize:
typeof (raw as any).fontSize === 'number'
? (raw as any).fontSize
: 14,
};
}
// Safe fallback default
return {
theme: 'dark',
fontSize: 14
};
}
Les données externes restent non fiables jusqu’à ce qu’elles soient validées ; le code interne prend alors une forme concrète.
Les données externes restent non fiables jusqu’à ce qu’elles soient validées ; le code interne prend alors une forme concrète.
any est particulièrement dangereux aux frontières des modules car il contamine les traitements ultérieurs : une valeur any provenant de JSON.parse peut annuler tous les contrôles d’une fonctionnalité entière. unknown empêche ce contage dès l’entrée. Utilisez des bibliothèques de validation de schéma lorsque les données sont volumineuses, ou des vérifications manuelles ciblées lorsque les structures sont petites et stables. Dans tous les cas, le noyau du domaine ne doit voir que des types validés.
5. Mesurer le temps de vérification des types en CI
Lorsque l’éditeur semble lent, mesurez d’abord avant de blâmer le langage :
npx tsc --noEmit --extendedDiagnostics
Surveillez le nombre d’instanciations, les fichiers et le temps de compilation. Les conditions récursives coûteuses ont souvent la part la plus importante. Si le système de types est coûteux à compiler, il doit justifier ce coût.
Si le système de types est coûteux à compiler, il doit justifier ce coût.
Les diagnostics approfondis révèlent souvent que quelques fichiers sont responsables de la plupart des instanciations — il s’agit fréquemment d’outils conditionnels récursifs importés par commodité. Supprimer ou simplifier ces éléments clés peut permettre d’économiser des minutes à chaque exécution CI. Suivez ce chiffre au fil du temps de la même manière que vous suivez la taille des bundles. Une architecture de types qui ne parvient pas à justifier son coût de compilation sera finalement attribuée au fait que « TypeScript est lent », ce qui incitera l’équipe à sous-investir dans un langage qui continue pourtant de les protéger en temps de exécution.
6. Le problème de l’ingéniosité au niveau des types
L’ingéniosité est un autre piège : des types qui dérivent toute leur interface API d’autres types, puis qui accumulent de plus en plus de conditions, de types mappés et de récursions jusqu’à ce que personne ne veuille les toucher. La capacité d’un type n’est pas une raison d’utiliser la fonctionnalité la plus lourde.
Comparez un accès indexé direct :
type UserName = UserProfile['fullName'];
à une utilité récursive qui cherche chaque propriété à valeur chaîne dans un objet arbitraire. Si le produit n’a besoin que de UserProfile['fullName'], cette complexité ajoute des risques. Une bonne ingénierie choisit l’outil le plus simple et le plus clair. Un type, six mois plus tard, doit se suffire à lui-même ; « ne touchez pas à ceci » signifie que l’abstraction a déjà échoué.
7. Un manifeste pragmatique pour TypeScript
1. Écrivez d’abord des types pour les humains, puis pour le compilateur
Si vos collègues ne peuvent pas comprendre une définition en moins d’une minute, simplifiez-la. Une élégance que seul l’auteur comprend est une dette technique.
2. Préférer la duplication au couplage prématuré
Ne modifiez pas l’interface d’un composant juste pour réutiliser trois champs provenant d’une entité non liée. Séparez les responsabilités, séparez les types.
3. Utiliser des unions discriminées pour l’état
Codez de véritables machines à états avec un discriminant d’état afin que le compilateur élimine les branches impossibles.
4. Ne jamais laisser les généricques dépasser deux paramètres
Trois ou plus de généricques indiquent généralement que l’abstraction est trop large. Divisez-la. Il s’agit d’une heuristique, pas d’une règle absolue — lorsque les relations sont difficiles à expliquer, reconsidérez la conception.
5. Traiter les données externes comme inconnues
Les réponses API, le stockage, les données provenant de tiers et les entrées utilisateur doivent être validés aux frontières avant de devenir des objets de domaine fiables.
6. Mesurer avant de blâmer TypeScript
Un CI lent ou un service linguistique en retard reflète souvent l’architecture des types, et non la marque du langage. Analysez-la puis simplifiez les types fréquemment utilisés.
TypeScript devrait rendre le code ennuyeux
TypeScript est au mieux de ses capacités lorsqu’il reste discret : complétion automatique, refacturations sûres, moins de surprises en temps de exécution. Il ne devrait pas ressembler à un puzzle à chaque modification de composant. Les interfaces les moins impressionnantes — de simples objets métier — sont souvent les plus utiles. Gardez les types simples, pratiques et liés à des concepts de produit réels afin que l’équipe puisse livrer du code plutôt que de déchiffrer une algèbre de types.
Gardez les types simples, pratiques et liés à des concepts de produit réels afin que l’équipe puisse livrer du code plutôt que de déchiffrer une algèbre de types.
Les retours d’expérience s’améliorent lorsque la discussion passe de la marque du langage aux choix de conception : combien de types génériques, quel niveau de dérivation, à quel point la machine d’états est honnête, comment les données externes pénètrent dans l’application. TypeScript récompense cette honnêteté en fournissant des retours plus rapides sur les changements importants. Il punit l’ingéniosité par des erreurs opaques. Choisissez délibérément la voie la plus simple, documentez les rares types avancés qui sont véritablement essentiels, et évitez les patterns de type complexes dans les bibliothèques partagées auxquelles vos nouveaux ingénieurs doivent avoir accès dès le premier jour.
Lorsqu’un changement semble bloqué par les types, demandez-vous si le modèle reflète bien le produit. Souvent, la solution n’est pas une condition plus complexe, mais plutôt une interface plus claire, un module séparé ou une union qui nomme les états déjà évoqués lors des réunions quotidiennes. C’est ainsi que TypeScript cesse d’être un fardeau pour redevenir un atout.