Modules et namespaces#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Dans tout projet logiciel d’une certaine envergure, l’organisation du code en unités autonomes et réutilisables est une nécessité absolue. TypeScript s’appuie sur le système de modules ES comme principal mécanisme d’encapsulation, tout en offrant des fonctionnalités supplémentaires — résolution de modules configurable, déclarations ambiantes, augmentation de modules — qui lui permettent de couvrir des scénarios que JavaScript seul ne peut pas adresser statiquement. Ce chapitre explore la chaîne complète, depuis l’import d’un fichier local jusqu’à la cohabitation avec des bibliothèques tierces non typées.

Modules ES (standard)#

Un fichier TypeScript devient un module dès lors qu’il contient au moins une déclaration import ou export de haut niveau. En l’absence de telles déclarations, le fichier est traité comme un script global, dont les déclarations sont visibles dans tout le projet — ce qui peut provoquer des conflits de noms inattendus.

Exports nommés et export par défaut#

// fichier : géométrie/formes.ts

// Export nommé — on peut en avoir autant que nécessaire
export interface Forme {
  calculerAire(): number;
  calculerPérimètre(): number;
}

export class Cercle implements Forme {
  constructor(readonly rayon: number) {}

  calculerAire(): number {
    return Math.PI * this.rayon ** 2;
  }

  calculerPérimètre(): number {
    return 2 * Math.PI * this.rayon;
  }
}

export class Rectangle implements Forme {
  constructor(
    readonly largeur: number,
    readonly hauteur: number
  ) {}

  calculerAire(): number {
    return this.largeur * this.hauteur;
  }

  calculerPérimètre(): number {
    return 2 * (this.largeur + this.hauteur);
  }
}

// Export par défaut — un seul par fichier, sans nom obligatoire à l'export
export default class Polygone implements Forme {
  constructor(private readonly côtés: number[]) {}

  calculerAire(): number { /* ... */ return 0; }
  calculerPérimètre(): number {
    return this.côtés.reduce((s, c) => s + c, 0);
  }
}
// fichier : main.ts

// Import nommé — le nom doit correspondre exactement à l'export
import { Cercle, Rectangle, type Forme } from "./géométrie/formes";

// Import de l'export par défaut — on choisit librement le nom local
import FigureComplexe from "./géométrie/formes";

// Import avec alias
import { Cercle as CercleMath } from "./géométrie/formes";

// Import de l'ensemble du module dans un espace de noms
import * as Formes from "./géométrie/formes";

const c = new Formes.Cercle(5);

import type#

TypeScript introduit la syntaxe import type pour importer uniquement des types, jamais des valeurs. Cette distinction est capitale pour l’outillage (notamment pour les bundlers comme esbuild ou swc) qui doit éliminer les imports purement typographiques sans risquer de supprimer une importation de valeur.

// Import de type uniquement — effacé complètement à la compilation
import type { Forme } from "./géométrie/formes";
import type { Utilisateur, Rôle } from "./modèles/utilisateur";

// On peut mélanger dans un seul import avec le qualificateur inline
import { Cercle, type Rectangle } from "./géométrie/formes";

function dessiner(forme: Forme): void {
  // Forme n'existe qu'à la compilation — aucun code JS émis pour cet import
}

Définition 33 (Module ES)

Un module ES (ECMAScript Module, souvent abrégé ESM) est un fichier qui exporte et/ou importe des entités via les mots-clés export et import. Chaque module possède sa propre portée : les variables déclarées au niveau supérieur d’un module ne sont pas visibles en dehors de celui-ci, sauf si elles sont explicitement exportées. Cette isolation est fondamentalement différente du comportement des scripts classiques.

Remarque 24

La distinction entre export nommé et export par défaut dépasse la simple syntaxe. Les exports nommés :

  • Sont plus faciles à renommer globalement (les outils de refactorisation les trouvent immédiatement).

  • Permettent l’auto-importation dans les éditeurs.

  • Sont plus adaptés aux modules qui exposent plusieurs entités.

Les exports par défaut sont utiles pour les modules qui exposent une entité principale (un composant React, par exemple), mais rendent les noms locaux arbitraires et compliquent la refactorisation automatique. De nombreux guides de style modernes déconseillent les exports par défaut dans les bibliothèques.

Résolution de modules#

Lorsque TypeScript rencontre import { x } from "chemin", il doit localiser le fichier source correspondant. Cette opération est régie par la stratégie de résolution de modules configurée dans tsconfig.json.

Stratégies de résolution#

{
  "compilerOptions": {
    "moduleResolution": "node16"
  }
}

Stratégie

Contexte typique

Comportement principal

node

Projets CommonJS, anciens projets Node.js

Algorithme de Node.js classique : node_modules, index, extensions

node16

Node.js ≥ 16 avec ESM natif ("type": "module")

Résolution ESM stricte : extensions obligatoires dans les imports

nodenext

Alias de node16, orienté futur

Identique à node16

bundler

Vite, esbuild, webpack

Résolution permissive : pas d’extension obligatoire, paths, etc.

classic

Projets legacy TypeScript ancienne génération

Dépréciée — à éviter dans tout nouveau projet

paths et baseUrl#

baseUrl définit le répertoire racine des imports non relatifs. paths permet de créer des alias, très utiles pour éviter des chemins relatifs profonds comme ../../../utils/format.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*":       ["src/*"],
      "@utils/*":  ["src/utils/*"],
      "@modèles/*": ["src/modèles/*"],
      "@services/*": ["src/services/*"]
    }
  }
}
// Sans alias — difficile à maintenir
import { formaterDate } from "../../../utils/format/date";

// Avec alias — clair et résistant aux déplacements de fichiers
import { formaterDate } from "@utils/format/date";
import type { Utilisateur } from "@modèles/utilisateur";

Remarque 25

TypeScript résout les alias paths au niveau des types uniquement. Pour que le code compilé (JavaScript) fonctionne à l’exécution, il faut configurer séparément le bundler ou utiliser un outil comme tsc-alias ou tsconfig-paths. C’est l’un des points de friction les plus courants pour les développeurs qui découvrent les alias TypeScript : les types sont corrects mais l’exécution échoue.

Modules ambiants et fichiers .d.ts#

Les fichiers de déclaration (.d.ts) décrivent la forme d’un module sans fournir d’implémentation. Ils permettent à TypeScript de comprendre des bibliothèques JavaScript pures ou des modules dont le code source n’est pas accessible.

declare module#

// fichier : types/modules.d.ts

// Déclaration d'un module entier
declare module "bibliothèque-sans-types" {
  export function calculer(valeur: number): number;
  export const VERSION: string;
  export interface Options {
    précision: number;
    arrondir: boolean;
  }
}

// Déclaration générique pour tous les fichiers .svg
declare module "*.svg" {
  const contenu: string;
  export default contenu;
}

// Déclaration générique pour les fichiers de configuration JSON
declare module "*.json" {
  const valeur: unknown;
  export default valeur;
}

@types/*#

Le dépôt DefinitelyTyped héberge des déclarations de types pour des milliers de bibliothèques JavaScript populaires. Ces déclarations sont publiées sur npm sous la portée @types/ et installées comme dépendances de développement :

npm install --save-dev @types/node @types/express @types/lodash

TypeScript les découvre automatiquement dans node_modules/@types/. Le paramètre typeRoots de tsconfig.json permet de restreindre les dossiers explorés, et types permet de lister explicitement les paquets @types à inclure.

Écrire ses propres déclarations#

// fichier : types/globaux.d.ts

// Étendre l'interface Window du DOM
interface Window {
  __ÉTAT_GLOBAL__: {
    utilisateur: { id: string; nom: string } | null;
    thème: "clair" | "sombre";
  };
}

// Déclarer une variable globale injectée par l'environnement
declare const __VERSION_APP__: string;
declare const __MODE_DÉVELOPPEMENT__: boolean;

// Déclarer un module CSS Modules
declare module "*.module.css" {
  const classes: Record<string, string>;
  export default classes;
}

Exemple 12 (Déclarer un plugin jQuery inexistant)

Si une bibliothèque jQuery tierce n’est pas typée, on peut écrire une déclaration minimale pour éviter les erreurs TypeScript :

// types/jquery-plugin.d.ts
interface JQuery {
  maAnimationPersonnalisée(options?: {
    durée: number;
    délai?: number;
  }): JQuery;
}

TypeScript fusionnera automatiquement cette déclaration avec l’interface JQuery existante grâce à la fusion de déclarations (declaration merging).


## Namespaces (ancienne approche)

Avant l'adoption des modules ES, TypeScript proposait les **namespaces** (anciennement appelés « modules internes ») comme mécanisme d'organisation du code.

```typescript
namespace Géométrie {
  export interface Forme {
    aire(): number;
  }

  export namespace Primitive {
    export class Cercle implements Forme {
      constructor(readonly rayon: number) {}
      aire(): number { return Math.PI * this.rayon ** 2; }
    }
  }
}

// Usage
const c = new Géométrie.Primitive.Cercle(3);

Usage légitime aujourd’hui#

Les namespaces restent utiles dans deux contextes précis :

  1. Dans les fichiers .d.ts : pour organiser de grands ensembles de déclarations de types sans émettre de code JavaScript.

  2. Pour les bibliothèques UMD : certaines bibliothèques exposent un espace de noms global (jQuery, _, moment) que les déclarations de namespaces ambiants décrivent fidèlement.

Pourquoi ils sont déconseillés dans le code applicatif moderne#

Remarque 26

Les namespaces présentent plusieurs inconvénients qui les rendent inadaptés au code applicatif moderne :

  • Ils ne sont pas compris nativement par les bundlers (webpack, Vite, esbuild) ni par les environnements ESM natifs (Node.js 16+, navigateurs modernes).

  • Ils nécessitent une compilation TypeScript spécifique (--outFile) pour être assemblés en un seul fichier, ce qui est incompatible avec la plupart des outils modernes.

  • Ils créent une dépendance à TypeScript dans l’architecture du code : si on retire TypeScript, la structure organisationnelle disparaît aussi.

  • Les modules ES sont la solution standard définie par ECMAScript, supportée par tous les environnements modernes et parfaitement outillée.

La règle est simple : dans un nouveau projet, on n’utilise que des modules ES. Les namespaces sont réservés aux déclarations ambiantes et à la maintenance de code legacy.

Augmentation de modules#

L”augmentation de modules (module augmentation) permet d’ajouter des déclarations à un module existant sans modifier son code source. C’est le mécanisme par lequel des bibliothèques peuvent étendre les types d’autres bibliothèques, ou par lequel on peut corriger des types incomplets.

Augmenter un module tiers#

// fichier : types/express-augmentation.d.ts

// Importer le module à augmenter
import "express";

// Augmenter les types d'Express
declare module "express-serve-static-core" {
  interface Request {
    // Ajouter une propriété personnalisée à toutes les requêtes Express
    utilisateur?: {
      id: string;
      rôles: string[];
      session: string;
    };
    corrélationId: string;
  }
}
// Maintenant, dans n'importe quel contrôleur Express :
app.get("/profil", (req, res) => {
  // TypeScript connaît req.utilisateur grâce à l'augmentation
  if (!req.utilisateur) {
    res.status(401).json({ erreur: "Non authentifié" });
    return;
  }
  res.json({ id: req.utilisateur.id });
});

Augmenter les types globaux#

Pour ajouter des propriétés à des interfaces globales comme Window, Array, ou Promise, on place les déclarations dans un fichier .d.ts sans import/export de haut niveau (pour que le fichier soit traité comme un script, pas comme un module) :

// fichier : types/globaux.d.ts — PAS d'import/export de haut niveau !

interface Array<T> {
  // Méthode utilitaire absente du standard
  dernier(): T | undefined;
  grouperPar<K extends string>(sélecteur: (élément: T) => K): Record<K, T[]>;
}

interface String {
  versSlug(): string;
}

Si le fichier doit être un module (il contient des imports), on utilise declare global :

// fichier : utils/extensions.ts
import { formaterDate } from "./format";

// Augmenter le type global depuis un module
declare global {
  interface Date {
    formater(patron: string): string;
  }
}

Date.prototype.formater = function (patron: string): string {
  return formaterDate(this, patron);
};

Définition 34 (Augmentation de modules)

L”augmentation de modules est une technique qui consiste à déclarer des membres supplémentaires dans l’espace de types d’un module existant, via la syntaxe declare module 'nom' { ... }. TypeScript fusionne ces déclarations avec celles du module original lors de la vérification des types. Cette technique est le fondement de la compatibilité des plugins et des extensions de bibliothèques.

/// <reference> directives#

Les directives de référence sont des commentaires spéciaux placés en tête de fichier qui indiquent au compilateur des dépendances de types supplémentaires à inclure.

/// <reference types="..." />#

/// <reference types="node" />
/// <reference types="jest" />

// Le fichier peut maintenant utiliser les types de @types/node et @types/jest
// sans que ces paquets soient listés dans tsconfig.json > types

const chemin = require("path"); // OK : path est connu via @types/node

/// <reference path="..." />#

/// <reference path="../types/globaux.d.ts" />

// Inclure explicitement un fichier de déclarations
// Utile dans les projets n'utilisant pas de bundler

/// <reference lib="..." />#

/// <reference lib="es2022" />
/// <reference lib="dom" />

// Inclure des bibliothèques TypeScript intégrées
// Équivalent à "lib": ["es2022", "dom"] dans tsconfig.json

Remarque 27

Les directives /// <reference> sont rarement nécessaires dans les projets modernes. tsconfig.json gère correctement l’inclusion des fichiers via include, exclude et types. Ces directives subsistent surtout dans :

  • Les fichiers .d.ts de bibliothèques publiées sur npm, qui ne peuvent pas dépendre d’un tsconfig.json spécifique.

  • Les fichiers de test qui nécessitent des types supplémentaires (comme @types/jest) uniquement dans le contexte des tests.

  • Les projets générant un seul fichier de sortie avec --outFile, où l’ordre d’inclusion des fichiers doit être explicite.

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis('off')
ax.set_title('Résolution de modules TypeScript', fontsize=16,
             fontweight='bold', pad=20)

palette = sns.color_palette("muted", 8)
bleu    = palette[0]
orange  = palette[1]
vert    = palette[2]
rouge   = palette[3]
violet  = palette[4]
gris    = '#888888'

# ---- Fichier source ----
src_box = patches.FancyBboxPatch(
    (0.3, 5.8), 3.2, 1.8,
    boxstyle="round,pad=0.15", linewidth=2.5,
    edgecolor=bleu, facecolor=bleu, alpha=0.15)
ax.add_patch(src_box)
ax.text(1.9, 6.95, 'Fichier source', ha='center', va='center',
        fontsize=12, fontweight='bold', color=bleu)
ax.text(1.9, 6.35,
        'import { x } from "chemin"',
        ha='center', va='center', fontsize=9,
        color='#333333', fontfamily='monospace',
        bbox=dict(boxstyle='round,pad=0.2', facecolor='white',
                  edgecolor=bleu, alpha=0.7))

# ---- Stratégie de résolution ----
strat_box = patches.FancyBboxPatch(
    (4.8, 5.8), 4.4, 1.8,
    boxstyle="round,pad=0.15", linewidth=2.5,
    edgecolor=violet, facecolor=violet, alpha=0.15)
ax.add_patch(strat_box)
ax.text(7.0, 7.1, 'Stratégie de résolution', ha='center', va='center',
        fontsize=12, fontweight='bold', color=violet)
ax.text(7.0, 6.55,
        'moduleResolution: node16 / bundler',
        ha='center', va='center', fontsize=9,
        color='#555555', fontfamily='monospace')
ax.text(7.0, 6.15,
        'baseUrl + paths (aliases)',
        ha='center', va='center', fontsize=9,
        color='#555555', fontfamily='monospace')

# Flèche source → stratégie
ax.annotate('', xy=(4.8, 6.7), xytext=(3.5, 6.7),
            arrowprops=dict(arrowstyle='->', color='#444444', lw=2))

# ---- Trois destinations ----
destinations = [
    (0.5,  2.4, 3.0, 1.4, vert,   'Fichier .ts / .tsx',
     'src/utils/format.ts\nsrc/modèles/user.ts'),
    (5.0,  2.4, 4.0, 1.4, orange, 'Fichier .d.ts',
     'types/globaux.d.ts\n*.d.ts du projet'),
    (10.0, 2.4, 3.5, 1.4, rouge,  'node_modules/@types',
     '@types/node, @types/express\n@types/react, …'),
]

for dx, dy, dw, dh, couleur, titre, détail in destinations:
    box = patches.FancyBboxPatch(
        (dx, dy), dw, dh,
        boxstyle="round,pad=0.15", linewidth=2.2,
        edgecolor=couleur, facecolor=couleur, alpha=0.15)
    ax.add_patch(box)
    ax.text(dx + dw / 2, dy + dh - 0.35,
            titre, ha='center', va='center',
            fontsize=10, fontweight='bold', color=couleur)
    ax.text(dx + dw / 2, dy + 0.42,
            détail, ha='center', va='center',
            fontsize=8, color='#555555', fontfamily='monospace',
            linespacing=1.5)

# Flèches stratégie → destinations
cibles = [(2.0, 3.8), (7.0, 3.8), (11.75, 3.8)]
for cx, cy in cibles:
    ax.annotate('', xy=(cx, cy), xytext=(7.0, 5.8),
                arrowprops=dict(arrowstyle='->', color='#666666',
                                lw=1.8, linestyle='dashed'))

# ---- Ordre de priorité ----
ax.text(7.0, 1.5,
        'Ordre de priorité : fichiers .ts locaux > chemins d\'alias > .d.ts > node_modules/@types',
        ha='center', va='center', fontsize=9.5,
        color='#555555', style='italic',
        bbox=dict(boxstyle='round,pad=0.4', facecolor='#f0f8ff',
                  edgecolor='#aaaacc', alpha=0.9))

# ---- tsconfig.json ----
cfg_box = patches.FancyBboxPatch(
    (0.3, 0.2), 13.4, 0.9,
    boxstyle="round,pad=0.1", linewidth=1.5,
    edgecolor='#999999', facecolor='#f9f9f9', alpha=0.8)
ax.add_patch(cfg_box)
ax.text(7.0, 0.65,
        'tsconfig.json  —  "moduleResolution", "baseUrl", "paths", "typeRoots", "types"',
        ha='center', va='center', fontsize=9.5,
        color='#444444', fontfamily='monospace')

plt.tight_layout()
plt.show()
_images/d230f305760dbb37461d1137e442769826ec0f59c53f507d8d64a2a7709067ab.png

Résumé#

Dans ce chapitre, nous avons parcouru l’ensemble du système de modules et de déclarations de TypeScript :

  • Un fichier TypeScript est un module dès qu’il contient import ou export. Sans cela, il est traité comme un script global, ce qui peut causer des conflits de noms.

  • Les exports nommés sont préférables aux exports par défaut pour la refactorisation et l’auto-importation. La syntaxe import type permet d’importer des types sans émettre de code JavaScript.

  • La résolution de modules est configurable via moduleResolution dans tsconfig.json. Les stratégies node16 et bundler sont recommandées dans les nouveaux projets. Les alias paths simplifient les imports profonds mais nécessitent une configuration complémentaire à l’exécution.

  • Les fichiers .d.ts décrivent la forme de bibliothèques JavaScript. Le dépôt @types/* fournit des déclarations pour des milliers de bibliothèques populaires. On peut écrire ses propres déclarations pour les bibliothèques non couvertes.

  • Les namespaces sont une fonctionnalité héritée, utile uniquement dans les fichiers .d.ts et pour les bibliothèques UMD. Dans tout code applicatif moderne, les modules ES doivent être préférés.

  • L”augmentation de modules permet d’étendre les types d’un module existant sans le modifier. C’est le mécanisme central pour les plugins de bibliothèques et pour ajouter des propriétés à des types globaux.

  • Les directives /// <reference> permettent d’inclure des fichiers de types supplémentaires, mais sont rarement nécessaires dans les projets modernes correctement configurés avec tsconfig.json.

Le chapitre suivant entre dans la pratique de l’écosystème : comment configurer et utiliser TypeScript dans un projet Node.js, depuis la structure de répertoires jusqu’au déploiement en production.