À propos de ce cours
Cette leçon présente les deux piliers de l'architecture d'Angular : les classes et les modules. Vous apprendrez à déclarer une classe avec son constructeur, ses propriétés et ses méthodes, à utiliser les accesseurs get et set, les membres static et les champs privés #, puis à mettre en œuvre l'héritage avec extends et super. Vous découperez ensuite un programme en modules avec export et import, en distinguant export nommé et export par défaut, organiserez un dossier avec un fichier baril et chargerez un module à la demande avec import(). Des schémas, un composant Angular annoté et cinq activités pratiques accompagnent chaque notion.
-
Objectifs
- Déclarer une classe avec son constructeur, ses propriétés et ses méthodes, puis créer des instances avec
new - Utiliser les accesseurs
get/set, les membresstaticet les membres privés# - Mettre en œuvre l'héritage avec
extendsetsuper - Découper un programme en modules avec
exportetimport - Distinguer export nommé et export par défaut, et créer un fichier « baril » (
index) - Charger un module à la demande avec
import() - Reconnaître ces notions dans l'architecture d'un projet Angular
- Déclarer une classe avec son constructeur, ses propriétés et ses méthodes, puis créer des instances avec
-
Présentation
- Les classes et les modules sont deux nouveautés majeures d'ES6 (ES2015), complétées par les versions suivantes (champs publics, membres privés
#etstaticen ES2022). - Une classe est un modèle qui décrit les données (propriétés) et les actions (méthodes) d'un type d'objet. Chaque objet créé à partir de ce modèle est une instance.
- Un module est un fichier qui exporte ce qu'il veut partager ; les autres fichiers l'importent. Un projet devient ainsi un ensemble de petits fichiers clairs et indépendants.
- Ces deux notions sont la base de toute l'architecture d'Angular: chaque composant, service, pipe ou directive est une classe, rangée dans son propre fichier, exportée puis importée là où on l'utilise.
- Les classes et les modules sont deux nouveautés majeures d'ES6 (ES2015), complétées par les versions suivantes (champs publics, membres privés

-
Déclarer une classe
- Une classe se déclare avec le mot-clé
classsuivi de son nom, écrit en PascalCase (majuscule à chaque mot :Stagiaire,ListeFormations). - Elle contient :
- des champs (propriétés déclarées directement dans la classe, avec une valeur par défaut) ;
- un constructeur
constructor(), exécuté automatiquement parnewpour initialiser l'objet ; - des méthodes: les actions de l'objet, qui utilisent
thispour accéder à ses propriétés.
- Une classe se déclare avec le mot-clé
class Stagiaire {
groupe = 'Non affecté'; // champ public avec une valeur par défaut
notes = [];
constructor(nom, prenom) { // appelé par new
this.nom = nom;
this.prenom = prenom;
}
ajouterNote(note) { // méthode
this.notes.push(note);
return this; // renvoyer this permet d'enchaîner les appels
}
presenter() {
return `${this.prenom} ${this.nom} (${this.groupe})`;
}
}
const amira = new Stagiaire('Ben Ali', 'Amira'); // une instance
amira.groupe = 'DEV1';
amira.ajouterNote(14).ajouterNote(16).ajouterNote(17);
const sami = new Stagiaire('Trabelsi', 'Sami'); // une autre instance
console.log(amira.presenter()); // Amira Ben Ali (DEV1)
console.log(sami.presenter()); // Sami Trabelsi (Non affecté)
console.log(amira.notes === sami.notes); // false : chaque instance a ses propres données
console.log(amira instanceof Stagiaire); // true
- À savoir : une classe doit toujours être appelée avec
new(TypeError: Class constructor Stagiaire cannot be invoked without 'new'), et elle n'est pas remontée comme une fonction : on ne peut pas l'utiliser avant sa déclaration (ReferenceError: Cannot access 'Stagiaire' before initialization).

-
Les accesseurs get et set
- Un accesseur est une méthode qui s'utilise comme une propriété, sans parenthèses :
getcalcule une valeur à chaque lecture (une moyenne, un nom complet, un prix TTC…) ;setest appelé lors d'une affectation, ce qui permet de contrôler ou transformer la valeur reçue.
- Un accesseur est une méthode qui s'utilise comme une propriété, sans parenthèses :
class Stagiaire {
notes = [];
constructor(nom, prenom) {
this.nom = nom;
this.prenom = prenom;
}
get moyenne() {
if (this.notes.length === 0) return 0;
return this.notes.reduce((s, n) => s + n, 0) / this.notes.length;
}
get nomComplet() {
return `${this.prenom} ${this.nom.toUpperCase()}`;
}
set nomComplet(valeur) {
const [prenom, ...reste] = valeur.trim().split(' ');
this.prenom = prenom;
this.nom = reste.join(' ');
}
}
const s = new Stagiaire('Trabelsi', 'Sami');
s.notes.push(12, 15);
console.log(s.moyenne); // 13.5 : calculé, sans parenthèses
console.log(s.nomComplet); // Sami TRABELSI
s.nomComplet = 'Sami Ben Salah'; // appelle le set
console.log(s.nom); // Ben Salah
s.moyenne = 20; // pas de set : affectation ignorée
// (en mode strict, ou dans un module : TypeError: Cannot set property moyenne
// of #<Stagiaire> which has only a getter)
-
Les membres statiques et les membres privés
- Un membre
staticappartient à la classe elle-même, pas à ses instances. On l'utilise avec le nom de la classe :Formation.TVA,Formation.gratuite(). C'est utile pour les constantes communes, les compteurs et les « fabriques » d'objets. - Un membre dont le nom commence par
#est privé: il n'est accessible que depuis le code de la classe. C'est l'encapsulation: l'objet protège ses données et impose de passer par ses méthodes.
- Un membre
class Formation {
static TVA = 0.19; // constante partagée
static #nombre = 0; // compteur privé et statique
#inscrits = []; // données privées de chaque formation
#capacite;
constructor(titre, capacite = 20, prixHT = 0) {
this.titre = titre;
this.prixHT = prixHT;
this.#capacite = capacite;
Formation.#nombre++;
}
static get nombre() { // accesseur statique
return Formation.#nombre;
}
static gratuite(titre) { // méthode statique « fabrique »
return new Formation(titre, 30, 0);
}
get placesRestantes() {
return this.#capacite - this.#inscrits.length;
}
get prixTTC() {
return +(this.prixHT * (1 + Formation.TVA)).toFixed(3);
}
set capacite(valeur) { // contrôle de la valeur
if (!Number.isInteger(valeur) || valeur < this.#inscrits.length) {
throw new RangeError(`Capacité invalide : ${valeur}`);
}
this.#capacite = valeur;
}
inscrire(nom) {
if (this.#estComplete()) return `${nom} : refusé, ${this.titre} est complète`;
this.#inscrits.push(nom);
return `${nom} inscrit(e) en ${this.titre}`;
}
#estComplete() { // méthode privée
return this.placesRestantes === 0;
}
}
const web = new Formation('Développement web', 2, 1200);
console.log(web.inscrire('Amira')); // Amira inscrit(e) en Développement web
console.log(web.inscrire('Sami')); // Sami inscrit(e) en Développement web
console.log(web.inscrire('Karim')); // Karim : refusé, Développement web est complète
console.log(web.placesRestantes, web.prixTTC); // 0 1428
const atelier = Formation.gratuite('Atelier Git');
console.log(Formation.nombre); // 2
web.capacite = 1; // RangeError: Capacité invalide : 1
console.log(web.inscrits); // undefined : le champ privé n'est pas visible
// console.log(web.#inscrits); // SyntaxError: Private field '#inscrits' must be
// declared in an enclosing class
console.log(Object.keys(web)); // [ 'titre', 'prixHT' ]
-
L'héritage : extends et super
- Une classe peut hériter d'une autre avec
extends: elle récupère toutes ses propriétés et méthodes, puis ajoute ou redéfinit ce qui lui est propre. super(…)appelle le constructeur de la classe parente. Dans une classe dérivée, il est obligatoire et doit être appelé avant toute utilisation dethis.super.methode()appelle la version parente d'une méthode redéfinie.
- Une classe peut hériter d'une autre avec

class Personne {
constructor(nom, prenom) {
this.nom = nom;
this.prenom = prenom;
}
get nomComplet() {
return `${this.prenom} ${this.nom}`;
}
presenter() {
return `Je m'appelle ${this.nomComplet}`;
}
toString() { // utilisée quand l'objet est converti en texte
return this.nomComplet;
}
}
class Stagiaire extends Personne {
constructor(nom, prenom, groupe) {
super(nom, prenom);
this.groupe = groupe;
}
presenter() {
return `${super.presenter()}, stagiaire en ${this.groupe}`;
}
}
class Formateur extends Personne {
constructor(nom, prenom, specialite) {
super(nom, prenom);
this.specialite = specialite;
}
presenter() {
return `${super.presenter()}, formateur en ${this.specialite}`;
}
}
const personnes = [
new Stagiaire('Ben Ali', 'Amira', 'DEV1'),
new Formateur('Gharbi', 'Ali', 'Angular'),
new Personne('Mansour', 'Leila'),
];
for (const p of personnes) {
console.log(p.presenter()); // chaque objet utilise SA version de presenter()
}
// Je m'appelle Amira Ben Ali, stagiaire en DEV1
// Je m'appelle Ali Gharbi, formateur en Angular
// Je m'appelle Leila Mansour
const ali = personnes[1];
console.log(ali instanceof Formateur, ali instanceof Personne, ali instanceof Stagiaire);
// true true false
console.log(`Responsable : ${ali}`); // Responsable : Ali Gharbi (toString)
- Appeler la même méthode sur des objets de classes différentes et obtenir un comportement adapté à chacun s'appelle le polymorphisme.
- Oubli de super :
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor. - Sous le capot : les classes sont une écriture plus claire (« sucre syntaxique ») du système de prototypes de JavaScript :
typeof Stagiairevaut'function'. Il n'est pas nécessaire de connaître les prototypes pour utiliser les classes.
Les modules : export et import
- Un module est un fichier JavaScript qui a sa propre portée: ses variables, fonctions et classes sont invisibles des autres fichiers, sauf celles qu'il exporte.
- Les modules s'exécutent toujours en mode strict, et chaque module n'est exécuté qu'une seule fois, même s'il est importé par plusieurs fichiers.
- Pour les utiliser :
- avec Node.js : extension
.mjs, ou"type": "module"danspackage.json; - dans un navigateur :
<script type="module" src="app.js"></script>, la page devant être servie par un serveur web ; - dans Angular : tous les fichiers
.tssont déjà des modules, rien à configurer.
- avec Node.js : extension
L'export nommé
- On place
exportdevant chaque élément à partager. Un fichier peut avoir autant d'exports nommés que nécessaire. Ils s'importent entre accolades, avec le même nom.
// utils/calculs.mjs
export const SEUIL_ADMISSION = 10;
export function moyenne(notes) {
if (notes.length === 0) return 0;
return notes.reduce((s, n) => s + n, 0) / notes.length;
}
export const estAdmis = (stagiaire) => stagiaire.moyenne >= SEUIL_ADMISSION;
// app.mjs
import { moyenne, estAdmis } from './utils/calculs.mjs'; // le chemin commence par ./ ou ../
import { SEUIL_ADMISSION as seuil } from './utils/calculs.mjs'; // renommer avec as
import * as calculs from './utils/calculs.mjs'; // tout le module dans un objet
console.log(moyenne([12, 15]), seuil, calculs.SEUIL_ADMISSION); // 13.5 10 10
- On peut aussi regrouper les exports en fin de fichier :
export { moyenne, estAdmis, SEUIL_ADMISSION };.
L'export par défaut
- Un fichier peut avoir un seul export par défaut, qui s'importe sans accolades, avec le nom de son choix. On peut combiner export par défaut et exports nommés.
// utils/calculs.mjs (suite)
export default function arrondir(valeur, decimales = 2) {
return Number(valeur.toFixed(decimales));
}
// app.mjs
import arrondir, { moyenne } from './utils/calculs.mjs'; // défaut + nommé
console.log(arrondir(moyenne([12.456, 15.1]))); // 13.78
| Export nommé | Export par défaut | |
|---|---|---|
| Nombre par fichier | illimité | un seul |
| Syntaxe d'export | export class Stagiaire { } |
export default class Stagiaire { } |
| Syntaxe d'import | import { Stagiaire } from … |
import Stagiaire from … |
| Nom à l'import | le même (ou renommé avec as) |
libre |
| Dans Angular | presque toujours | rare |
- Conseil : préférez les exports nommés. Le nom est imposé, ce qui évite les incohérences, et VS Code peut ajouter automatiquement l'import quand vous tapez le nom d'une classe (Ctrl + .).
Organiser un projet : le fichier baril
- Quand un dossier contient plusieurs modules, on peut créer un fichier
indexqui réexporte leur contenu. Ce fichier « baril » (barrel) offre un seul point d'entrée au dossier.

centre/
├── models/
│ ├── personne.mjs
│ ├── stagiaire.mjs
│ ├── formateur.mjs
│ └── index.mjs ← baril
├── utils/
│ ├── calculs.mjs
│ └── rapport.mjs
└── app.mjs
// models/stagiaire.mjs
import { Personne } from './personne.mjs';
import { moyenne } from '../utils/calculs.mjs';
export class Stagiaire extends Personne {
constructor(nom, prenom, groupe, notes = []) {
super(nom, prenom);
this.groupe = groupe;
this.notes = notes;
}
get moyenne() {
return moyenne(this.notes);
}
presenter() {
return `${super.presenter()}, stagiaire en ${this.groupe} (${this.moyenne.toFixed(2)})`;
}
}
// models/index.mjs : le baril
export { Personne } from './personne.mjs';
export { Stagiaire } from './stagiaire.mjs';
export { Formateur } from './formateur.mjs';
// app.mjs : un seul import pour tout le dossier models
import { Stagiaire, Formateur } from './models/index.mjs';
import { estAdmis, SEUIL_ADMISSION as seuil } from './utils/calculs.mjs';
const groupe = [
new Stagiaire('Ben Ali', 'Amira', 'DEV1', [14, 16, 17]),
new Stagiaire('Trabelsi', 'Sami', 'DEV1', [8, 11, 9]),
];
const formateur = new Formateur('Gharbi', 'Ali', 'Angular');
console.log(formateur.presenter());
groupe.forEach((s) => console.log(s.presenter()));
console.log(`Admis (seuil ${seuil}) :`, groupe.filter(estAdmis).map((s) => s.prenom));
node app.mjs
Je m'appelle Ali Gharbi, formateur en Angular
Je m'appelle Amira Ben Ali, stagiaire en DEV1 (15.67)
Je m'appelle Sami Trabelsi, stagiaire en DEV1 (9.33)
Admis (seuil 10) : [ 'Amira' ]
-
Charger un module à la demande : import()
- La fonction
import()charge un module pendant l'exécution, seulement quand on en a besoin. Elle renvoie une promesse: on l'utilise avecawaitou.then(). - Le code du module n'est pas chargé au démarrage : l'application démarre plus vite.
- La fonction
// utils/rapport.mjs
console.log('(module rapport chargé)');
export function genererRapport(stagiaires) {
const lignes = stagiaires.map((s) => `- ${s.nomComplet} : ${s.moyenne.toFixed(2)}`);
return ['Rapport du groupe', ...lignes].join('\n');
}
// app.mjs (suite)
if (process.argv.includes('--rapport')) {
const { genererRapport } = await import('./utils/rapport.mjs');
console.log(genererRapport(groupe));
}
node app.mjs --rapport
…
(module rapport chargé)
Rapport du groupe
- Amira Ben Ali : 15.67
- Sami Trabelsi : 9.33
- Sans l'option
--rapport, le message « module rapport chargé » n'apparaît pas : le fichier n'a jamais été lu.
Classes et modules dans Angular
- Un fichier Angular est un module qui importe ce dont il a besoin et exporte sa classe :
// src/app/stagiaires/liste-stagiaires.ts
import { Component, OnInit } from '@angular/core'; // paquet npm : pas de ./
import { StagiaireService } from '../services/stagiaire.service'; // fichier du projet : chemin relatif
@Component({
selector: 'app-liste-stagiaires',
templateUrl: './liste-stagiaires.html',
})
export class ListeStagiaires implements OnInit { // export nommé d'une classe
titre = 'Mes stagiaires';
protected stagiaires: string[] = [];
readonly capacite = 25;
constructor(private service: StagiaireService) {} // propriété déclarée dans le constructeur
ngOnInit(): void {
this.stagiaires = this.service.lister();
}
get nombre(): number { // accesseur
return this.stagiaires.length;
}
}
- TypeScript complète les classes ECMAScript avec quelques ajouts que vous verrez dans le chapitre Programmer en TypeScript:
- les types des propriétés et des paramètres (
stagiaires: string[]) ; - les modificateurs d'accès
public,protectedetprivate, vérifiés à la compilation (par exempleTS2341: Property 'service' is private and only accessible within class 'ListeStagiaires'.) ; - les propriétés de paramètre:
constructor(private service: StagiaireService) {}déclare et remplit la propriété en une ligne ; implementspour respecter une interface (OnInitimpose la méthodengOnInit()) ;- les décorateurs comme
@Component, qui ajoutent des informations à la classe.
- les types des propriétés et des paramètres (
- Chargement à la demande: Angular utilise
import()pour ne charger une page que lorsque l'utilisateur la visite (lazy loading), vu dans le chapitre Routage:
// app.routes.ts
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin').then((m) => m.Admin),
},
];
-
Activité 1 : la classe Stagiaire
- Dans le dossier
rappel-ecmascript(dépôt GitHub), créer le fichier24-stagiaire.jset écrire une classeStagiaireavec :- les champs
groupe = 'Non affecté'etnotes = [], et un constructeur(nom, prenom); - une méthode
ajouterNote(note)qui renvoiethispour permettre l'enchaînement ; - les accesseurs
moyenne(0 sans note) etnomComplet(nom en majuscules), et unset nomCompletqui découpe « Prénom Nom » ; - une méthode
presenter().
- les champs
- Tester avec le code suivant :
- Dans le dossier
const amira = new Stagiaire('Ben Ali', 'Amira');
amira.groupe = 'DEV1';
amira.ajouterNote(14).ajouterNote(16).ajouterNote(17);
console.log(amira.presenter());
const sami = new Stagiaire('Trabelsi', 'Sami');
console.log(sami.presenter());
sami.nomComplet = 'Sami Ben Salah';
console.log(sami.nom, '|', sami.prenom);
// Résultat attendu :
// Amira BEN ALI (DEV1) – moyenne 15.67
// Sami TRABELSI (Non affecté) – moyenne 0.00
// Ben Salah | Sami
- Répondre en commentaire : que se passe-t-il si on déclare
notes = []dans le constructeur plutôt qu'en champ ? Et si on oublienew?
Activité 2 : encapsuler une formation
- Dans
25-formation.js, écrire la classeFormationavec un champ statiqueTVA, un compteur statique privé#nombrelisible par un accesseur statique, des champs privés#inscritset#capacite, une méthode privée#estComplete(), les accesseursplacesRestantesetprixTTC, unset capacitequi lève uneRangeErrorsi la valeur est invalide, une méthodeinscrire(nom)et une méthode statiquegratuite(titre). - Tester :
const web = new Formation('Développement web', 2, 1200);
console.log(web.inscrire('Amira'), '|', web.inscrire('Sami'), '|', web.inscrire('Karim'));
console.log(web.placesRestantes, web.prixTTC, web.inscrits, Formation.nombre);
const atelier = Formation.gratuite('Atelier Git');
console.log(atelier.titre, atelier.placesRestantes, Formation.nombre);
try {
web.capacite = 1;
} catch (e) {
console.log(e.name + ': ' + e.message);
}
web.capacite = 5;
console.log(web.placesRestantes);
// Résultat attendu :
// Amira inscrit(e) en Développement web | Sami inscrit(e) en Développement web | Karim : refusé, Développement web est complète
// 0 1428 undefined 1
// Atelier Git 30 2
// RangeError: Capacité invalide : 1
// 3
- Expliquer en commentaire pourquoi
web.inscritsvautundefined, et l'intérêt d'obliger à passer parinscrire().
Activité 3 : héritage et polymorphisme
- Dans
26-heritage.js, écrire les classesPersonne,StagiaireetFormateurde la leçon, puis ajouter une troisième classe dérivéeAdministratif(propriétéservice, par exemple « Scolarité »). - Créer un tableau contenant une instance de chaque classe et afficher
presenter()pour chacune avec une bouclefor…of. - Avec
filteretinstanceof, n'afficher que les formateurs, puis compter le nombre de personnes qui sont desPersonne. - Supprimer l'appel à
super(nom, prenom)dans un constructeur : noter le message d'erreur, puis le remettre.
Activité 4 : découper en modules
- Créer le dossier
27-centre/avec l'arborescence de la leçon :models/(une classe par fichier et un barilindex.mjs),utils/calculs.mjs(exports nommés et un export par défaut) etapp.mjs. - Exécuter
node 27-centre/app.mjs: le résultat doit être celui de la leçon. - Provoquer les trois erreurs suivantes une par une, noter le message obtenu en commentaire, puis corriger :
| Modification | Message attendu |
|---|---|
importer { SEUIL } au lieu de { SEUIL_ADMISSION } |
SyntaxError: The requested module './utils/calculs.mjs' does not provide an export named 'SEUIL' |
écrire './models/personne' sans l'extension .mjs |
Error [ERR_MODULE_NOT_FOUND]: Cannot find module … |
import Stagiaire from './models/stagiaire.mjs' (sans accolades) |
SyntaxError: … does not provide an export named 'default' |
- Remarque : Node.js exige l'extension complète du fichier dans un import relatif. Dans Angular, on l'omet (
'./models/stagiaire') : c'est l'outil de compilation qui retrouve le fichier.
Activité 5 : chargement à la demande
- Ajouter le module
utils/rapport.mjset le chargement avecawait import()présentés dans la leçon. - Exécuter
node 27-centre/app.mjs, puisnode 27-centre/app.mjs --rapport. Expliquer en commentaire pourquoi le message « (module rapport chargé) » n'apparaît que dans le second cas. - Ajouter
console.log('(module calculs chargé)');en haut deutils/calculs.mjs. Ce module est importé parapp.mjset parstagiaire.mjs: combien de fois le message s'affiche-t-il ? Pourquoi ? - Faire un commit après chaque activité, puis un push :
git commit -m "Classes et modules : activité N". Mettre à jour leREADME.mddu dépôt avec la liste des activités de la section.
Erreurs fréquentes
| Message | Cause | Solution |
|---|---|---|
TypeError: Class constructor X cannot be invoked without 'new' |
Classe appelée comme une fonction | Écrire new X(…) |
ReferenceError: Cannot access 'X' before initialization |
Classe utilisée avant sa déclaration | Déclarer la classe avant de l'utiliser |
ReferenceError: Must call super constructor in derived class … |
super() oublié, ou appelé après this |
Appeler super(…) en premier dans le constructeur |
SyntaxError: Private field '#x' must be declared in an enclosing class |
Accès à un champ privé hors de la classe, ou champ non déclaré | Déclarer #x dans la classe et passer par une méthode ou un accesseur |
TypeError: Cannot set property x of #<Y> which has only a getter |
Affectation d'un accesseur sans set |
Ajouter un set, ou ne pas modifier cette valeur calculée |
SyntaxError: The requested module … does not provide an export named 'x' |
Nom mal orthographié, export oublié, ou export par défaut importé entre accolades (et inversement) |
Vérifier le nom et le type d'export |
SyntaxError: Cannot use import statement outside a module |
Script classique : navigateur sans type="module", ou ancienne version de Node.js avec un fichier .js |
type="module", extension .mjs ou "type": "module" |
Error [ERR_MODULE_NOT_FOUND] |
Chemin incorrect ou extension oubliée (Node.js) | Vérifier le chemin relatif (./, ../) et l'extension |
-
À retenir
- Une classe est un modèle ;
newcrée une instance et appelle leconstructor. get/sets'utilisent comme des propriétés ;staticappartient à la classe ;#rend un membre privé.extendspermet l'héritage ;super()appelle le constructeur parent, obligatoirement avantthis.- Un module a sa propre portée et ne partage que ce qu'il exporte.
- Export nommé :
import { X }; export par défaut :import X. Angular utilise les exports nommés. - Un fichier baril
indexregroupe les exports d'un dossier ;import()charge un module à la demande.
- Une classe est un modèle ;
Un composant Angular, c'est une classe exportée par un module. Avec cette leçon, vous avez toutes les bases d'ECMAScript pour lire et écrire le code d'Angular.