1.3 · Modules
Les diapositives de la séance 1
- expliquer ce qu’un module apporte : portée isolée et dépendances explicites
- exporter des constantes et des fonctions avec
export - importer avec
import { … } from "./fichier.ts" - reconnaître les quatre formes d’import et d’export
- résoudre un conflit de noms avec
as
Savoir écrire et typer une fonction : 1.1 · Hello world TypeScript. Savoir lancer un projet Astro : 1.2 · Une page web avec Astro.
Au chapitre précédent, la page Astro chargeait un script avec <script src="../scripts/app.ts">. Ce chapitre, volontairement court, fait un pas de plus : découper ce script en plusieurs fichiers qui s’échangent des fonctions. Vous vous en servirez dans tout le reste du cours, à commencer par la séance 3, où chaque classe aura son propre fichier.
Pourquoi découper en modules
Un module est simplement un fichier .ts qui exporte certains éléments et en importe d’autres. Deux bénéfices comptent plus que tous les autres :
- Portée isolée : tout ce qui n’est pas exporté est invisible de l’extérieur. Deux fichiers peuvent déclarer une variable
totalsans jamais se marcher dessus. - Dépendances explicites : la ligne
importen haut d’un fichier dit exactement de quoi ce fichier a besoin. C’est la première chose à lire pour comprendre du code.
S’y ajoutent la réutilisation (un module de calcul sert à dix pages sans copier-coller) et la liberté de réécrire l’intérieur d’un module sans rien casser, tant que ses exports gardent la même signature.
#include "monfichier.h" recopie du texte dans votre fichier avant compilation : tout ce qui est dans l’en-tête arrive chez vous, que vous le vouliez ou non. import { additionner } from "./moduleMath.ts" est bien plus précis : rien n’est recopié, et vous nommez exactement ce que vous prenez.
Le parallèle utile : une fonction non exportée en TypeScript correspond à une fonction static en C, visible dans son fichier et nulle part ailleurs. La différence est que TypeScript fait de l’isolement la règle par défaut, et de l’exposition l’exception.
Le projet d’exemple
Le code du chapitre, dans un projet Astro : télécharger moduleMath.ts, app.ts et index.astro.
src/
├── pages/
│ └── index.astro # la page, charge le script
└── scripts/
├── app.ts # script principal, importe le module
└── moduleMath.ts # le module, exporte constantes et fonctions
Le module : moduleMath.ts
src/scripts/moduleMath.ts
// Constantes exportées : lisibles depuis n'importe quel fichier qui les importe
export const pi = 3.14159;
export const phi = 1.61803; // nombre d'or
export function additionner(a: number, b: number): number {
return a + b;
}
export function soustraire(a: number, b: number): number {
return a - b;
}
export function diviser(a: number, b: number): number {
if (b === 0) {
throw new Error("Division par zéro impossible");
}
return a / b;
}
export function absolue(num: number): number {
if (num < 0) return num * -1;
return num;
}export devant une déclaration suffit. Sans lui, pi resterait privé au module : visible à l’intérieur du fichier, introuvable à l’extérieur. Le type de retour est écrit explicitement : c’est le contrat que les autres fichiers vont lire.
Le consommateur : app.ts
src/scripts/app.ts
import { pi, phi, additionner, soustraire, absolue } from "./moduleMath.ts";
const container: HTMLDivElement = document.createElement("div");
container.style.fontFamily = "monospace";
const a: number = 10;
const b: number = 3;
container.innerHTML = `
<p>pi = ${pi}, phi = ${phi}</p>
<p>additionner(${a}, ${b}) = ${additionner(a, b)}</p>
`;
const difference: number = soustraire(phi, pi); // 1.61803 - 3.14159 = -1.52356
const resultatAbsolu: number = absolue(difference); // 1.52356
console.log(`|phi - pi| = ${resultatAbsolu.toFixed(5)}`);
document.body.appendChild(container);| Morceau | Sens |
|---|---|
import |
mot-clé d’importation |
{ pi, phi, … } |
les noms exacts exportés par le module, et eux seuls |
from "./moduleMath.ts" |
chemin relatif : ./ signifie « même dossier » |
Vous n’importez que ce que vous utilisez. Ici diviser existe dans le module mais n’est pas importé : c’est normal, et c’est même souhaitable.
Le ./ n’est pas décoratif. import { pi } from "moduleMath.ts", sans point, demande un paquet installé dans node_modules, pas votre fichier voisin. L’erreur ressemble à Failed to resolve import.
Autre source d’échec : l’extension. Dans un projet Astro, écrivez "./moduleMath.ts". Dans un projet compilé avec tsc seul, la convention est souvent "./moduleMath" sans extension. Suivez ce qui marche dans le projet où vous êtes.
La page qui charge le module
src/pages/index.astro
---
---
<html lang="fr">
<body>
<script src="../scripts/app.ts"></script>
</body>
</html>On ne charge que app.ts. Astro suit la chaîne des import et regroupe tout ce qui est nécessaire : moduleMath.ts n’a pas à être mentionné dans la page.
Les quatre formes d’import et d’export
| Forme | Dans le module | Dans le fichier appelant |
|---|---|---|
| Export nommé (recommandé) | export const pi = 3.14; |
import { pi } from "./m.ts"; |
| Export par défaut | export default function calcul() {} |
import calcul from "./m.ts"; |
| Renommage | export const pi = 3.14; |
import { pi as PI_APPROX } from "./m.ts"; |
| Import global | (rien de particulier) | import * as Maths from "./m.ts"; puis Maths.pi |
L’export nommé est celui à privilégier : le nom est imposé par le module, donc le même partout dans le projet, et l’éditeur peut l’auto-importer et le renommer. L’export par défaut laisse choisir le nom à l’import, ce qui est une source de confusion dans un projet à plusieurs.
Résoudre un conflit de noms
Deux modules peuvent exporter le même nom. La solution est le renommage à l’import :
import { calculerRestants as restantsVacances } from "./moduleVacances.ts";
import { calculerRestants as restantsTravail } from "./moduleTravail.ts";
console.log(restantsVacances(10));
console.log(restantsTravail(150));Un import déclare un nom dans votre fichier. Importer deux fois le même nom depuis deux modules différents provoque Duplicate identifier : le compilateur ne peut pas choisir à votre place. C’est exactement ce que as résout.
La collision de noms n’est pas un accident : c’est la situation normale dès qu’un projet grossit. JOURS_TOTAL est un bon nom à l’intérieur d’un module de congés, où le contexte lève l’ambiguïté. C’est au consommateur, qui seul voit les deux modules à la fois, de trancher avec as.
Exercices
Exercice 1.7 : Étendre le module de calcul
essentiel
Dans moduleMath.ts, ajoutez :
- une constante exportée
Evalant2.71828; - une fonction exportée
puissance(base: number, exposant: number): number; - une fonction non exportée
arrondir(x: number): number, utilisée uniquement à l’intérieur depuissance.
Depuis app.ts, tentez d’importer arrondir.
Réussite : vous savez citer le message d’erreur exact et expliquer en une phrase pourquoi arrondir est inaccessible.
src/scripts/moduleMath.ts
// … le contenu précédent du module reste inchangé
export const E = 2.71828;
// Pas de export : arrondir n'existe que dans ce fichier
function arrondir(x: number): number {
return Math.round(x * 10000) / 10000;
}
export function puissance(base: number, exposant: number): number {
let resultat: number = 1;
for (let i = 0; i < exposant; i++) {
resultat = resultat * base;
}
return arrondir(resultat);
}src/scripts/app.ts
import { E, puissance } from "./moduleMath.ts";
console.log(E); // 2.71828
console.log(puissance(2, 10)); // 1024
console.log(puissance(1.1, 3)); // 1.331 : arrondir a supprimé la traîne de décimalesLa tentative d’import :
import { arrondir } from "./moduleMath.ts";error TS2459: Module '"./moduleMath"' declares 'arrondir' locally,
but it is not exported.
Le message est remarquablement précis, et vaut la peine d’être lu en entier : le compilateur vous dit qu’il a trouvé arrondir dans le fichier. Il ne s’agit donc ni d’une faute de frappe ni d’un mauvais chemin, mais d’un refus délibéré : sans export, un nom reste privé au module qui le déclare.
Le point clé : c’est l’intérêt central des modules, et il est facile à sous-estimer. arrondir est un détail d’implémentation de puissance. Tant qu’il n’est pas exporté, vous êtes certain que personne d’autre ne l’utilise, et vous pouvez donc le renommer, changer son arrondi à six décimales ou le supprimer sans rien casser ailleurs dans le projet. Dès que vous écrivez export, ce nom devient un engagement envers tout le reste du code. Exportez le moins possible : c’est la seule manière de garder la liberté de modifier l’intérieur d’un module.
Si vous aviez vraiment besoin d’arrondir ailleurs, la réponse n’est pas de l’exporter à la hâte, mais de constater qu’il s’agit d’un outil général, qui aurait sa place dans un module dédié.
Note : la boucle for est écrite ici pour rester explicite ; Math.pow(base, exposant) et l’opérateur base ** exposant font la même chose en une expression. Attention cependant : cette version ne gère pas les exposants négatifs, contrairement à Math.pow.
Exercice 1.8 : Une troisième source, sans toucher au reste
essentiel transfert
Un projet compte déjà deux modules qui exportent les mêmes noms, et un script qui les utilise en résolvant le conflit avec as. Recopiez ces trois fichiers dans src/scripts/, et une page src/pages/exercice.astro qui charge calculJours.ts.
src/scripts/moduleVacances.ts
export const JOURS_TOTAL: number = 25; // congés annuels
export function calculerJoursRestants(joursPris: number): number {
return JOURS_TOTAL - joursPris;
}src/scripts/moduleTravail.ts
export const JOURS_TOTAL: number = 218; // jours travaillés dans l'année
export function calculerJoursRestants(joursPris: number): number {
return JOURS_TOTAL - joursPris;
}src/scripts/calculJours.ts
import {
JOURS_TOTAL as JOURS_VACANCES_TOTAL,
calculerJoursRestants as calculerVacancesRestantes
} from "./moduleVacances.ts";
import {
JOURS_TOTAL as JOURS_TRAVAIL_TOTAL,
calculerJoursRestants as calculerTravailRestant
} from "./moduleTravail.ts";
const container: HTMLDivElement = document.createElement("div");
container.style.fontFamily = "monospace";
const ligneVacances: HTMLParagraphElement = document.createElement("p");
ligneVacances.textContent =
`Vacances : ${calculerVacancesRestantes(10)} jours restants sur ${JOURS_VACANCES_TOTAL}`;
container.appendChild(ligneVacances);
const ligneTravail: HTMLParagraphElement = document.createElement("p");
ligneTravail.textContent =
`Travail : ${calculerTravailRestant(150)} jours restants sur ${JOURS_TRAVAIL_TOTAL}`;
container.appendChild(ligneTravail);
document.body.appendChild(container);src/pages/exercice.astro
---
---
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Calcul des jours</title>
</head>
<body>
<h1>Gestion des jours</h1>
<script src="../scripts/calculJours.ts"></script>
</body>
</html>Ajoutez maintenant moduleRTT.ts, qui exporte lui aussi JOURS_TOTAL (valant 12) et calculerJoursRestants, et intégrez-le à calculJours.ts pour afficher les RTT restants après 3 jours pris.
Réussite : la page affiche RTT : 9 jours restants sur 12, et l’ajout ne demande aucune modification des deux autres modules. C’est précisément ce que l’on attend d’un découpage en modules.
src/scripts/moduleRTT.ts
export const JOURS_TOTAL: number = 12; // jours de RTT annuels
export function calculerJoursRestants(joursPris: number): number {
return JOURS_TOTAL - joursPris;
}Dans calculJours.ts, un import de plus et trois lignes d’affichage, rien d’autre :
src/scripts/calculJours.ts
import {
JOURS_TOTAL as JOURS_RTT_TOTAL,
calculerJoursRestants as calculerRTTRestants
} from "./moduleRTT.ts";
const joursPrisRTT: number = 3;
const rttRestants: number = calculerRTTRestants(joursPrisRTT);
const ligneRTT: HTMLParagraphElement = document.createElement("p");
ligneRTT.textContent = `RTT : ${rttRestants} jours restants sur ${JOURS_RTT_TOTAL}`;
container.appendChild(ligneRTT);Le point clé : l’exercice ne se juge pas sur ce que vous avez écrit, mais sur ce que vous n’avez pas eu à toucher. moduleVacances.ts et moduleTravail.ts sont restés strictement identiques, alors que le nouveau module réutilise leurs deux noms d’export. C’est possible parce que chaque module définit ses noms dans sa propre portée : JOURS_TOTAL n’existe pas « globalement », il existe trois fois, dans trois espaces séparés, et c’est calculJours.ts qui décide comment les appeler chez lui.
Comparez avec la situation en C sans précaution : trois #define JOURS_TOTAL recopiés dans le même fichier, et la troisième définition l’emporte silencieusement. Ici le compilateur exige que vous choisissiez. Sans as sur le nouvel import :
error TS2300: Duplicate identifier 'JOURS_TOTAL'.
Variante : avec trois sources structurées à l’identique, la répétition commence à se voir. L’import global la supprime :
import * as Vacances from "./moduleVacances.ts";
import * as Travail from "./moduleTravail.ts";
import * as RTT from "./moduleRTT.ts";
console.log(Vacances.calculerJoursRestants(10)); // 15
console.log(RTT.JOURS_TOTAL); // 12Plus aucun as à écrire : le nom du module préfixe tout ce qu’il contient. En contrepartie, l’éditeur auto-importe moins bien, et l’on ne voit plus d’un coup d’œil ce qui est réellement utilisé dans le fichier.
Pour aller plus loin
Exercice 1.9 : Les fichiers de départ, écrits vous-même
approfondissement
Refermez les fichiers de départ de l’exercice 1.8 et réécrivez-les sans les regarder :
moduleVacances.ts: exporteJOURS_TOTAL = 25etcalculerJoursRestants(joursPris: number): number;moduleTravail.ts: exporteJOURS_TOTAL = 218et une fonctioncalculerJoursRestantsde même signature ;calculJours.ts: importe les deux modules, résout le conflit de noms avecas, et affiche dans la page, viacreateElementetappendChild, les jours de vacances restants pour 10 jours pris et les jours de travail restants pour 150 jours travaillés.
Réussite : la page affiche 15 et 68, et calculJours.ts ne contient aucune constante numérique en dur qui appartienne aux modules (ni 25, ni 218).
Les fichiers de départ de l’exercice 1.8 sont la solution. Dans calculJours.ts, on peut préférer nommer les données avant de les utiliser :
src/scripts/calculJours.ts
import {
JOURS_TOTAL as JOURS_VACANCES_TOTAL,
calculerJoursRestants as calculerVacancesRestantes
} from "./moduleVacances.ts";
import {
JOURS_TOTAL as JOURS_TRAVAIL_TOTAL,
calculerJoursRestants as calculerTravailRestant
} from "./moduleTravail.ts";
const joursPrisVacances: number = 10;
const joursTravailles: number = 150;
const vacancesRestantes: number = calculerVacancesRestantes(joursPrisVacances);
const travailRestant: number = calculerTravailRestant(joursTravailles);
const container: HTMLDivElement = document.createElement("div");
container.style.fontFamily = "monospace";
const ligneVacances: HTMLParagraphElement = document.createElement("p");
ligneVacances.textContent =
`Vacances : ${vacancesRestantes} jours restants sur ${JOURS_VACANCES_TOTAL}`;
container.appendChild(ligneVacances);
const ligneTravail: HTMLParagraphElement = document.createElement("p");
ligneTravail.textContent =
`Travail : ${travailRestant} jours restants sur ${JOURS_TRAVAIL_TOTAL}`;
container.appendChild(ligneTravail);
document.body.appendChild(container);src/scripts/moduleVacances.ts
export const JOURS_TOTAL: number = 25;
export function calculerJoursRestants(joursPris: number): number {
return JOURS_TOTAL - joursPris;
}src/scripts/moduleTravail.ts
export const JOURS_TOTAL: number = 218;
export function calculerJoursRestants(joursPris: number): number {
return JOURS_TOTAL - joursPris;
}Sur « aucune constante en dur » : ni 25 ni 218 n’apparaissent dans calculJours.ts, qui les lit via JOURS_VACANCES_TOTAL et JOURS_TRAVAIL_TOTAL. Les valeurs 10 et 150 y restent, mais ce sont les données de l’exercice, pas des règles métier : dans une vraie application, elles viendraient d’un formulaire ou d’une base.
L’erreur classique : recopier 25 dans calculJours.ts pour afficher le total. Le jour où la convention collective passe à 27 jours, vous corrigez le module et la page continue d’afficher 25. Une valeur, un seul endroit où elle est écrite.
QCM
1. moduleMath.ts contient const pi = 3.14159;, sans export. Que se passe-t-il dans app.ts ?
import { pi } from "./moduleMath.ts";A. pi vaut 3.14159. B. Une erreur de compilation : le module n’exporte pas pi. C. pi vaut undefined. D. pi est importé en lecture seule.
B. Ce qui ne porte pas export reste invisible hors du fichier. Le compilateur le dit : declares 'pi' locally, but it is not exported.
2. Pourquoi import { pi } from "moduleMath.ts"; échoue-t-il, alors que le fichier est dans le même dossier ?
A. L’extension .ts est interdite dans un import. B. Sans ./, le nom désigne un paquet, cherché dans node_modules. C. Les accolades sont en trop. D. Il faut écrire import * as.
B. Un chemin relatif commence toujours par ./ ou ../.
3. Deux modules exportent chacun une fonction calculerRestants. Comment utiliser les deux dans app.ts ?
A. C’est impossible : il faut renommer la fonction dans l’un des deux modules. B. Importer les deux sous le même nom : le second remplace le premier. C. Les renommer à l’import : import { calculerRestants as restantsVacances }, et de même pour l’autre. D. Les importer dans deux fichiers différents seulement.
C. Le conflit se règle dans le fichier qui importe, le seul à voir les deux modules. B donne Duplicate identifier.
Pour lire plus loin :
À retenir
- Un module = un fichier ; seul ce qui porte
exportsort du fichier import { nom } from "./fichier.ts": le./et l’extension comptent- Préférez les exports nommés : un seul nom pour une même chose dans tout le projet
asrenomme à l’import et résout les conflits de noms- La page ne charge que le script d’entrée ; Astro suit les
importtout seul