Développement

Comment migrer une base de code legacy vers TypeScript sans bloquer les releases

Comment migrer une base de code legacy vers TypeScript sans bloquer les releases

Transformer une base de code legacy en TypeScript sans bloquer les releases ressemble parfois à un numéro d’équilibriste : il faut avancer, sécuriser, et surtout ne pas casser la production. J’ai mené plusieurs migrations progressives ces dernières années, et j’aime partager les étapes concrètes qui m’ont permis de garder les équipes productives tout en augmentant progressivement la qualité du code.

Pourquoi migrer vers TypeScript progressivement ?

TypeScript apporte un filet de sécurité très utile : typage statique, meilleure autocomplétion, refactorings plus fiables. Mais dans un projet existant, une migration « tout ou rien » est rarement réaliste — elle bloque les releases, fatigue l’équipe et génère de la dette temporaire. À la place, je privilégie une approche incrémentale qui permet de livrer des fonctionnalités en continu tout en refactorant par petits pas.

Principes que j’applique systématiquement

  • Ne jamais bloquer le pipeline de release : chaque changement doit pouvoir être déployé indépendamment.
  • Itérer par petits lots : migrer une bibliothèque, un dossier ou un module à la fois.
  • Automatiser les vérifications : linters, compilations partielles, tests unitaires et E2E.
  • Conserver la compatibilité JavaScript pendant la transition : permettre aux fichiers .js et .ts de coexister.
  • Étape 0 — Préparation et audit

    Avant de toucher au code, je réalise un audit rapide : volume de fichiers JavaScript, complexité du build, dépendances problématiques, et tests existants. J’aime cartographier les « zones chaudes » : modules avec beaucoup de dépendances ou code fragile. Ces zones ne sont pas nécessairement les premières à migrer.

    Checklist d’audit :

  • Identifier les points d’entrée (bundler, server).
  • Lister les dépendances non typées (packages sans types @types).
  • Vérifier la couverture de tests unitaires et E2E.
  • Analyser la configuration CI/CD : qu’est-ce qui bloque si on ajoute la compilation TypeScript ?
  • Étape 1 — Installer TypeScript sans forcer la compilation totale

    Installer TypeScript et configurer tsconfig.json en mode permissif. Mon trick : commencer avec allowJs à true et checkJs à false, ainsi les fichiers .js continuent d’être exécutés et on peut ajouter progressivement des fichiers .ts.

  • npm install --save-dev typescript
  • tsconfig.json minimal recommandé :
  • compilerOptions{ "target": "ES2019", "module": "commonjs", "allowJs": true, "checkJs": false, "outDir": "./dist", "noEmit": false, "skipLibCheck": true }

    Objectif : que le build CI ne fasse pas échouer les pipelines dès l’ajout de TypeScript. Je fais tourner la compilation TypeScript localement mais sans l’intégrer comme gate strict en CI au début.

    Étape 2 — Ajouter des règles de lint progressivement

    Le linting est mon allié : il guide les bonnes pratiques sans imposer le typage immédiatement. J’ajoute ESLint et le plugin TypeScript mais je n’active les règles sévères qu’après avoir stabilisé la base.

  • Installer ESLint avec @typescript-eslint.
  • Activer d’abord les règles stylistiques et de sécurité basiques.
  • Planifier l’activation progressive des règles stricte (noImplicitAny, strictNullChecks) fichier par fichier.
  • Étape 3 — Migrer par domaine fonctionnel ou par package

    Plutôt que de convertir des fichiers au hasard, je choisis une zone logique — par exemple l’API client, puis les utilitaires, puis les composants UI. Pour un monorepo, migrer package par package est encore plus pratique : chaque package peut devenir un module TypeScript avec sa propre configuration.

  • Commencer par les modules faiblement couplés.
  • Privilégier les modules testés : les tests détecteront les régressions.
  • Documenter les interfaces publiques lors de la conversion (types, JSDoc).
  • Étape 4 — Utiliser JSDoc et checkJs comme transition

    Quand convertir tous les fichiers en .ts est coûteux, j’utilise JSDoc dans les .js et active checkJs progressivement. Cela offre des bénéfices du typage (détection d’erreurs) sans renommer les fichiers tout de suite.

  • Ajouter des annotations JSDoc aux fonctions critiques.
  • Activer checkJs sur un dossier à la fois pour découvrir les warnings.
  • Étape 5 — Mettre en place des builds de type incrémental et des tests automatisés

    Le secret pour ne pas bloquer les releases, c’est d’isoler la compilation TypeScript et d’en faire une étape non bloquante au début. J’ajoute un job CI qui compile mais ne bloque pas la merge. Une fois que la compilation est stable, j’augmente son importance.

  • Job CI A : lint + tests — obligatoire.
  • Job CI B : compilation TypeScript — en soft-fail d’abord, puis en hard-fail après stabilisation.
  • Exécuter les tests sur la sortie compilée pour éviter les divergences runtime.
  • Étape 6 — Gérer les types tiers et solutions pratiques

    Les dépendances sans types sont souvent le frein. Pour ces cas, j’utilise plusieurs tactiques :

  • Rechercher des packages @types sur DefinitelyTyped.
  • Créer des déclarations .d.ts minimalistes pour l’application.
  • Encapsuler les dépendances non typées derrière une petite façade typée.
  • Exemple : au lieu d’utiliser directement un utilitaire non-typé, je crée un fichier lib/wrapper.ts avec une signature claire. Ainsi le reste de l’appli gagne en sécurité sans devoir typiser la dépendance entière.

    Étape 7 — Stratégie de commit et branches

    Pour éviter le chaos, je recommande des PRs petites et ciblées. Chaque PR migrera un dossier, ajoutera des types et adaptera les tests.

  • Nommer les PRs : [migrate][ts] dossier/nom.
  • Regarder les diffs pour s’assurer que les modifications sont uniquement typage ou refactor non-fonctionnel.
  • Utiliser des revues pair-à-pair pour propager les patterns TypeScript dans l’équipe.
  • Étape 8 — Monter progressivement l’exigence de typage

    Une fois qu’une part significative du code est en TypeScript et stable, j’augmente les règles de tsconfig : activer noImplicitAny, strictNullChecks, et réduire skipLibCheck si possible. Je fais cela dossier par dossier.

  • Introduire des // @ts-expect-error temporaires plutôt que // @ts-ignore.
  • Transformer les exceptions temporaires en tickets techniques pour réparation.
  • Outils et patterns que j’utilise

  • TypeScript (évidemment).
  • ESLint + @typescript-eslint.
  • ts-node pour tests rapides et scripts.
  • Babel + TypeScript si l’on a besoin d’intégration avec un pipeline Babel existant.
  • ts-jest ou vitest pour les tests unitaires avec TypeScript.
  • Un dernier conseil pratique : mesurez le progrès. J’utilise des métriques simples — pourcentage de fichiers .ts, nombre d’erreurs TypeScript en CI, couverture de tests — pour suivre la migration et ajuster la cadence sans paniquer l’équipe.

    Vous devriez également consulter les actualités suivante :