TypeScript en pratique - 13 - Migration JS vers TS progressive

Comment migrer un projet JavaScript vers TypeScript fichier par fichier, en partant de allowJs jusqu'a strict mode complet.

13 - Migration JS vers TS progressive

Ce que tu vas apprendre

  • Comment démarrer une migration avec allowJs et checkJs
  • La stratégie fichier par fichier pour éviter le big bang
  • Le chemin any -> unknown -> types precis
  • Comment gerer les dépendances non typees pendant la transition
  • Comment migrer les tests
  • Comment mesurer la progression
  • Des attentes realistes sur la duree

Prerequisites

Avoir lu l'article sur les monorepos et le partage de types.


Pourquoi ne pas tout migrer d'un coup

J'ai vu des équipes tenter la migration big bang : on renomme tous les .js en .ts un vendredi, on corrige les 2000 erreurs le weekend, et on merge le lundi.

Ca ne marche jamais. Les erreurs en cascadent, les merges sont impossibles, et l'équipe passe plus de temps a se battre avec le compilateur qu'a livrer des features. La migration progressive est la seule approche qui fonctionne sur un vrai projet.

Étape 1 : ajouter TypeScript sans rien casser

Installe TypeScript et créé un tsconfig minimal :

bashpnpm add -D typescript
jsonc// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowJs": true,        // accepte les fichiers .js
    "checkJs": false,        // pas de verification sur les .js (pour l'instant)
    "outDir": "./dist",
    "strict": false,         // on active strict plus tard
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Avec allowJs: true, TypeScript compile les fichiers .js et .ts ensemble. Ton projet fonctionne exactement comme avant.

Étape 2 : activer checkJs progressivement

Au lieu d'activer checkJs globalement, active-le fichier par fichier avec un commentaire en haut du fichier :

javascript// @ts-check

/** @type {string} */
const apiUrl = process.env.API_URL || "http://localhost:3000"

/**
 * @param {string} userId
 * @returns {Promise<{id: string, name: string}>}
 */
async function getUser(userId) {
  const res = await fetch(`${apiUrl}/users/${userId}`)
  return res.json()
}

@ts-check active la vérification TypeScript sur un fichier JavaScript. Les annotations JSDoc donnent les types. C'est un moyen de tester les eaux sans renommer le fichier.

Pour le contraire — désactiver la vérification sur un fichier spécifique quand checkJs: true est global :

javascript// @ts-nocheck
// Ce fichier est trop complexe pour etre migre maintenant

Étape 3 : renommer les fichiers un par un

La stratégie : commence par les fichiers les plus simples (utilitaires, constantes, types) et progresse vers les fichiers les plus complexes (routes, contrôleurs, composants).

bash# Renommer un fichier
mv src/utils/format.js src/utils/format.ts

Apres le renommage, corrige les erreurs TypeScript dans ce fichier. Un fichier a la fois. Un commit par fichier (ou par lot de fichiers lies).

Ordre recommande :

  1. Constantes et config : types simples, peu de dépendances
  2. Types et interfaces : créer un fichier types.ts avec les entités du domaine
  3. Utilitaires : fonctions pures, faciles a typer
  4. Services / data access : requêtes API, acces base de donnees
  5. Composants / routes : le plus dépendant, a faire en dernier

Le chemin any -> unknown -> types precis

Quand tu renommes un fichier, le compilateur va se plaindre. La tentation est de mettre any partout pour que ca compile :

typescript// ❌ Etape 0 : any partout (compile mais inutile)
function processOrder(order: any): any {
  return order.items.map((item: any) => item.price * item.quantity)
}

C'est acceptable comme étape intermediaire, mais ne t'arrêté pas la. Le chemin :

typescript// Etape 1 : unknown force a verifier
function processOrder(order: unknown): number[] {
  if (!isOrder(order)) throw new Error("Invalid order")
  return order.items.map(item => item.price * item.quantity)
}

// Etape 2 : types precis
interface OrderItem {
  id: string
  price: number
  quantity: number
}

interface Order {
  id: string
  items: OrderItem[]
  status: "pending" | "confirmed" | "shipped"
}

function processOrder(order: Order): number[] {
  return order.items.map(item => item.price * item.quantity)
}

unknown est meilleur que any comme étape intermediaire parce qu'il force les verifications. any désactivé silencieusement le typage.

Déclarations ambiantes pour les deps non typees

Pendant la migration, certaines dépendances n'ont pas de types. Au lieu de les ignorer, créé des déclarations minimales :

typescript// src/types/ambient.d.ts

// Lib sans types — declaration minimale
declare module "old-csv-parser" {
  export function parse(input: string): string[][]
  export function stringify(data: string[][]): string
}

// Lib que tu ne veux pas typer maintenant
declare module "legacy-auth-lib"

// Fichiers non-JS
declare module "*.css" {
  const content: Record<string, string>
  export default content
}

Le fichier ambient.d.ts est un fourre-tout temporaire. Au fur et a mesure de la migration, tu remplaces ces déclarations par des @types/ ou des types precis. Voir l'article sur les déclaration files pour les détails.

Activer strict incrementalement

strict: true active plusieurs options d'un coup. Tu peux les activer une par une :

jsonc{
  "compilerOptions": {
    "strict": false,

    // Active ces options une a la fois, dans cet ordre
    "noImplicitAny": true,         // 1. Plus de 'any' implicite
    "strictNullChecks": true,      // 2. null/undefined explicites
    "strictFunctionTypes": true,   // 3. Contravariance des parametres
    "strictBindCallApply": true,   // 4. bind/call/apply types
    "strictPropertyInitialization": true, // 5. Props initialisees
    "noImplicitThis": true,        // 6. this explicite
    "alwaysStrict": true           // 7. "use strict" partout
  }
}

L'ordre est a peu pres du plus impactant au moins impactant. noImplicitAny et strictNullChecks revelent le plus d'erreurs.

Sur le projet le plus gros que j'ai migre, activer noImplicitAny a généré 400 erreurs. strictNullChecks en a ajoute 600. On les a corrigees en 3 semaines, quelques dizaines par jour.

Migrer les tests

Les tests sont souvent les derniers migres. Deux approches :

Migrer avec le code

Quand tu migres src/utils/format.js -> .ts, migre aussi test/utils/format.test.js -> .test.ts. L'avantage : les tests verifient que tes types sont corrects.

Garder les tests en JS

Si tes tests utilisent beaucoup de mocks et de donnees partielles, les migrer généré beaucoup d'erreurs de type. Tu peux garder les tests en .js avec allowJs: true et les migrer plus tard.

typescript// En JS, ce mock fonctionne
const mockUser = { name: "Alice" }
// En TS, l'erreur dit qu'il manque id, email, role, createdAt...

Pour les mocks partiels en TypeScript, utilise Partial ou une factory :

typescriptfunction createMockUser(overrides: Partial<User> = {}): User {
  return {
    id: "test-id",
    email: "test@test.com",
    name: "Test User",
    role: "user",
    createdAt: "2024-01-01T00:00:00Z",
    ...overrides
  }
}

// Dans les tests
const user = createMockUser({ name: "Alice" })

Ce pattern de factory resout 90% des problèmes de mocks en TypeScript.

Mesurer la progression

Compte les occurrences de any pour suivre la migration :

bash# Compter les 'any' explicites
grep -r ": any" src/ --include="*.ts" | wc -l

# Compter les fichiers JS restants
find src/ -name "*.js" | wc -l

# Compter les fichiers TS
find src/ -name "*.ts" | wc -l

Sur paltemps.fr, je suivais un tableau simple :

Semaine 1 : 120 .js / 30 .ts  / 45 any
Semaine 2 : 95 .js  / 55 .ts  / 38 any
Semaine 3 : 60 .js  / 90 .ts  / 22 any
Semaine 6 : 0 .js   / 150 .ts / 3 any

Les 3 derniers any etaient dans du code générique de sérialisation ou any est le bon type.

Attentes realistes

Quelques chiffres issus de mon experience et de la communauté :

Taille du projet Temps de migration Avec combien de personnes
< 10k lignes 1-2 semaines 1 personne
10k-50k lignes 1-2 mois 1-2 personnes
50k-200k lignes 3-6 mois 2-3 personnes
> 200k lignes 6-12 mois équipe

Ces chiffres supposent une migration progressive (pas de freeze des features). La migration se fait en parallèle du développement normal.

Le piège : commencer la migration, la laisser trainer, et se retrouver avec un projet moitie JS moitie TS pendant des annees. Fixe une deadline et tiens-la.

Checklist de migration

  1. Installer TypeScript, configurer allowJs: true, strict: false
  2. Creer types/ambient.d.ts pour les deps non typees
  3. Migrer les fichiers utilitaires et constantes
  4. Migrer les fichiers de services et data access
  5. Migrer les composants / routes
  6. Activer noImplicitAny
  7. Activer strictNullChecks
  8. Activer les autres options strict une a une
  9. Supprimer les any restants
  10. Activer strict: true et supprimer les options individuelles
  11. Migrer les tests

Résumé

  • allowJs: true permet de faire coexister JS et TS dans le meme projet
  • Migre fichier par fichier, des plus simples aux plus complexes
  • Utilise unknown au lieu de any comme type intermediaire
  • Active les options strict une a une, en commencant par noImplicitAny et strictNullChecks
  • Les déclarations ambiantes (declare module) comblent les trous pendant la transition
  • Mesure ta progression en comptant les fichiers JS restants et les occurrences de any

Article précédent : 12 - Monorepo et partage de types

Article suivant : 14 - Performance du compilateur

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.