13 - Migration JS vers TS progressive
Ce que tu vas apprendre
- Comment démarrer une migration avec
allowJsetcheckJs - 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 :
- Constantes et config : types simples, peu de dépendances
- Types et interfaces : créer un fichier
types.tsavec les entités du domaine - Utilitaires : fonctions pures, faciles a typer
- Services / data access : requêtes API, acces base de donnees
- 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
- Installer TypeScript, configurer
allowJs: true,strict: false - Creer
types/ambient.d.tspour les deps non typees - Migrer les fichiers utilitaires et constantes
- Migrer les fichiers de services et data access
- Migrer les composants / routes
- Activer
noImplicitAny - Activer
strictNullChecks - Activer les autres options strict une a une
- Supprimer les
anyrestants - Activer
strict: trueet supprimer les options individuelles - Migrer les tests
Résumé
allowJs: truepermet de faire coexister JS et TS dans le meme projet- Migre fichier par fichier, des plus simples aux plus complexes
- Utilise
unknownau lieu deanycomme type intermediaire - Active les options strict une a une, en commencant par
noImplicitAnyetstrictNullChecks - 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
- Migrating from JavaScript par Microsoft
- TypeScript strict mode par Microsoft
- Stripe's TypeScript Migration par Stripe Engineering