Retour au cours

frontend / typescript

Modules et namespaces

Leçon 121 exercice

Explication

Ce que vous allez apprendre

  • Organiser un projet TypeScript en modules avec import/export
  • Distinguer un export nommé d'un export par défaut
  • Utiliser import type/export type pour les éléments qui n'existent qu'à la compilation
  • Comprendre l'impact d'un barrel file (index.ts) sur le tree-shaking
  • Situer l'usage encore pertinent des namespace face aux modules ES

Dans quel contexte ?

Une équipe migre son build vers esbuild pour accélérer sa CI, et le build échoue avec une erreur sur un fichier qui fait import { Utilisateur } from "./types";Utilisateur est une interface, pas une valeur JavaScript réelle. Comme esbuild transpile chaque fichier indépendamment (sans connaître le contenu de ./types), il ne peut pas deviner que cet import doit être totalement effacé du JavaScript final plutôt que conservé. Remplacer import { Utilisateur } par import type { Utilisateur } résout immédiatement le problème en rendant l'intention explicite.

Organiser le code en unités indépendantes

Un projet TypeScript grandit rapidement au-delà d'un seul fichier. Les modules ES, le système import/export standard de JavaScript moderne, permettent de découper le code en unités indépendantes, chacune exposant explicitement ce qu'elle souhaite partager avec le reste du projet, et gardant le reste privé par défaut.

Prérequis

Cette leçon suppose les modules ES vus côté JavaScript (leçon 14 du cours JavaScript) déjà connus : TypeScript ajoute uniquement la distinction types/valeurs par-dessus ce système existant.

Types et valeurs, une distinction essentielle

Un point spécifique à TypeScript : certains éléments importés n'existent que pour le typage, comme une interface ou un type alias, et n'ont aucune existence dans le JavaScript final. Les mots-clés import type et export type rendent cette distinction explicite, ce qui devient important avec les compilateurs modernes qui traitent chaque fichier isolément, comme esbuild, et ne peuvent pas deviner, sans cette annotation, si un import doit être conservé ou totalement effacé.

ImportExiste dans le JS final ?Mot-clé recommandé
Fonction, classe, constanteOuiimport { x }
Interface, type aliasNonimport type { X }
Mélange des deux depuis un modulePartiellementSéparer en deux imports distincts

Piège fréquent

Importer une interface sans import type fonctionne avec tsc seul, mais peut échouer avec des compilateurs mono-fichier comme esbuild ou swc (option isolatedModules). Prenez l'habitude d'utiliser import type dès qu'un import ne sert qu'au typage.

Barrel files : pratique, avec un revers

Regrouper plusieurs modules derrière un seul point d'entrée, un fichier index.ts qui ré-exporte tout, simplifie grandement les imports ailleurs dans le projet. Mais ce confort a un coût : les outils de tree-shaking, qui éliminent le code mort du bundle final, ont parfois plus de mal à analyser ces fichiers intermédiaires, ce qui peut alourdir le résultat final si on n'y prend pas garde.

Namespaces : un outil plus ancien

Avant que les modules ES ne deviennent le standard, TypeScript proposait les namespace pour organiser le code. Ils restent utiles dans des cas précis, comme regrouper des types globaux ou fusionner un espace de noms avec une classe pour lui attacher des types statiques, mais pour du code applicatif moderne, les modules ES restent le choix par défaut.

Commandes & code

Modules et namespaces

ts
// math.ts — export nommés
export function additionner(a: number, b: number): number {
  return a + b;
}
export const PI = 3.14159;
export interface Point {
  x: number;
  y: number;
}

// export par défaut : un seul par fichier
export default class Calculatrice {
  memoire = 0;
}

// consommateur.ts
import Calculatrice, { additionner, PI, type Point } from "./math";
// "type Point" : import de type explicite, effacé à la compilation (aucun impact runtime)

const c = new Calculatrice();
console.log(additionner(2, PI));

// Ré-export : agréger plusieurs modules dans un seul point d'entrée (barrel file)
// index.ts
export * from "./math";
export { default as Calculatrice } from "./math";

// Import dynamique : charge le module à la demande (code-splitting)
async function chargerModuleLourd() {
  const module = await import("./gros-module");
  module.initialiser();
}

// import type / export type : garantit qu'aucune valeur runtime n'est importée,
// utile avec "isolatedModules" et les compilateurs par-fichier (esbuild, swc)
import type { Utilisateur } from "./types";
export type { Utilisateur };

// Namespaces : organisation legacy, surtout utile pour regrouper des types globaux
// (préférer les modules ES pour du code applicatif moderne)
namespace Validation {
  export interface Regle {
    valider(valeur: string): boolean;
  }
  export class ReglePasVide implements Regle {
    valider(valeur: string): boolean {
      return valeur.trim().length > 0;
    }
  }
}
const regle: Validation.Regle = new Validation.ReglePasVide();

// Fusion de namespace avec une classe : pattern pour attacher des types statiques
class Widget {
  static creer(config: Widget.Config): Widget {
    return new Widget();
  }
}
namespace Widget {
  export interface Config {
    titre: string;
  }
}

Résumé

  • import type / export type sont effacés à la compilation : aucun coût runtime.
  • Les barrel files (export * from) simplifient les imports mais peuvent nuire au tree-shaking.
  • Les namespaces restent utiles pour des types globaux ou fusionner types + valeurs statiques.

Exercices pratiques

1 disponible
1

Mission : réparer un build esbuild cassé par un import de type

Objectif : Diagnostiquer pourquoi un import d'interface fait échouer un compilateur mono-fichier, puis le corriger avec import type.

Contexte

Une équipe migre son build vers esbuild, et le build échoue sur import { Utilisateur } from "./types"; où Utilisateur est une interface. Ta mission : comprendre pourquoi esbuild ne peut pas deviner tout seul que cet import doit être totalement effacé, puis corriger l'import.

Résoudre l’exercice →