Le scénario est classique. Tu bump TypeScript (RC ou 6.0), rien n’a bougé côté app, et pourtant ta CI passe au rouge. Localement, tu as même parfois l’impression que “ça marche”, jusqu’au moment où un build propre ou un package du monorepo part en sucette.
TypeScript 6 ne s’est pas réveillé un matin en décidant de casser ton projet. Il a surtout arrêté de deviner à ta place des trucs dangereux. Sauf que si ton repo vivait bien grâce à des defaults implicites, tu vas le sentir passer. Les trois pièges qui reviennent le plus : rootDir, types, et le fameux tsc foo.ts lancé “pour checker vite fait”.
Pourquoi ça casse “sans rien changer”
Beaucoup de configs TypeScript reposent sur des comportements historiques : des @types qui se chargent “par magie”, un rootDir déduit de tes fichiers inclus, et des habitudes de commande où tu passes un fichier à tsc en pensant utiliser ton tsconfig.json.
TypeScript 6 est plus strict sur ces points. Et franchement, c’est plutôt sain. Le problème, c’est que ce sont des changements qui touchent l’outillage, pas ton code. Donc tu te retrouves à déboguer une CI, un build de lib, un tsc --build ou un tool de test, alors que “tu voulais juste upgrader”.
Sponsorisé par Le Scribouillard
Besoin de contenu optimisé SEO ?
Utilisez la meilleure plateforme française de création de contenu assistée par IA ! Et générez des articles pour moins de 1€ !
Piège n°1 : rootDir par défaut devient le dossier du tsconfig.json
Le piège le plus violent en monorepo, ou dans un projet où tu compiles plusieurs dossiers, c’est le nouveau comportement par défaut de rootDir. En gros, si tu ne le fixes pas explicitement, TypeScript 6 considère que ton rootDir est le dossier où se trouve le tsconfig.json.
Concrètement, ça peut te refaire l’arborescence de sortie dans outDir, changer les chemins des .d.ts, ou déclencher des erreurs parce que TypeScript voit des fichiers “hors root”. Et si tu as des libs qui sortent du JS + declarations, tu peux te retrouver avec un packaging qui n’a plus du tout la tête attendue par ton bundler ou par Node.
Le fix est bête, mais il faut le faire volontairement : décide où est ta racine de sources. Dans 90% des projets frontend, c’est src. Dans une lib, pareil. Dans un monorepo, c’est presque toujours packages/xxx/src au niveau de chaque package, pas la racine du repo. L’objectif est simple : arrêter de laisser TypeScript “inventer” ta structure de build.
Autre point qui pique : si tu as un include trop large (genre "**/*") ou un mélange src + scripts + tests, le nouveau rootDir par défaut transforme vite ton output en mille-feuille. Là, ce n’est pas TypeScript 6 le problème, c’est que ton tsconfig n’a jamais dit clairement “ceci est du code compilé” vs “ceci est du tooling”. Sépare, ou exclue.
Piège n°2 : types par défaut passe à [] (fin des @types “magiques”)
Celui-là est sournois parce qu’il fait tomber des erreurs qui n’ont rien à voir avec ton dernier commit. Du jour au lendemain, TypeScript ne charge plus automatiquement tous les types présents dans node_modules/@types. Le default devient [].
Si tu t’appuyais sur des globals injectés par @types/node, @types/jest, vitest/globals, ou même des types transitifs que tu n’avais jamais déclarés, tu peux te retrouver avec des Cannot find name 'process', des describe inconnus, ou des fichiers de test qui deviennent “rouges” alors que ton app n’a pas bougé.
Le fix propre, c’est d’être explicite. Soit tu veux Node partout et tu l’annonces. Soit tu veux des globals uniquement dans les tests et tu le limites à un tsconfig dédié (ce que je préfère, parce que ça évite d’avoir des describe() valides dans src/ par accident).
Sur un projet Vite/React, un setup qui tient bien la route, c’est un tsconfig “app” et un tsconfig “tests”. Et dans le tsconfig de tests, tu déclares les types que tu veux. Ça enlève une classe entière de bugs “ça compile chez moi” parce que quelqu’un a installé un @types global qui pollue tout le repo.
Piège n°3 : tsc foo.ts devient une erreur si un tsconfig.json existe
Je l’ai vu plein de fois : tu veux vérifier un fichier vite fait, tu lances tsc src/someFile.ts, et tu t’attends à ce que ça respecte ton projet. Historiquement, ce n’était déjà pas super clair, parce que passer un fichier à tsc te met dans un mode “hors projet” et ignore une partie de la config.
TypeScript 6 rend ça plus explicite, et donc plus cassant : si tu es dans un dossier qui a un tsconfig.json, ce mode devient une erreur. L’idée est simple : arrêter les checks trompeurs où tu crois compiler “comme en CI”, alors que tu compiles en fait avec des options par défaut.
Le réflexe à prendre est net. Si tu veux compiler le projet, tu fais tsc -p . (ou tsc -p path/to/tsconfig.json). Si tu veux vraiment compiler un fichier isolé sans projet, tu le fais en assumant, avec l’option prévue pour ça. Et si c’était un script de CI ou un outil interne qui faisait tsc foo.ts, tu as trouvé ton break : il faut le remettre dans le chemin “projet”.
Des configs qui évitent les surprises (Vite/React, tests, monorepo)
Je te mets un exemple volontairement “sobriété maximale”. Le but n’est pas d’empiler les options, c’est de verrouiller les trois points qui cassent : rootDir, types, et des include propres.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"rootDir": "src",
"noEmit": true,
"types": [],
"strict": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["dist", "node_modules"]
}
Oui, types: [] fait bizarre au début. Mais c’est justement le point : tu sais exactement ce qui est disponible globalement dans l’app. Pour les tests, je fais un tsconfig.spec.json qui étend celui-ci et rajoute les globals de test (Vitest, Jest, Node selon ton cas). Et si tu es en monorepo, je préfère mille fois un tsconfig par package, avec rootDir: "src" dans chaque package, plutôt qu’un gros include à la racine qui finit toujours par compiler des trucs qui ne devraient pas l’être.
Si tu build des libs (donc pas noEmit), c’est encore plus important. Là, le trio qui doit être clair est : rootDir (sources), outDir (sortie), et les include qui n’embarquent pas les tests. TypeScript 6 te force juste à assumer ces choix, au lieu de laisser l’outil deviner.
Stratégie de migration sans big-bang (et sans “fixes sales”)
Mon conseil est très terre-à-terre : commence par rendre les erreurs déterministes. Mets rootDir explicitement dans les packages qui émettent du code, et nettoie les include trop larges. Rien que ça enlève les surprises de structure de build et les erreurs “fichier hors root”.
Ensuite, traite types comme une frontière. Si ton app a besoin de Node (scripts Vite, config, SSR), sépare ces fichiers via un tsconfig.node.json ou au moins via des types dédiés. Si tes tests ont des globals, ne les fais pas fuiter dans src/. C’est une petite discipline, mais en échange tu gagnes des builds reproductibles.
Et pour finir, chasse les usages de tsc foo.ts dans les scripts. Il y en a souvent dans des vieux package.json, des snippets de doc interne, ou des CI “quick check”. Remplace par tsc -p. C’est plus long à taper, mais tu compiles enfin la même chose partout. C’est exactement le genre de détail qui évite les vendredis soirs moches.
Mon avis (assumé) : TypeScript 6 te rend service, mais il te met face à tes implicites
Si tu as l’impression que TS6 “casse pour casser”, c’est souvent que ton projet vivait sur des zones grises. Le rootDir deviné, les @types chargés sans contrat, les commandes qui ne compilent pas ce que tu crois compiler… tout ça marche, jusqu’au jour où ça ne marche plus. Et ce jour-là, ça tombe en CI, pas dans ton IDE.
Le bon move est de transformer ces implicites en config explicite. C’est un petit coût maintenant, et un énorme gain de stabilité après. Surtout en monorepo, surtout avec plusieurs outils (Vite, Vitest/Jest, tsup, tsc build). TypeScript 6 est juste le moment où tu arrêtes de repousser ça.