Migrer vers TypeScript 7 sans casser votre monorepo

Par Diogo Hudson Dias
Senior engineer in a São Paulo office examining a tsconfig file and CI dashboard on dual monitors during the evening.

Les montées de version majeures de TypeScript ne cassent pas votre app en production. Elles cassent votre CI, votre éditeur et votre planning. TypeScript 7 ne fera pas exception. Le vérificateur de types se durcit, les anciennes échappatoires se referment, et votre monorepo passe au rouge parce que le « union narrowing » s’est affiné d’un côté tandis que votre plugin de décorateurs a pris une semaine de retard de l’autre. Si vous traitez cela comme une simple mise à jour de dépendances du vendredi, vous perdrez un sprint.

Ce n’est pas un réquisitoire contre les mises à niveau. C’est un playbook pour migrer vers TypeScript 7 sans bloquer la livraison. L’objectif : continuer à livrer des fonctionnalités tout en basculant de compilateur. Concrètement, deux pipelines (build vs types), des flags activés par étapes, une baseline que vous ferez converger vers zéro, et une petite équipe commando responsable. Si vous dirigez l’ingénierie d’une startup US avec un patrimoine TypeScript de 300K–2M LOC, lisez la suite.

Pourquoi cette mise à niveau compte pour un CTO

  • Les sémantiques du compilateur évoluent à chaque majeure. Même si la sortie runtime est identique, un changement dans l’analyse des flots de contrôle ou le typage JSX peut invalider des milliers de lignes. Votre CI rouge n’est pas un bug ; c’est la nouvelle réalité.
  • L’écosystème bouge avec vous. Frameworks, linters et paquets de types conditionnent la prise en charge par majeure. Si votre produit dépend de Next.js, Vite ou d’un design system qui fige l’outillage, vous serez tôt ou tard forcé d’avancer.
  • Le coût est réel mais borné. En pratique, nous constatons 0,5–1,5 semaine ETP par 100K LOC pour migrer un codebase sain avec une chaîne mature. Sur un monorepo d’1 M LOC, cela représente 1–3 semaines focalisées pour un petit pod plateforme.

Ce qui casse les équipes (indépendamment des détails de version)

  • Dérive de résolution de modules. Les paquets ESM/CJS en double, les maps "exports" et les différents modes moduleResolution déstabilisent les path aliases et les imports profonds. Votre config Jest/Vitest est souvent en retard sur votre config de build.
  • Options par défaut plus strictes ou inférence resserrée. Un vérificateur plus intelligent révèle des schémas risqués sur lesquels vous comptiez. 70–80 % des erreurs se regroupent généralement autour de quelques anti‑patterns.
  • Décorateurs et métadonnées. Si vous utilisez des ORM ou des frameworks avec décorateurs, vous êtes tributaire de leur cadence de sortie. Un léger retard peut bloquer une montée de version majeure du compilateur.
  • Couplage build vs types. Babel/esbuild/swc transpilent sans problème tandis que tsc échoue, ou l’inverse. Les équipes qui lient l’emit à la vérification de types doivent en découpler un pour débloquer l’autre.

La méta‑stratégie : deux pipelines, une baseline

Arrêtez de mélanger compilation et vérification de types. Votre pipeline runtime ne doit pas dépendre de l’humeur du vérificateur aujourd’hui.

  1. Dissociez build et types. Utilisez esbuild ou swc pour transpiler/packager. Exécutez tsc --noEmit pour la vérification de types. Gardez-les dans des jobs CI séparés avec cache et responsables indépendants.
  2. Ajoutez une étape « duo » TypeScript. Pendant quelques semaines, exécutez TypeScript 6 (actuel) et TypeScript 7 (cible) en parallèle dans la CI. TS6 reste bloquant. TS7 alimente un fichier de baseline qui doit décroître chaque jour.
  3. Établissez une baseline. Première exécution TS7 : capturez toutes les erreurs dans une baseline. N’accordez pas de dérogation globale. Créez des suppressions ciblées, relues, dans le code, avec une date d’expiration claire.

Hygiène pré‑migration (un sprint de travail ennuyeux qui s’amortit)

1) Faites l’inventaire de votre surface

  • Taille et structure : Comptez les LOC par package. Identifiez les 20 principaux packages par temps de compilation et densité d’erreurs une fois un build d’essai basculé. Il vous faut une carte avant de choisir vos batailles.
  • Trous de typage : Mesurez le taux d’usage de any. Si plus de 10–15 % des fichiers reposent sur des any implicites ou des as any agressifs, alignez les attentes : la migration fera ressortir beaucoup de rouge.
  • Empreinte du runner de tests : Combien de configs Jest ? Du ts-jest historique ? Si oui, prévoyez de migrer vers ts-jest avec transpilation isolée ou de passer à Vitest avec esbuild/swc pour éviter le couplage aux types.

2) Démêlez la résolution des modules

  • Standardisez sur la sémantique NodeNext. Les packages ESM/CJS mixtes sont un fait. Alignez tsconfig, le runtime Node, le bundler et le runner de tests sur la même histoire de modules. Ne livrez pas en prod un graphe de modules différent de celui que vous testez en local.
  • Tuez l’habitude des imports profonds. Bannissez les imports du type lib/something/internal entre packages. Utilisez des points d’entrée publics. Ajoutez une règle de linter pour détecter les imports profonds.
  • Rendez les alias de chemins honnêtes. Si tsconfig.paths mappe @app/foo vers src/foo, votre bundler et votre runner de tests doivent le résoudre de manière identique. La dérive ici crée des effets à distance difficiles à diagnostiquer lors des montées de version.

3) Adoptez les project references

Si vous avez un monorepo et que vous n’utilisez pas les project references de TypeScript, vous laissez 20–40 % de temps de compilation sur la table et vous disposez de moins de leviers pendant les upgrades. Les builds composites vous donnent des frontières par package et de la mise en cache rendant les cycles rouge/vert supportables.

  • Rendez les packages composites. Ajoutez composite: true et des frontières. Générez des .d.ts pour les leaf packages qui exportent des types.
  • Utilisez tsc -b en local et en CI. Le mode build détecte les cycles et contrôle l’invalidation. Sur un repo d’1 M LOC, nous voyons régulièrement le build à froid passer de 15–20 minutes à 6–10 avec les references et le cache.

4) Échelonnez des flags plus stricts avant le saut

Vous pouvez réduire la surprise en activant des contrôles plus stricts avec le compilateur actuel. Choisissez 2–3 flags que votre codebase peut tolérer et montez‑les avec des dérogations :

  • noUncheckedIndexedAccess pour faire remonter les accès tableau/objet non sûrs.
  • exactOptionalPropertyTypes pour ne plus confondre « absent » et « undefined ».
  • useUnknownInCatchVariables pour en finir avec les any paresseux dans les gestionnaires d’erreurs.

Activez‑les dans une poignée de packages avec un quota clair de corrections par équipe et par semaine. Le gain : une surface d’impact plus faible quand vous basculerez sur TS7.

La migration, pas à pas

Phase 0 (1–2 jours) : Geler et forker

  • Verrouillez l’environnement. Figez votre toolchain : version de Node, gestionnaire de paquets, bundler, runner de tests et paquets de types. Monter le compilateur tout en changeant Node ou Jest est la recette de régressions d’une semaine.
  • Forkez le compilateur. Ajoutez TypeScript 7 comme dépendance de dev séparée dans un job CI parallèle. Gardez TypeScript 6 comme vérificateur bloquant pour main.
  • Créez la baseline d’erreurs TS7. La première exécution complète de tsc --noEmit avec TS7 génère votre « dette ». Stockez‑la. Publiez‑la quotidiennement. Votre pod plateforme est responsable de la réduire à zéro.

Phase 1 (3–5 jours) : Réduire les 80 %

  • Regroupez les erreurs. Sur un repo de 500K–1M LOC, attendez‑vous à 1 000–5 000 erreurs initiales. 70–80 % sont des motifs répétitifs. Exemples classiques : garde satisfies manquante pour des littéraux d’objet, types conditionnels fragiles exposés par un resserrement du narrowing, et fuite d’alias de chemin jusqu’au runtime.
  • Automatisez les correctifs ennuyeux. Utilisez ts-morph ou des codemods pour appliquer la même refactorisation locale à des dizaines de packages. Ajoutez des règles de lint pour que l’anti‑pattern ne repousse pas.
  • Publiez la burn‑down. Traitez cela comme le suivi d’incidents : un graphique quotidien pour l’organisation. Vous voulez une ligne droite vers zéro d’ici la fin de la première semaine.

Phase 2 (2–4 jours) : Les 20 % têtus

  • Décorateurs et retard de l’écosystème. Si vous dépendez de plugins de décorateurs ou de réflexion d’un framework, coordonnez avec cette équipe ou le vendeur. Fixez‑vous sur une version connue‑bonne. Si le support est en attente, isolez ces packages derrière une frontière temporaire et n’accordez des dérogations que là.
  • Surfaces d’API publiques. Les bibliothèques qui publient des .d.ts demandent une attention particulière. Exécutez API Extractor pour vérifier que l’API publique reste stable après le changement de compilateur.
  • Parité du runner de tests. Assurez‑vous que Jest/Vitest résolvent ESM/CJS comme votre bundler et Node. Les décalages ici provoquent des échecs « marche en dev, casse en CI » qui ressemblent à des régressions TypeScript mais sont en réalité des bugs de modules.

Phase 3 (1 jour) : Inverser le verrou

  • Faites de TS7 le bloqueur. Quand la baseline atteint zéro et que la branche principale reste verte 48 heures avec le job duo, faites de TS7 le vérificateur bloquant. Gardez le job TS6 une semaine comme canari, puis retirez‑le.
  • Dégelez. Dépointez prudemment l’écosystème. Mettez à jour une classe d’outils par jour (linters, runners de tests, puis bundlers) pour éviter les explosions combinatoires.

Garde‑fous de performance et de fiabilité

  • Cachez tout. Mettez en cache à distance la sortie esbuild/swc et les artefacts de tsc -b. Sur un repo d’1 M LOC, le temps CI baisse de 30–50 % avec des clés de cache fiables.
  • Laissez skipLibCheck activé en CI sauf si vous publiez des bibliothèques. Ajoutez alors un job « libcheck » nocturne séparé pour détecter la dérive des paquets de types sans taxer chaque PR.
  • Mesurez le delta. Suivez le temps de build et la taille des bundles avant/après. Pour un repo sain, attendez‑vous à un temps mur tsc --noEmit dans ±10 % et aucun changement significatif au runtime. Si vous voyez de plus fortes régressions, c’est généralement une nouvelle explosion de récursion au niveau des types dans un seul package — trouvez‑la et simplifiez.

Organisation et modèle de coût

Vous n’avez pas besoin d’une armée. Il vous faut un petit pod capable d’apporter rapidement des changements peu glamour et de se coordonner sereinement.

  • Équipe : 2–4 ingénieurs seniors plateforme/outillage avec l’autorité pour toucher à tous les packages. Ajoutez un représentant de votre plus grande équipe produit pour le contexte.
  • Temps : 1–3 semaines pour 1 M LOC. Un monorepo sain avec project references et une pile test/build moderne se situe plutôt vers une semaine. Si vous n’avez pas les references et transpilez encore via tsc, prévoyez trois semaines.
  • Le recouvrement horaire compte. Si vous utilisez un pod nearshore au Brazil, vous aurez 6–8 heures de recouvrement US pour la burn‑down quotidienne et la gestion des dépendances, et 20–30 % de coût en moins pour la monotonie des codemods et des diff de config.

Ce qui va casser (et comment le désamorcer)

  • Jest avec ts-jest dans des repos ESM. Si vous compilez encore les types via Jest, passez à l’ESM natif avec Babel/esbuild pour la transformation et laissez les types à tsc --noEmit. Ou adoptez Vitest quand c’est possible.
  • Alias de chemins au runtime. Des imports comme @app/foo fonctionnent dans l’éditeur mais explosent dans Node. Résolvez‑les dans votre bundler/tests et générez de vrais imports relatifs dans le code émis. Un simple contrôle pré‑vol « aucun alias non résolu » fait gagner des heures.
  • Fichiers « barrel » et dépendances circulaires. Le vérificateur devient meilleur pour détecter des cycles subtils. Exécutez dependency-cruiser et cassez explicitement les cycles avant de basculer.
  • Fuites de types ambiants depuis les tests. Des déclarations globales se répandent dans la source dans des monorepos avec des globs approximatifs. Cadrez votre champ types dans tsconfig et séparez les types de test des types de prod.

Un pattern de résultat réel (anonymisé)

Dans une scale‑up fintech avec ~700K LOC TypeScript réparties sur 86 packages, nous avons mené une montée de version majeure TS sur deux semaines sans gel des fonctionnalités :

  • Semaine 0 : Ajout de project references à 18 packages en retard ; migration des tests hors de ts‑node ; standardisation de la résolution des modules.
  • Semaine 1 : Double vérificateur en CI (ancien bloquant, nouveau en baseline). 2 840 erreurs TS7 initiales ; 2 250 corrigées avec trois codemods et deux règles de lint. Temps CI passé de 28 à 17 minutes après references et cache distant.
  • Semaine 2 : Package ORM riche en décorateurs bloqué 3 jours en attendant un patch du vendeur ; isolé et dérogation locale. Baseline à zéro ; promotion de TS7 en bloqueur. Aucun incident en production. Sentiment développeurs : moins d’erreurs fantômes d’IntelliSense.

Quand vous ne devriez pas encore migrer

  • Votre framework cœur n’est pas prêt. Si votre app dépend d’un framework qui n’a pas annoncé son support, attendez. Ne forkez pas votre propre plugin de décorateurs à moins d’accepter de l’assumer.
  • Vous changez aussi de Node, bundler ou runner de tests. Scindez les changements. Une dimension à la fois. Sinon, la fiabilité n’est pas de votre côté.
  • Votre dette de types est catastrophique. Si l’implicit any est partout et que vos tests dépendent de ts‑node, faites d’abord un sprint de durcissement. Vous en économiserez deux sur l’upgrade.

Gouvernance : consolider les gains

  • Ajoutez un rituel trimestriel de montée de version du compilateur. Mettez‑le sur la roadmap plateforme. Les petits pas battent les sauts de plusieurs années.
  • Gardez le job duo. Vous n’avez pas besoin de deux majeures en permanence, mais refaites le pattern duo à chaque mineure/majeure. C’est une assurance peu coûteuse.
  • Suivez la santé des types comme un KPI. Taux d’any, nombre de suppressions, densité d’erreurs par package sont des indicateurs avancés de la douleur d’une future montée de version.

Si vous voulez de l’aide

Nos pods nearshore basés au Brazil font cela toute l’année : démêler la résolution des modules, déployer les project references à l’échelle d’un monorepo, supprimer par codemods les patterns fragiles, et garder la CI verte pendant que vous livrez. Vous avez un recouvrement US (6–8 heures), des checklists éprouvées, et une équipe qui a déjà cassé (et réparé) ces choses.

À retenir

  • Dissociez build et types ; utilisez esbuild/swc pour l’emit et tsc --noEmit pour la vérification.
  • Faites tourner un double vérificateur en CI (actuel en gate, TS7 en baseline) pendant 1–2 semaines.
  • Adoptez les project references et le cache distant pour dompter les temps de compilation.
  • Standardisez une seule histoire de modules (NodeNext) entre runtime, bundler et tests.
  • Corrigez les 70–80 % d’erreurs répétitives avec des codemods ; isolez les 20 % récalcitrants.
  • Allouez 0,5–1,5 semaine ETP par 100K LOC ; privilégiez un petit pod plateforme, pas une armée.
  • Ne migrez pas en même temps que des changements majeurs de Node/bundler/tests ; une dimension à la fois.
  • Considérez la santé des types comme une métrique produit pour que la prochaine montée de version soit ennuyeuse.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation