Vous écrivez un type qui parcourt des objets imbriqués, construisant des chemins séparés par des points pour l'autocomplétion. Cela fonctionne parfaitement sur un petit objet de test. Puis, vous l'appliquez à une charge utile (payload) d'API réelle, et l'éditeur freeze. Finalement, TypeScript renvoie l'erreur TS2589 : Type instantiation is excessively deep and possibly infinite.

Ce message ne signifie pas que votre code contient une boucle infinie au sens traditionnel. Cela signifie que le compilateur a abandonné. Le type que vous lui avez demandé de calculer était soit véritablement non borné, soit fini mais si volumineux que son évaluation épuiserait les limites internes de TypeScript. Lorsque cela se produit, le compilateur s'arrête avant de faire planter votre IDE.

Quand l'erreur TS2589 apparaît

Les types récursifs sont les coupables les plus fréquents. TypeScript évalue les types de manière immédiate (eagerly), et si un type utilitaire s'appelle lui-même de manière répétée — surtout via une logique conditionnelle — la pile de calcul croît rapidement. Vous rencontrerez généralement ce mur dans quelques scénarios spécifiques :

  • Les types conditionnels récursifs qui déstructurent de manière répétée un tuple, un objet ou un modèle de chaîne (string template) jusqu'à atteindre un cas de base.
  • Les générateurs de chemins d'objets profondément imbriqués, qui transforment des structures comme { user: { address: { street: string } } } en unions de littéraux de chaînes tels que "user" | "user.address" | "user.address.street".
  • Les types de littéraux de gabarits (template literal types) qui analysent des chaînes caractère par caractère ou jeton par jeton.
  • Les types mappés (mapped types) itérés sur des objets possédant des dizaines de clés et plusieurs niveaux.
  • Les types conditionnels qui se distribuent sur de grandes unions, multipliant silencieusement la charge de travail pour chaque membre.

L'exemple des chemins imbriqués est particulièrement séduisant. Les bibliothèques de formulaires et les outils de gestion d'état adorent proposer des chemins typés afin que vous bénéficiiez de l'autocomplétion pour les noms de champs. Sur un objet peu profond, générer chaque chemin de point légal sous forme d'union de chaînes est trivial. Sur un objet profond ou large, cette union explose. TypeScript doit maintenir chaque permutation en mémoire de travail simultanément. À une certaine profondeur, le compilateur remarque que le travail dépasse son budget et tire le frein d'urgence.

Solution 1 : Ajouter une limite de profondeur stricte

Le moyen le plus direct de résoudre TS2589 est d'arrêter de prétendre que votre type peut récurer indéfiniment. Introduisez un compteur de profondeur qui agit comme un disjoncteur.

En pratique, cela signifie ajouter un paramètre générique numérique — souvent représenté par un tuple dont la longueur décroît — qui diminue à chaque récursion du type. Lorsque le compteur atteint zéro, le type renvoie une solution de repli large telle que string au lieu de creuser davantage. Les utilisateurs bénéficient toujours d'une autocomplétion précise pour les quatre ou cinq premiers niveaux, ce qui couvre la grande majorité des objets du monde réel. Au-delà, le compilateur élargit simplement le type et passe à la suite.

Cette approche ne rend pas votre type utilitaire moins correct de manière significative. Elle le rend borné. Un système de types qui fait planter le compilateur n'est pas plus utile qu'un système qui abandonne proprement après une profondeur raisonnable.

Solution 2 : Valider un seul chemin à la fois

Si la génération de tous les chemins possibles en amont est trop coûteuse, changez le contrat. Au lieu de produire une union massive de toutes les chaînes valides, écrivez un type qui vérifie si une chaîne spécifique est un chemin valide.

Pensez à la différence entre la création d'un dictionnaire de tous les mots anglais et la vérification de l'orthographe d'un seul mot. Le premier est une structure de données énorme ; le second est un balayage léger. En termes de TypeScript, plutôt que d'exporter un utilitaire Paths<T> qui produit "user.address.street" | "user.settings.theme" | ..., vous exportez quelque chose comme IsValidPath<T, "user.address.street">. Le compilateur n'évalue que le chemin que vous passez réellement.

Ce changement modifie la conception de vos API. Vos signatures de fonction pourraient accepter une chaîne, puis utiliser une contrainte générique pour la vérifier par rapport à la forme de l'objet. L'IDE continuera de signaler une erreur si le développeur tape un mauvais chemin, mais le compilateur n'aura jamais à matérialiser l'ensemble complet des chemins légaux pendant la vérification des types. Pour les objets volumineux, la différence de performance est spectaculaire.

Tactiques rapides pour continuer à avancer

Au-delà des deux corrections structurelles, quelques petites habitudes peuvent empêcher les types récursifs de franchir la limite :

  • Enveloppez les paramètres de type dans des tuples pour bloquer la distribution. Un paramètre de type nu dans une conditionnelle, comme T extends Foo ? Bar : Baz, distribue la vérification sur chaque membre lorsque T est une union. Si cette union compte cinquante membres, TypeScript effectue cinquante instanciations distinctes. Écrire [T] extends [Foo] ? Bar : Baz évalue la conditionnelle une seule fois par rapport à l'ensemble de l'union. Utilisez cette méthode dès que vous n'avez pas réellement besoin que le type itère sur chaque membre de l'union individuellement.

  • Réduisez vos entrées lors du débogage. Lorsque l'erreur TS2589 apparaît, remplacez le type de votre objet de production par un stub minuscule comportant deux propriétés et un seul niveau d'imbrication. Si l'erreur disparaît, vous avez confirmé que le problème vient de la profondeur ou de la cardinalité, et non d'une erreur de syntaxe. Cela vous évite de réécrire une logique qui était en réalité correcte sur le plan structurel.

  • Assouplissez les types de vos API publiques. En interne, vous pourriez avoir besoin d'une précision chirurgicale. À l'externe, la perfection coûte parfois plus cher qu'elle ne rapporte. Si un type d'autocomplétion légèrement plus large évite un décalage de deux secondes dans l'éditeur, le compromis en vaut généralement la peine. Vous pouvez coupler le type plus souple avec un validateur au runtime pour intercepter les mauvais chemins lors des tests.

Pourquoi TypeScript impose cette limite

TypeScript ne peut pas résoudre le problème de l'arrêt (halting problem). Il ne sait pas si votre type récursif finira par s'arrêter ou s'il va s'emballer indéfiniment. Plutôt que de risquer une boucle infinie à l'intérieur du compilateur, il impose une limite conservatrice. Parfois, cette limite bloque un type qui aurait pu se terminer, avec suffisamment de temps. TS2589 est l'aveu du compilateur qu'il préfère être prudent plutôt que de prendre des risques.

Respecter cette limite fait partie de l'écriture de types de qualité production. Une définition de type est du code qui s'exécute dans le compilateur, et un code coûteux a des conséquences réelles. Un autocomplétion lent nuit à la vélocité des développeurs tout autant qu'un code d'exécution lent nuit à l'expérience utilisateur.

L'essentiel à retenir

TS2589 n'est pas le signe que vous êtes un mauvais programmeur de systèmes de types. C'est le signe que votre type effectue trop de travail à la fois. Limitez votre récursion, validez de manière paresseuse (lazily) et protégez-vous contre la distribution inutile. L'objectif des types avancés n'est pas de prouver chaque vérité possible au moment de la compilation ; il s'agit de fournir à votre équipe des outils rapides et fiables. Un type qui compile en quelques millisecondes et couvre quatre-vingt-quinze pour cent des cas est bien plus précieux qu'un type théoriquement parfait qui fait planter le serveur de langage (language server).