TypeScript et ses tsconfig

Qui vérifie les types, avec quelle configuration : le tsconfig commun et ses options, l'héritage par extends, les alias de chemins, la contrainte qu'impose un fichier compilé par deux configurations, et les fichiers de déclaration.

Précédemment. Le dépôt réunit quatre paquets, qui se rejoignent par des alias plutôt que par leurs noms. Ces alias, comme toutes les options de TypeScript, vivent dans les fichiers tsconfig.

Un fichier tsconfig.json dit à TypeScript quels fichiers lire et avec quelles règles. Le projet en a six. Ce chapitre explique ce que chacun règle, et surtout comment ils s'articulent : c'est de là que viennent les erreurs les plus déroutantes, celles où un import marche dans l'éditeur et casse au typecheck.

TypeScript vérifie, il ne compile pas#

Dans ce projet, tsc (le compilateur TypeScript) ne produit aucun fichier JavaScript. Next.js transforme lui-même le TypeScript en JavaScript, très vite, en retirant simplement les types. Pendant pnpm dev, il ne les vérifie pas : une erreur de type n'empêche pas la page de s'afficher. La vérification est le travail de tsc --noEmit, lancé par pnpm typecheck et par pnpm check. next build la refait aussi avant de construire : une erreur de type oubliée fait échouer la construction de l'image au déploiement.

Fichier

Lu par

Pour

tsconfig.base.json
Tous les autres (extends)
Les options communes.
apps/web/tsconfig.json
pnpm typecheck, next build, l’éditeur
L’application, et tout ce qu’elle importe des paquets.
packages/backend/tsconfig.json
pnpm --filter backend typecheck, l’éditeur
Le backend seul.
packages/contracts/tsconfig.json
L’éditeur
Les contrats (non vérifiés par eux-mêmes, voir plus bas).
packages/finder-core/tsconfig.json
pnpm --filter finder-core typecheck, l’éditeur
Le moteur du Finder.
tsconfig.json (racine)
pnpm typecheck, l’éditeur
prisma.config.ts et les scripts TypeScript de prisma/.

L'éditeur (VS Code) prend, pour chaque fichier ouvert, le tsconfig.json le plus proche en remontant les dossiers. C'est pourquoi chaque paquet a le sien : un fichier du backend ouvert dans l'éditeur est vérifié avec la configuration du backend.

La configuration commune#

tsconfig.base.json · fichier complet
json
{  "compilerOptions": {    "target": "ES2023",    "lib": ["dom", "dom.iterable", "esnext"],    "allowJs": true,    "skipLibCheck": true,    "strict": true,    // "noEmit": true,    "esModuleInterop": true,    "module": "esnext",    "moduleResolution": "bundler",    "resolveJsonModule": true,    "isolatedModules": true,    "jsx": "react-jsx",    "incremental": true,    "plugins": [{ "name": "next" }]  }}

Option

Ce qu’elle règle

strict
Le mode strict : null et undefined doivent être traités, les paramètres typés, etc. L’option qui rend TypeScript utile.
target, lib
La version de JavaScript visée et les API connues (le DOM du navigateur, les nouveautés du langage).
module, moduleResolution: bundler
Des imports ES (import / export), résolus comme le fait un bundler : sans extension de fichier, avec les exports des package.json.
isolatedModules
Chaque fichier doit pouvoir être traduit seul, comme le fait Next.js ; interdit les quelques constructions qui exigent de connaître les autres fichiers.
jsx: react-jsx
Le JSX est compris sans import React en tête de chaque fichier.
skipLibCheck
Ne vérifie pas les fichiers de déclaration des bibliothèques (node_modules) : plus rapide, et leurs erreurs ne sont pas les nôtres.
allowJs, resolveJsonModule
Accepte les fichiers .js et l’import de fichiers .json.
incremental
Garde le résultat de la vérification précédente pour aller plus vite.
plugins: next
Ajoute à l’éditeur les vérifications propres à Next.js (directives use client, exports d’une page).

Un tsconfig est du JSON avec commentaires : // "noEmit": true, est une ligne désactivée, pas une erreur. Les virgules finales y sont tolérées aussi.

L'héritage : extends#

Chaque paquet part de la configuration commune et n'écrit que ce qui le distingue :

apps/web/tsconfig.json · extrait
json
{  "extends": "../../tsconfig.base.json",  "compilerOptions": {    "moduleResolution": "bundler",    "types": ["mdx"],

extends recopie toutes les options du fichier désigné, puis celles écrites ici s'y ajoutent ou les remplacent. Une option changée dans tsconfig.base.json vaut donc pour tous les paquets à la fois.

Les alias : paths#

apps/web/tsconfig.json · extrait
json
"paths": {  "@/*": ["./src/*"],  // indispensable pour shadcn/ui  "@web/*": ["./src/*"],  "@app/*": ["./src/app/*"],  "@contracts/*": ["../../packages/contracts/src/*"],  "@backend/*": ["../../packages/backend/src/*"],  "@features/*": ["./src/features/*"],  "@components/*": ["./src/components/*"],  "@hooks/*": ["./src/hooks/*"],  "@lib/*": ["./src/lib/*"],  "@styles/*": ["./src/styles/*"],  "@types/*": ["./src/types/*"],  "@trpc/*": ["./src/core/trpc/*"],  "@config/*": ["../../packages/config/*"],  "@finder-core/*": ["../../packages/finder-core/src/*"]}

Chaque ligne associe un préfixe d'import à un dossier, relatif au tsconfig qui la déclare : @features/admin/… devient apps/web/src/features/admin/…, @backend/prisma devient packages/backend/src/prisma. Next.js lit ces mêmes alias pour construire l'application ; il n'y a pas de seconde liste à tenir.

Deux alias désignent le même dossier, @/ et @web/. @/ est celui qu'attendent les composants générés par shadcn/ui (le commentaire le rappelle) ; @web/ est l'usage du projet. Ils coexistent, mais un nouvel import devrait utiliser le plus parlant des alias spécialisés (@features/…, @lib/…).

Un fichier, deux configurations#

Les paquets ne sont pas compilés à part : quand une page importe @backend/modules/tickets/router, ce fichier du backend est lu et vérifié avec la configuration de apps/web. Et lui-même importe @backend/prisma, @contracts/…. Ces alias doivent donc exister dans le tsconfig du web, pas seulement dans celui du backend :

packages/backend/tsconfig.json
· fichier complet
{  "extends": "../../tsconfig.base.json",  "compilerOptions": {    // "outDir": "dist",    "paths": {      "@backend/*": ["./src/*"],      "@contracts/*": ["../contracts/src/*"],    },    "moduleResolution": "bundler",    "allowImportingTsExtensions": false  },  "include": ["src", "../contracts/src/**/*.ts", "../contracts/src/slug/slug.schema.ts"]}

C'est la contrainte principale de ce montage : tout alias employé par un fichier doit être déclaré, sous le même nom et vers le même dossier, dans chaque tsconfig qui finit par compiler ce fichier. @contracts/* figure ainsi dans les quatre configurations. Un alias ajouté au seul backend fonctionnerait dans l'éditeur, ouvert sur un fichier du backend, puis casserait pnpm typecheck dès qu'une page importerait ce fichier.

Le symptôme typique : « Cannot find module '@backend/…' » au typecheck du web, alors que l'éditeur ne signale rien sur le fichier du backend. Le remède est d'ajouter l'alias au tsconfig qui échoue, pas de le retirer du code.

Ce qui n'est pas vérifié à part#

packages/contracts/tsconfig.json
· fichier complet
{  "extends": "../../tsconfig.base.json",  "compilerOptions": {    "typeRoots": ["node"],    "noCheck": true,    "paths": {      "@contracts/*": ["./src/*"]    }  },  "include": ["src", "**/*.ts"]}

noCheck: true désactive la vérification de ce paquet par lui-même. Les contrats ne sont pas vérifiés pour autant à l'aveugle : le web et le backend les incluent et les vérifient, avec leurs propres règles, comme le montre la liste include du web :

Ce que vérifie le web
JSON
apps/web/tsconfig.json
empty code block

Cette liste a longtemps porté des traces d'un ancien rangement : app, components, features… à la racine de apps/web, où ils n'existent pas (tout est sous src/). Elles étaient sans effet, et le nettoyage l'a vérifié : tsc --listFilesOnly liste exactement les mêmes fichiers avant et après leur retrait (partie « Qualité »). exclude écarte node_modules, et .next-e2e, la construction des tests de bout en bout : **/*.ts y trouverait les types que Next génère, d’une construction peut-être ancienne.

Les fichiers de déclaration#

Un fichier .d.ts ne contient que des types : il décrit à TypeScript ce qu'il ne peut pas deviner.

Fichier

Ce qu’il déclare

apps/web/next-env.d.ts
Les types de Next.js (images, routes typées). Généré par Next : ne pas le modifier.
apps/web/src/global.d.ts
Qu’un import de fichier .css, .scss ou .sass est accepté.

Les pages de cette documentation sont des fichiers .mdx, mais aucun n'est importé par le code : elles sont lues sur le disque et compilées à la demande (chapitre suivant). Aucune déclaration n'est donc nécessaire pour eux.

La configuration de la racine#

La configuration de la racine
TS
tsconfig.json
// ⚠ Fichier introuvable : tsconfig.json// Il a été déplacé ou supprimé depuis la rédaction de ce chapitre.

Elle couvre prisma.config.ts, les scripts TypeScript de prisma/, et les tests de bout en bout (playwright.config.ts, e2e/) : ils ne font partie d'aucun paquet, et sans elle l'éditeur les vérifierait avec des options par défaut. Longtemps lue par l'éditeur seul, elle est aussi vérifiée par pnpm typecheck (tsc -p tsconfig.json) : un fichier que rien ne vérifie finit par contenir une erreur que personne ne voit.

Récapitulatif#

Question

Réponse du projet

Qui transforme le TypeScript en JavaScript ?
Next.js, sans vérifier les types ; tsc ne fait que vérifier (noEmit).
Où sont les options communes ?
tsconfig.base.json, repris par chaque paquet avec extends.
Comment fonctionne un alias ?
paths associe un préfixe à un dossier, relatif au tsconfig qui le déclare ; Next.js lit les mêmes.
Pourquoi le même alias est-il déclaré plusieurs fois ?
Un fichier du backend est aussi compilé par la configuration du web : chaque tsconfig qui le compile doit connaître ses alias.
Que sont les .d.ts ?
Des déclarations de types pour ce que TypeScript ne peut pas deviner (fichiers .css, types de Next).

À suivre#

TypeScript vérifie le code ; Next.js le construit. Sa propre configuration décide de la forme du build, de ce qu'il embarque pour Docker, et comment il traite les pages MDX de cette documentation.

Chapitre suivant : Next.js et next.config.ts. Le build autonome pour l'image Docker, le React Compiler, les redirections, MDX, et ce que chaque réglage change.

Sur cette page