Configuration de tsconfig pour un nouveau projet TypeScript

1. Aperçu

Le fichier tsconfig.json détermine comment TypeScript vérifie les types et compile le code. Une mauvaise configuration peut entraîner des erreurs d’import à l’exécution, ou faire que le compilateur rate de nombreuses erreurs de type.

Cet article construit un tsconfig.json pour un nouveau projet Node.js à partir de zéro. Nous allons activer les drapeaux de vérification stricte, choisir le bon système de module, puis séparer la configuration de build de la configuration de test. Après cette lecture, vous aurez une configuration prête à l’emploi et comprendrez chaque ligne.

2. Initialiser le projet

Créez un répertoire et installez TypeScript comme dépendance de développement :

mkdir order-service && cd order-service
npm init -y
npm install --save-dev typescript @types/node
npx tsc --init

La commande tsc --init génère un tsconfig.json avec beaucoup de lignes de commentaire. Ce fichier est utile pour la consultation, mais difficile à maintenir. Nous le remplacerons par une version concise, documentant chaque option.

Ensuite, déclarez que le projet utilise ES Module dans package.json :

{
  "name": "order-service",
  "type": "module",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc -p tsconfig.build.json",
    "start": "node dist/index.js"
  }
}

La ligne "type": "module" indique à Node.js que les fichiers .js sont en ESM. Ce choix affecte directement la façon dont TypeScript résout les modules, comme vous le verrez à la section 3.

3. Configuration minimale pour Node.js

Voici la configuration de base de tsconfig.json :

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "types": ["node"],

    "rootDir": ".",
    "outDir": "dist",
    "sourceMap": true,
    "declaration": false,

    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src", "tests"]
}

3.1. target, lib et types

  • target est la version JavaScript que le compilateur génère. ES2022 convient à Node.js 18 et versions ultérieures. Avec cette cible, async/await, optional chaining et les champs de classe sont préservés, pas déclassés.
  • lib déclare les API disponibles à l’exécution. Pas de "DOM", donc vous n’appellerez pas accidentellement document ou window dans le code serveur.
  • types: ["node"] charge uniquement les types de Node.js. Si vous ne le déclarez pas, TypeScript charge automatiquement tous les packages dans node_modules/@types. Cela peut entraîner des types globaux non désirés.

3.2. module et moduleResolution

Pour un projet s’exécutant directement sur Node.js, utilisez NodeNext pour les deux options. En ce mode, TypeScript simule correctement la façon dont Node.js résout les imports. Il lit les champs "type" et "exports" dans package.json, et exige que les imports relatifs aient une extension de fichier.

Supposons que vous ayez deux fichiers :

// src/order.ts
export interface Order {
  id: string;
  items: string[];
  note?: string;
}

export function totalItems(order: Order): number {
  return order.items.length;
}
// src/index.ts
import { totalItems } from "./order";

console.log(totalItems({ id: "A1", items: ["pho", "tra-da"] }));

L’exécution de npx tsc --noEmit signalera une erreur :

src/index.ts:1:28 - error TS2835: Relative import paths need explicit file extensions
in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'.
Did you mean './order.js'?

Vous devez le changer en import { totalItems } from "./order.js";. L’extension est .js et non .ts, car à l’exécution Node.js lit les fichiers compilés. Cette règle peut sembler étrange au premier abord, mais elle vous aide à éviter l’erreur ERR_MODULE_NOT_FOUND lors du déploiement.

3.3. rootDir et outDir

outDir est l’emplacement des fichiers .js après la compilation. rootDir détermine la structure des répertoires dans outDir. Dans le fichier de base, rootDir est "." car include contient à la fois src et tests. À la section 6, le fichier de configuration de build changera rootDir en src, pour que dist/index.js soit au bon endroit au lieu d’être à dist/src/index.js.

4. Mode strict et drapeaux de vérification supplémentaires

Le drapeau strict: true active un ensemble de vérifications, notamment strictNullChecks, noImplicitAny et strictFunctionTypes. Pour un nouveau projet, vous devez toujours activer ce drapeau. L’activer plus tard pour une grande base de code coûtera beaucoup plus de travail.

En plus de strict, vous devriez ajouter les drapeaux suivants :

{
  "compilerOptions": {
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Fusionnez ces lignes dans le bloc compilerOptions de votre fichier de base. Les deux premiers drapeaux méritent une explication plus détaillée.

4.1. noUncheckedIndexedAccess

Par défaut, TypeScript suppose que les éléments obtenus par index existent toujours. En réalité, ce n’est pas le cas :

import type { Order } from "./order.js";

const ordersById: Record<string, Order> = {};

const order = ordersById["A1"];
console.log(order.id);

Quand ce drapeau est activé, le type de order est Order | undefined, et le compilateur signale :

error TS18048: 'order' is possibly 'undefined'.

Vous êtes obligé de vérifier avant utilisation : if (order) { console.log(order.id); }. Si vous désactivez le drapeau, le code ci-dessus se compilera toujours mais lèvera une TypeError à l’exécution.

4.2. exactOptionalPropertyTypes

Ce drapeau distingue « pas de propriété » de « propriété ayant la valeur undefined » :

import type { Order } from "./order.js";

const order: Order = { id: "A2", items: [], note: undefined };
error TS2375: Type '{ id: string; items: never[]; note: undefined; }' is not assignable
to type 'Order' with 'exactOptionalPropertyTypes: true'.

Cette distinction est importante quand vous utilisez Object.keys, l’opérateur in ou sérialisez en JSON. Si vous voulez vraiment permettre undefined, déclarez-le explicitement : note?: string | undefined.

Note : exactOptionalPropertyTypes est parfois en conflit avec les types des bibliothèques tierces, en particulier les bibliothèques de gestion de formulaires et les ORM. Si ce drapeau cause trop d’erreurs hors de votre contrôle, désactivez-le. En revanche, vous devriez conserver noUncheckedIndexedAccess.

5. Interaction entre les modules

Ajoutez ces deux drapeaux à compilerOptions :

{
  "compilerOptions": {
    "verbatimModuleSyntax": true,
    "isolatedModules": true
  }
}

verbatimModuleSyntax a une règle simple : tout import sans le mot type est conservé dans la sortie, tandis que tout import avec type est supprimé. De cette façon, le résultat de la compilation est prévisible. En contrepartie, vous devez marquer clairement les imports qui ne sont utilisés que pour les types :

// Incorrect : Order est juste une interface
import { Order } from "./order.js";
error TS1484: 'Order' is a type and must be imported using a type-only import
when 'verbatimModuleSyntax' is enabled.
// Correct
import type { Order } from "./order.js";
import { totalItems } from "./order.js";

isolatedModules garantit que chaque fichier peut être compilé indépendamment. Des outils comme esbuild, swc ou Vitest traitent chaque fichier individuellement, donc ce drapeau vous aide à détecter les erreurs avant qu’elles ne posent des problèmes.

Vous avez peut-être vu esModuleInterop dans de nombreux anciens guides. Avec module: NodeNext, ce drapeau est activé par défaut, il n’est donc pas nécessaire de le déclarer.

6. Séparer la configuration typecheck et build

Le fichier tsconfig.json original sert l’éditeur et la commande typecheck. Il inclut les tests. Lors de la compilation, vous ne voulez pas inclure les fichiers de test dans dist. Par conséquent, créez un tsconfig.build.json supplémentaire :

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist",
    "noEmit": false
  },
  "include": ["src"],
  "exclude": ["**/*.test.ts"]
}

extends hérite de tous les compilerOptions du fichier de base. Dans le fichier enfant, include et exclude remplacent complètement les valeurs du fichier parent au lieu de les fusionner. Exécutez la commande build :

npm run build
ls dist
index.js  index.js.map  order.js  order.js.map

Le répertoire dist contient uniquement le code d’application, placé à la racine grâce à rootDir: "src".

6.1. Écrire des tests avec Vitest

Installez Vitest :

npm install --save-dev vitest

Ajoutez "test": "vitest run" à scripts, puis écrivez un test pour la fonction totalItems :

// tests/order.test.ts
import { describe, expect, it } from "vitest";
import type { Order } from "../src/order.js";
import { totalItems } from "../src/order.js";

describe("totalItems", () => {
  it("compte correctement les articles de la commande", () => {
    const order: Order = { id: "A1", items: ["pho", "tra-da", "quay"] };
    expect(totalItems(order)).toBe(3);
  });

  it("retourne 0 pour une commande vide", () => {
    expect(totalItems({ id: "A2", items: [] })).toBe(0);
  });
});
npm run typecheck && npm test
 ✓ tests/order.test.ts (2 tests)
 Test Files  1 passed (1)
      Tests  2 passed (2)

Vitest supprime automatiquement les types lors de l’exécution sans les vérifier. Par conséquent, exécutez toujours typecheck dans CI. Sinon, un test avec une erreur de type peut toujours « passer ».

7. Variante pour les projets utilisant un bundler

Si le code est bundlé par Vite, esbuild ou webpack au lieu de s’exécuter directement sur Node.js, changez la partie module comme suit :

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "noEmit": true,
    "jsx": "react-jsx"
  }
}
  • moduleResolution: "Bundler" permet les imports sans extension de fichier, car le bundler les résout.
  • noEmit: true signifie que tsc vérifie uniquement les types, tandis que le bundler gère la génération des fichiers.
  • Ajoutez "DOM" car le code s’exécute dans le navigateur. L’option jsx n’est nécessaire que si vous utilisez React.

Note : N’utilisez pas Bundler pour le code s’exécutant directement avec node dist/index.js. Le compilateur acceptera les imports sans l’extension .js, mais Node.js signalera une erreur à l’exécution.

8. Erreurs courantes

Utiliser paths pour créer des alias et générer avec tsc. Supposons que vous déclariez "paths": { "@/*": ["src/*"] }. L’éditeur comprend cet alias, mais tsc ne réécrit pas les chemins dans la sortie, donc node ne trouvera pas @/order.js. Pour un projet Node.js, utilisez les subpath imports dans package.json :

{
  "imports": {
    "#src/*": "./dist/*"
  }
}

Ensuite, vous importez avec import { totalItems } from "#src/order.js";. En mode NodeNext, TypeScript lit le champ imports et mappe à rebours de dist à src, donc à la fois l’éditeur et le runtime résolvent correctement.

Abuser de skipLibCheck. Ce drapeau saute uniquement la vérification des fichiers .d.ts, ce qui accélère la compilation et évite les erreurs provenant des types de bibliothèque. Cela ne rend pas votre code moins sûr. Cependant, si vous écrivez vous-même des fichiers .d.ts, souvenez-vous que les erreurs dans ces fichiers seront également ignorées.

Oublier include. Sans include, TypeScript analyse le répertoire complet, y compris dist. Le résultat est généralement l’erreur TS5055: Cannot write file ... because it would overwrite input file.

Mélanger CommonJS et ESM. Si package.json n’a pas "type": "module" mais que vous utilisez toujours NodeNext, les fichiers .ts seront compilés en CommonJS. Importer un package qui supporte uniquement ESM causera une erreur. Choisissez une direction dès le départ. Pour un nouveau projet, vous devriez choisir ESM.

Activer strict puis mettre any partout. Cela rend le mode strict inefficace. Quand vous ne connaissez pas le type des données, utilisez unknown puis affinez le type avec une instruction de vérification.

9. Conclusion

Cet article a construit un tsconfig.json pour un nouveau projet Node.js avec les sections suivantes :

  • module et moduleResolution définis à NodeNext, accompagnés de "type": "module" pour que le compilateur simule correctement le runtime.
  • strict avec les drapeaux noUncheckedIndexedAccess et exactOptionalPropertyTypes pour détecter les erreurs tôt.
  • verbatimModuleSyntax et isolatedModules pour une sortie prévisible et compatible avec les outils de compilation rapides.
  • Fichier tsconfig.build.json séparé, pour que dist ne contienne que le code d’application.

Si votre projet utilise un bundler, vous n’avez qu’à changer moduleResolution: "Bundler" et noEmit: true. Les drapeaux de vérification strict restent identiques. Enfin, exécutez toujours tsc --noEmit dans CI, car la plupart des outils de test et des bundlers ne vérifient pas les types pour vous.

typescripttsconfignodejsconfigurationvitest