TypeScript

Configuration et écosystème

tsconfig, mode strict, résolution de modules.

Vérifié en septembre 2026 · TypeScript 6.0 · environ 17 min

Le fichier tsconfig.json est au compilateur TypeScript ce que le .csproj est à MSBuild : il dit quels fichiers forment le projet, quel JavaScript produire et avec quelle sévérité vérifier. Deux projets au code identique peuvent ainsi accepter ou refuser les mêmes lignes, et émettre des imports qui tournent ou non à l'exécution. Ce cours part de la configuration réelle de ce site, détaille ce que couvre strict et ce qu'il laisse de côté, puis les options qui décident du JavaScript émis et de la résolution des imports. La référence est TypeScript 6.0, une version de transition : elle change plusieurs valeurs par défaut et déprécie ce que TypeScript 7.0, le compilateur réécrit en Go sorti en juillet 2026, a retiré. Angular 22.1 exige encore TypeScript 6.0 (>=6.0 <6.1 pour @angular/compiler-cli et @angular/build) ; les messages cités sont ceux de tsc 6.0.3, et les exécutions celles de Node 24. Le système de types lui-même fait l'objet des cours Types et inférence et Types avancés.

Anatomie d'un tsconfig.json

compilerOptions règle le compilateur ; include, exclude et files choisissent les fichiers, par motifs relatifs au dossier du fichier ou par liste explicite ; extends hérite d'une autre configuration. L'héritage fusionne les options une à une ; include, exclude et files, eux, remplacent ceux du parent au lieu de s'y ajouter, et references ne se transmet jamais. Le site en donne un exemple complet : un fichier racine qui porte les options communes sans compiler aucun fichier, et deux fichiers qui en héritent, l'un pour l'application, l'autre pour les tests.

// Les trois fichiers de web/, sans leurs commentaires d'en-tete, les tableaux
// remis sur une ligne.

// web/tsconfig.json : les options communes, et aucun fichier.
{
  "compileOnSave": false,
  "compilerOptions": {
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "ES2022",
    "module": "preserve"
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  },
  "files": [],
  "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.spec.json" }]
}

// web/tsconfig.app.json : le programme que compile ng build (angular.json, "tsConfig").
{
  "extends": "./tsconfig.json",
  "compilerOptions": { "types": [] },
  "include": ["src/**/*.ts"],
  "exclude": ["src/**/*.spec.ts"]
}

// web/tsconfig.spec.json : les tests, avec les globales describe, it, expect.
{
  "extends": "./tsconfig.json",
  "compilerOptions": { "types": ["vitest/globals"] },
  "include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
}

Depuis TypeScript 6.0, strict vaut vrai par défaut : pour tsc, la ligne "strict": true ne change rien. Le fichier l'écrit pourtant, pour le compilateur d'Angular, qui ne lance certains diagnostics de template (NG8102, NG8107) que si l'option est écrite. strictTemplates, lui aussi actif par défaut depuis Angular 22 (cours Nouveautés, d'Angular 17 à 22), est écrit pour que le fichier dise lui-même ce qu'il vérifie. Les options propres à Angular vivent dans angularCompilerOptions, que tsc ignore et que lit le compilateur d'Angular. En cas de doute, tsc --showConfig -p tsconfig.app.json affiche la configuration fusionnée et la liste des fichiers. Il montre les options écrites et certaines options qu'elles impliquent, comme preserveConstEnums: true, qu'aucun fichier n'écrit mais qu'isolatedModules entraîne ; il ne montre pas les valeurs par défaut : la résolution bundler n'y figure pas.

Ce que strict active

strict n'est pas un mode à part, c'est un raccourci qui allume une famille de drapeaux. En TypeScript 6.0, elle en compte huit.

DrapeauCe qu'il refuse
noImplicitAnyun paramètre ou une variable dont le type, faute d'indice, deviendrait any
strictNullChecksnull et undefined affectés à un type qui ne les mentionne pas
strictFunctionTypes une fonction passée là où ses paramètres ne sont pas compatibles (contravariance)
strictBindCallApplydes arguments mal typés dans call, apply et bind
strictPropertyInitializationun champ de classe que le constructeur n'initialise pas
strictBuiltinIteratorReturnle retour d'un itérateur intégré traité comme any
noImplicitThisun this dont le type est inconnu
useUnknownInCatchVariablesl'usage direct de la variable d'un catch, qui devient unknown

alwaysStrict, qui émet "use strict", a quitté la famille : il est toujours actif en 6.0, et le mettre à false est déprécié. Les deux drapeaux les plus visibles, noImplicitAny et useUnknownInCatchVariables, sont illustrés dans le cours Types et inférence, et strictFunctionTypes est expliqué dans Types avancés ; voici les autres.

// Chaque erreur ci-dessous vient d'un drapeau de la famille strict ; avec
// "strict": false, le fichier compile sans un mot, lignes fautives comprises.

// strictPropertyInitialization : un champ est affecte par le constructeur ou
// porte un initialiseur.
class Panier {
  lignes: string[];
  // total: number;
  // error TS2564: Property 'total' has no initializer and is not definitely assigned in the constructor.
  total = 0;

  constructor() {
    this.lignes = [];
  }
}

// noImplicitThis : dans une fonction libre, this n'a pas de type.
// function compter() { return this.lignes.length; }
// error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
function compter(this: Panier): number {
  return this.lignes.length;
}

// strictBindCallApply : call, apply et bind verifient leurs arguments.
function ajouter(reference: string, quantite: number): void {
  console.log(`${quantite} x ${reference}`);
}
// ajouter.call(undefined, 'USB-64', '2');
// error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
ajouter.call(undefined, 'USB-64', 2); // 2 x USB-64

// strictBuiltinIteratorReturn (TypeScript 5.6) : ce que rend un iterateur de la
// bibliotheque une fois epuise est undefined, et non plus any.
const iterateur = ['USB-64', 'SSD-1T'][Symbol.iterator]();
const valeur = iterateur.next().value; // string | undefined
// const reference: string = valeur;
// error TS2322: Type 'string | undefined' is not assignable to type 'string'.
//   Type 'undefined' is not assignable to type 'string'.

console.log(compter.call(new Panier()), valeur ?? 'aucune'); // 0 USB-64

Chaque drapeau reste réglable seul, et une valeur explicite l'emporte sur strict : "strict": true suivi de "strictPropertyInitialization": false garde les sept autres. La famille s'agrandit avec les versions, comme strictBuiltinIteratorReturn en 5.6 ; une mise à jour de TypeScript peut donc faire apparaître des erreurs dans un code inchangé, et c'est voulu.

Au-delà de strict

Le trou le plus coûteux que strict laisse ouvert concerne les accès par index. Un Record<string, number> lu avec une clé quelconque, ou un tableau lu à un indice quelconque, rend un number ou une string, comme si l'élément existait toujours.

// "strict": true, sans noUncheckedIndexedAccess.
const stock: Record<string, number> = { 'USB-64': 12, 'SSD-1T': 3 };
const references = ['USB-64', 'SSD-1T'];

// Un acces par index rend le type de l'element, comme si la cle ou l'indice
// existait toujours.
function disponible(reference: string): number {
  return stock[reference] - 1; // number, pour le compilateur
}

const troisieme: string = references[2]; // string, pour le compilateur

console.log(disponible('HDMI-2M')); // NaN
console.log(troisieme.toUpperCase());
// A l'execution (Node) : TypeError: Cannot read properties of undefined (reading 'toUpperCase')

noUncheckedIndexedAccess ajoute undefined au type de chaque accès par index, et oblige à traiter l'absence. L'option n'est pas dans strict parce qu'elle coûte : une boucle for classique sur les indices demande un contrôle que l'humain sait inutile. Les cas sûrs, for...of et tuples, restent intacts.

// "strict": true et "noUncheckedIndexedAccess": true.
const stock: Record<string, number> = { 'USB-64': 12, 'SSD-1T': 3 };
const references = ['USB-64', 'SSD-1T'];

// Chaque acces par index rend desormais T | undefined.
// function disponible(reference: string): number { return stock[reference] - 1; }
// error TS2532: Object is possibly 'undefined'.
function disponible(reference: string): number {
  const quantite = stock[reference]; // number | undefined
  return quantite === undefined ? 0 : quantite - 1;
}

// const troisieme: string = references[2];
// error TS2322: Type 'string | undefined' is not assignable to type 'string'.
//   Type 'undefined' is not assignable to type 'string'.
const troisieme = references[2] ?? 'aucune';

// Ce qui ne peut pas manquer n'est pas touche : l'element d'un for...of, et
// l'indice d'un tuple, dont la longueur est connue.
for (const reference of references) {
  console.log(reference.toLowerCase()); // usb-64, puis ssd-1t
}
const paire: [string, number] = ['USB-64', 12];
const prix: number = paire[1];

console.log(disponible('HDMI-2M'), troisieme, prix); // 0 aucune 12

Les autres options hors famille resserrent des points plus étroits. Le site en active quatre ; exactOptionalPropertyTypes, la plus exigeante, distingue une propriété absente d'une propriété présente qui vaut undefined, ce que JavaScript distingue aussi (in, Object.keys, la décomposition).

// Les quatre options que web/tsconfig.json ajoute a strict, puis
// exactOptionalPropertyTypes, qu'il n'active pas.

// noImplicitOverride : redefinir une methode heritee exige override. Que la
// methode de base soit renommee, et la redefinition devient une erreur au lieu
// d'une methode orpheline que plus personne n'appelle.
class Composant {
  initialiser(): void {}
}

class Panier extends Composant {
  // initialiser(): void {}
  // error TS4114: This member must have an 'override' modifier because it overrides a member in the base class 'Composant'.
  override initialiser(): void {}
}

// noPropertyAccessFromIndexSignature : une cle venue d'une signature d'index se
// lit entre crochets, ce qui la distingue d'une propriete declaree.
const entetes: { [nom: string]: string } = { accept: 'application/json' };
// console.log(entetes.accept);
// error TS4111: Property 'accept' comes from an index signature, so it must be accessed with ['accept'].
console.log(entetes['accept']); // application/json

// noImplicitReturns : toutes les branches rendent une valeur, ou aucune.
// function libelle(code: number) { if (code === 200) { return 'OK'; } }
// error TS7030: Not all code paths return a value.
function libelle(code: number): string {
  return code === 200 ? 'OK' : 'Erreur';
}

// noFallthroughCasesInSwitch : un case non vide finit par break ou return.
// function niveau(code: number) { switch (code) { case 404: console.log('404'); case 500: return 'erreur'; } return 'info'; }
// error TS7029: Fallthrough case in switch.
function niveau(code: number): string {
  switch (code) {
    case 404:
      return 'avertissement';
    case 500:
      return 'erreur';
    default:
      return 'info';
  }
}

// exactOptionalPropertyTypes : ? veut dire « peut manquer », pas « peut valoir
// undefined ». Sans l'option, { delaiMs: undefined } est admis, et ecrase le
// defaut dans une decomposition : { ...defauts, ...options } rend alors
// { delaiMs: undefined }.
interface Options {
  delaiMs?: number;
}

const defauts = { delaiMs: 500 };
// const options: Options = { delaiMs: undefined };
// error TS2375: Type '{ delaiMs: undefined; }' is not assignable to type 'Options' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
//   Types of property 'delaiMs' are incompatible.
//     Type 'undefined' is not assignable to type 'number'.
const options: Options = {};

console.log(libelle(200), niveau(404), { ...defauts, ...options });
// OK avertissement { delaiMs: 500 }
new Panier().initialiser();

target et lib

Les deux options se confondent souvent et répondent pourtant à deux questions différentes. target fixe la syntaxe du JavaScript émis : ce qui est plus récent que la cible est réécrit avec des constructions plus anciennes. lib choisit les fichiers de déclarations qui décrivent l'environnement d'exécution — Array.prototype.findLast, document, Promise.try. Sans lib, TypeScript prend celle qui correspond à la cible, plus dom ; écrire lib la remplace entièrement, et oublier dom fait échouer le premier document avec l'erreur TS2584.

// tsconfig.json : "target": "es2022", sans "lib" : lib vaut alors es2022 et dom.
interface Ligne {
  reference: string;
  prix?: number;
}

const lignes: Ligne[] = [{ reference: 'USB-64', prix: 12 }, { reference: 'CABLE-2M' }];

// lib decrit les API qui existent a l'execution. findLast date d'ES2023.
// const derniere = lignes.findLast((l) => l.prix !== undefined);
// error TS2550: Property 'findLast' does not exist on type 'Ligne[]'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2023' or later.
// error TS7006: Parameter 'l' implicitly has an 'any' type.

// "lib": ["es2023", "dom"] fait taire l'erreur, mais n'ajoute rien au
// navigateur : s'il ne connait pas findLast, l'appel leve a l'execution.
// lib promet ; un polyfill fournit.
const derniere = lignes.at(-1); // at date d'ES2022 : admis

// target decrit la syntaxe emise. Avec "target": "es2019" et une lib qui declare
// at ("lib": ["es2022", "dom"] ; seule, la cible es2019 amene sa propre lib, et
// at y devient l'erreur TS2550), tsc reecrit ?. et ??, qui datent d'ES2020 :
//   const prix = (_b = (_a = lignes[0]) === null || _a === void 0 ? void 0 : _a.prix) !== null && _b !== void 0 ? _b : 0;
// mais il laisse lignes.at(-1) tel quel : il ne reecrit jamais une API.
const prix = lignes[0]?.prix ?? 0;

console.log(derniere?.reference, prix); // CABLE-2M 12
export {};

Aucune des deux n'ajoute de polyfill : tsc abaisse la syntaxe, jamais les API. En 6.0, la cible par défaut suit la dernière version d'ECMAScript prise en charge, es2025 en 6.0.3, et dom inclut désormais dom.iterable et dom.asynciterable, restés comme fichiers vides. Dans un projet Angular, target n'est pas le dernier mot : @angular/build 22.1 relève toute cible inférieure à ES2022 à ES2022, avec un avertissement qui renvoie à la configuration Browserslist, c'est elle qui fixe la syntaxe livrée aux navigateurs.

module et moduleResolution

module dit quel système de modules le JavaScript émis utilise et quelles règles d'import s'appliquent ; moduleResolution dit comment un spécificateur comme './panier' ou 'prix-utils' devient un fichier. La seconde se déduit de la première, et trois valeurs couvrent les projets actuels.

ValeurPourImports relatifs
nodenext (ou node16, node18, node20)un programme exécuté par Node sans outil intermédiaire extension obligatoire dans un fichier ESM ("type": "module" ou .mts) ; facultative dans un fichier CommonJS, émis en require
bundlerun code que Vite, esbuild ou webpack assembleraextension facultative, comme le permettent les bundlers
preserve (valeur de module)même cas, en émettant chaque import tel qu'il est écritrésolution bundler implicite ; c'est le choix du site

Le piège vient d'une configuration de bundler appliquée à un programme que Node exécute directement. TypeScript n'ajoute jamais d'extension : il vérifie l'import selon les règles qu'on lui a données, et l'émet tel quel. Sans module, TypeScript 6.0 émet des modules ECMAScript et résout en mode bundler, ce qui produit exactement ce cas.

// tsconfig.json : "module": "esnext", donc "moduleResolution": "bundler", pour
// un programme que Node execute sans bundler. package.json : "type": "module".
// src/panier.ts : export const lignes = [{ reference: 'USB-64', prix: 12 }];
import { lignes } from './panier';
import { arrondir } from 'prix-utils';

console.log(arrondir(lignes[0].prix * 1.2));

// tsc ne signale rien et emet l'import tel qu'il est ecrit : './panier'.
// A l'execution (Node 24), dist/panier n'existe pas, seul dist/panier.js existe
// (chemins abreges) :
// Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../dist/panier' imported from .../dist/main.js

Avec nodenext, le compilateur applique les règles de Node et refuse l'import au lieu de laisser Node le refuser. L'extension écrite est .js même dans un fichier .ts : un import désigne le fichier qui existera à l'exécution.

// tsconfig.json : "module": "nodenext" ; moduleResolution suit, nodenext.
// TypeScript applique la regle de Node : un import relatif porte son extension,
// celle du fichier emis.
// import { lignes } from './panier';
// error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './panier.js'?
import { lignes } from './panier.js';
import { arrondir } from 'prix-utils';

// Le champ exports de prix-utils ne publie que "." : le reste du paquet
// n'existe pas, pour Node comme pour TypeScript (bundler le respecte aussi).
// import { TVA } from 'prix-utils/interne/taux.js';
// error TS2307: Cannot find module 'prix-utils/interne/taux.js' or its corresponding type declarations.

console.log(arrondir(lignes[0].prix * 1.2)); // 14.4

ESM et CommonJS sous nodenext

Sous nodenext, le format ne se choisit pas dans le tsconfig mais fichier par fichier, comme Node le fait : un .mts est ESM, un .cts est CommonJS, et un .ts suit le champ type du package.json le plus proche. La règle des extensions ne vaut que pour ESM ; dans un fichier CommonJS, l'import devient un require, qui les complète seul.

// tsconfig.json : "module": "nodenext" ; package.json sans champ "type" :
// pour Node, un .js y est CommonJS.
// src/panier.ts : export const lignes = [{ reference: 'USB-64', prix: 12 }];

// src/main.ts est donc CommonJS : l'import s'emet en require("./panier"), et
// require trouve panier.js sans extension. Le fichier compile et s'execute.
import { lignes } from './panier';

console.log(lignes.length); // 1

// src/outil.mts : l'extension .mts rend le fichier ESM (emis en outil.mjs), quel
// que soit package.json ; .cts rend un fichier CommonJS de la meme facon.
// import { lignes } from './panier';
// error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './panier.js'?

node16, node18 et node20 figent le comportement d'une version de Node ; nodenext suit la plus récente que connaît le compilateur, et se comporte en 6.0.3, sur les deux points ci-dessous, comme node20. La différence porte sur ce que Node a appris entre-temps : les attributs d'import, puis le require d'un module ESM.

# Meme projet CommonJS ; prix-esm est un paquet ESM ("type": "module").
# src/main.ts : import { TVA } from 'prix-esm';
# src/json.mts : import config from './config.json' with { type: 'json' };
# (chemins abreges)

# "module": "node16"
src/json.mts(1,36): error TS2823: Import attributes are only supported when the '--module' option is set to 'esnext', 'node18', 'node20', 'nodenext', or 'preserve'.
src/main.ts(1,21): error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("prix-esm")' call instead.
  To convert this file to an ECMAScript module, change its file extension to '.mts', or add the field `"type": "module"` to '.../package.json'.

# "module": "node18" : TS2823 disparait, TS1479 reste.
# "module": "node20" ou "nodenext" : tout compile, et Node 24 execute le
# require("prix-esm") emis :
node dist/main.js
0.2

preserve contre esnext

Les deux valeurs résolvent en mode bundler. esnext exige une syntaxe ESM pure ; preserve admet aussi import x = require() et n'y touche pas, parce qu'un bundler sait assembler un fichier qui mêle les deux formes. C'est la valeur du web/tsconfig.json du site, dont esbuild assemble la sortie.

// src/panier.ts : export const lignes = [{ reference: 'USB-64', prix: 12 }];

// "module": "esnext" n'admet que la syntaxe ESM :
// import panier = require('./panier');
// error TS1202: Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.

// "module": "preserve" admet les deux syntaxes dans un meme fichier et les
// emet telles quelles ; c'est au bundler de les meler.
import panier = require('./panier');
import { lignes } from './panier';

console.log(panier.lignes === lignes);

// JavaScript emis :
//   const panier = require("./panier");
//   import { lignes } from './panier';
//   console.log(panier.lignes === lignes);

Le champ exports d'un paquet

Le champ exports d'un paquet est une liste blanche de points d'entrée. nodenext et bundler le respectent ; l'ancien node10 l'ignorait, si bien qu'un import profond compilait puis échouait dans Node avec ERR_PACKAGE_PATH_NOT_EXPORTED. Le champ imports fait l'inverse pour le paquet lui-même : il déclare des alias internes, préfixés par #.

// node_modules/prix-utils/package.json : exports est une liste blanche.
{
  "name": "prix-utils",
  "type": "module",
  "exports": {
    ".": {
      "types": "./index.d.ts",
      "default": "./index.js"
    }
  }
}

// package.json de l'application : imports declare des alias internes, qui
// commencent toujours par #. Depuis TypeScript 6.0 (nodenext et bundler), un
// alias peut commencer par #/ tout court, comme dans les versions recentes de
// Node : import { TVA } from '#/domaine/prix.js';
{
  "name": "boutique",
  "type": "module",
  "imports": {
    "#/*": "./dist/*"
  }
}

Un point d'entrée peut aussi dépendre de conditions : types pour le compilateur, import et require selon le format de l'importeur, default pour tous. Elles sont lues dans l'ordre de l'objet, et la première qui s'applique gagne ; default se place donc en dernier.

// node_modules/double/package.json : un point d'entree par format d'importeur.
{
  "name": "double",
  "exports": {
    ".": {
      "import": { "types": "./esm.d.mts", "default": "./esm.mjs" },
      "require": { "types": "./cjs.d.cts", "default": "./cjs.cjs" }
    }
  }
}

// esm.d.mts : export declare const format: 'esm';  (cjs.d.cts : 'cjs')
// src/a.mts :
//   import { format } from 'double';
//   const f: 'esm' = format;
// src/b.cts : les memes lignes, avec 'cjs'.

// Sous nodenext, a.mts recoit esm, b.cts recoit cjs : Node 24 affiche esm,
// puis cjs. Sous bundler, un .ts recoit esm : la condition import s'applique.

// La premiere condition qui s'applique gagne, dans l'ordre du fichier. Placee
// en tete, "default" s'applique a tous et masque import :
//   ".": { "default": { ... cjs ... }, "import": { ... esm ... } }
// src/a.mts(2,7): error TS2322: Type '"cjs"' is not assignable to type '"esm"'.

isolatedModules et verbatimModuleSyntax

tsc compile un programme entier : il sait, en lisant les autres fichiers, qu'un nom importé est un type. esbuild, SWC ou Babel transpilent un fichier à la fois. Ils effacent sans peine un import qui ne sert qu'en position de type, puisque le fichier lui-même le montre ; ils ne peuvent pas décider pour une réexportation, où rien dans le fichier ne dit si le nom existe à l'exécution. isolatedModules fait signaler par tsc tout ce qui ne peut pas être compilé sans voir les autres fichiers, à commencer par ce cas.

// panier.ts : export interface Ligne { ... } et export function total(...) { ... }
// Sans isolatedModules, tsc accepte ce fichier : il lit panier.ts, sait que
// Ligne est un type, et efface ce qui le concerne.
import { Ligne, total } from './panier';

// Un outil qui transpile fichier par fichier ne lit pas panier.ts : il ne sait
// pas s'il doit garder Ligne dans le JavaScript emis.
export { Ligne, total } from './panier';
// "isolatedModules": true
// error TS1205: Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'.

// "verbatimModuleSyntax": true signale en plus l'import :
// error TS1484: 'Ligne' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
// error TS1205: Re-exporting a type when 'verbatimModuleSyntax' is enabled requires using 'export type'.

export function resume(lignes: Ligne[]): string {
  return `${lignes.length} ligne(s), ${total(lignes)} EUR`;
}

verbatimModuleSyntax va plus loin et rend la règle d'émission lisible dans le source : un import ou un export marqué type disparaît, tout le reste est émis tel quel. Il implique isolatedModules. Le code juste dit alors lui-même ce qui existe à l'exécution.

// "verbatimModuleSyntax": true. Ce qui porte type disparait, le reste est emis
// tel qu'il est ecrit.
import { total } from './panier';
import type { Ligne } from './panier';

export { total } from './panier';
export type { Ligne } from './panier';

export function resume(lignes: Ligne[]): string {
  return `${lignes.length} ligne(s), ${total(lignes)} EUR`;
}

// JavaScript emis :
//   import { total } from './panier';
//   export { total } from './panier';
//   export function resume(lignes) { ... }

// Le modificateur en ligne, import { type Ligne }, efface le nom mais garde la
// declaration : seul, il laisse import {} from './panier', qui charge encore le
// module. import type efface la ligne entiere.

Le site active isolatedModules, et ce n'est pas qu'une précaution : dans @angular/build 22.1, cette option fait passer la compilation par une voie rapide où l'émission se fait fichier par fichier, au lieu de l'émission par TypeScript sur le programme entier.

Références de projet

Les références découpent un dépôt en projets TypeScript qui dépendent les uns des autres, comme des ProjectReference entre .csproj. Un projet référencé déclare composite, qui impose que tous ses fichiers soient listés et que ses déclarations soient émises ; le projet qui le référence lit ces fichiers .d.ts au lieu de revérifier ses sources.

// Un depot npm a deux espaces de travail : package.json racine,
// "workspaces": ["domaine", "app"], et "type": "module" partout.

// domaine/package.json : le paquet publie ce que tsc emet dans dist.
{
  "name": "@boutique/domaine",
  "type": "module",
  "exports": {
    ".": { "types": "./dist/prix.d.ts", "default": "./dist/prix.js" }
  }
}

// domaine/tsconfig.json : un projet reference doit etre composite.
{
  "compilerOptions": {
    "module": "nodenext",
    "composite": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

// app/tsconfig.json : app depend de domaine, pas l'inverse.
{
  "compilerOptions": {
    "module": "nodenext",
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"],
  "references": [{ "path": "../domaine" }]
}

// app/src/main.ts :
//   import { total, type Ligne } from '@boutique/domaine';
//   const lignes: Ligne[] = [{ reference: 'USB-64', prix: 12, quantite: 2 }];
//   console.log(total(lignes));

tsc -b construit le graphe dans l'ordre et saute les projets à jour, ce qui accélère un monorepo. Le sens des dépendances en devient une règle, par deux mécanismes : un projet composite ne compile que ses propres fichiers, sous son rootDir, si bien qu'un import vers les sources d'un autre projet échoue ; et le graphe des références doit rester sans cycle.

# tsc -p app, domaine n'ayant jamais ete construit : app lit les .d.ts de
# domaine, qui n'existent pas encore (chemins abreges ici et plus bas).
app/src/main.ts(1,35): error TS2307: Cannot find module '@boutique/domaine' or its corresponding type declarations.

# tsc -b app construit domaine, puis app, dans l'ordre du graphe.
tsc -b app
node app/dist/main.js
24

# Au second appel, rien n'a change ; tsc -b app --verbose dit pourquoi chaque
# projet est saute (horodatage omis) :
Project 'domaine/tsconfig.json' is up to date because newest input 'domaine/src/prix.ts' is older than output 'domaine/tsconfig.tsbuildinfo'

# domaine/src/remise.ts importe '../../app/src/panier.js' : le sens interdit.
domaine/src/remise.ts(1,24): error TS6059: File '.../app/src/panier.ts' is not under 'rootDir' '.../domaine/src'. 'rootDir' is expected to contain all source files.
  File is ECMAScript module because '.../app/package.json' has field "type" with value "module"
domaine/src/remise.ts(1,24): error TS6307: File '.../app/src/panier.ts' is not listed within the file list of project '.../domaine/tsconfig.json'. Projects must list all files or use an 'include' pattern.
  File is ECMAScript module because '.../app/package.json' has field "type" with value "module"

# Et une reference de domaine vers app forme un cycle :
error TS6202: Project references may not form a circular graph. Cycle detected: .../app/tsconfig.json
.../domaine/tsconfig.json

Le web/tsconfig.json du site emploie les références autrement : ses "files": [] en font un fichier « solution », pris en charge depuis TypeScript 3.9, qui sert à l'éditeur à rattacher chaque fichier au bon projet — un .spec.ts à la configuration des tests, avec les globales de Vitest. ng build, lui, compile directement tsconfig.app.json.

Ce que TypeScript 6.0 change

TypeScript 6.0 est, selon son annonce, la dernière version bâtie sur le compilateur écrit en JavaScript ; la 7.0, native, en reprend les défauts et fait des dépréciations de la 6.0 des erreurs définitives. La 6.0 aligne d'abord les valeurs par défaut sur les projets actuels.

OptionAvant 6.0Depuis 6.0
strictfalsetrue
targetes5la dernière version prise en charge (es2025)
modulecommonjs (résolution node10) es2022, déduit de la cible es2025 (les notes de version annoncent esnext) ; résolution bundler
typestous les paquets @typesaucun : [ ]
rootDirdéduit des fichiers sourcele dossier du tsconfig.json
noUncheckedSideEffectImportsfalsetrue
libReplacementtruefalse

libReplacement permettait de remplacer un fichier de lib par un paquet @typescript/lib-* installé ; le chercher coûtait des résolutions de modules manquées à chaque compilation. Le module par défaut a une conséquence discrète : es2022 ne connaît pas les attributs d'import, et un import ... with { type: 'json' } sans module écrit échoue avec TS2823. Les autres changements se voient à la première compilation d'un projet qui comptait sur les anciens défauts : un rootDir à écrire, un tableau types à remplir, comme le fait tsconfig.spec.json avec vitest/globals.

# tsc 6.0. rootDir vaut desormais le dossier du tsconfig.json : avec les sources
# dans src/ et "outDir": "dist", la sortie irait dans dist/src/.
tsconfig.json(1,24): error TS5011: The common source directory of 'tsconfig.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.

# types vaut [] : les paquets @types ne sont plus charges d'office.
src/spec.ts(1,1): error TS2593: Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha` and then add 'jest' or 'mocha' to the types field in your tsconfig.

# noUncheckedSideEffectImports vaut true : import './polyfils' (faute de frappe).
src/effet.ts(1,8): error TS2882: Cannot find module or type declarations for side-effect import of './polyfils'.

# tsc fichier.ts, dans un dossier qui contient un tsconfig.json :
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.

La version déprécie ensuite tout ce qui ne servait qu'à des environnements disparus : la cible es5 et downlevelIteration, les formats de modules AMD, UMD et SystemJS, outFile, la résolution node10 et classic, baseUrl comme racine de résolution, et les valeurs false d'esModuleInterop, allowSyntheticDefaultImports et alwaysStrict. Une option dépréciée n'est pas ignorée : elle fait échouer la compilation.

// Un tsconfig.json courant il y a quelques annees, compile par tsc 6.0.
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "baseUrl": "./src",
    "esModuleInterop": false,
    "downlevelIteration": true,
    "alwaysStrict": false,
    "outDir": "dist"
  }
}

// tsconfig.json(3,15): error TS5107: Option 'target=ES5' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
// tsconfig.json(5,25): error TS5107: Option 'moduleResolution=node10' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
//   Visit https://aka.ms/ts6 for migration information.
// tsconfig.json(6,5): error TS5101: Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
//   Visit https://aka.ms/ts6 for migration information.
// tsconfig.json(7,24): error TS5107: Option 'esModuleInterop=false' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
// tsconfig.json(8,5): error TS5101: Option 'downlevelIteration' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.
// tsconfig.json(9,21): error TS5107: Option 'alwaysStrict=false' is deprecated and will stop functioning in TypeScript 7.0. Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.

// Meme message pour outFile (TS5101), module amd, umd, system et none, et
// moduleResolution classic (TS5107).

"ignoreDeprecations": "6.0" fait taire ces erreurs, et c'est un sursis, pas une solution : la 7.0 a retiré ces options. Les remplacements sont connus. nodenext ou bundler prennent la place de node10, et bundler s'associe désormais aussi à "module": "commonjs" ; les entrées de paths s'écrivent relatives au tsconfig, sans baseUrl ; un bundler remplace outFile. Du côté du code, module Foo {} doit devenir namespace Foo {}, et les attributs d'import s'écrivent with au lieu d'assert. Ces deux formes anciennes sont déjà des erreurs, TS1540 et TS2880, et ignoreDeprecations ne fait taire que la seconde : module Foo échoue quoi qu'il arrive.

Ce cours vous a servi ? Offrir un café Signaler une erreur