À propos de ce cours
Un TP complet pour maîtriser TypeScript avant Angular. Vous créez pas à pas un programme de gestion des inscriptions : modèles typés, classes, paiements avec une classe abstraite, dépôt générique, service et décorateurs, avec un commit à chaque étape.
TP TypeScript : gestion des inscriptions du centre FormaPlus
-
Objectifs
- Ce TP de 6 heures clôt le chapitre Programmer en TypeScript. En le réalisant, vous allez apprendre à :
- créer et configurer un projet TypeScript avec Node.js :
tsconfig.json, mode strict, scripts npm, compilation avectsc; - utiliser les types de base, l'inférence, les tableaux, les tuples, les unions et les types littéraux, et comprendre la différence entre
anyetunknown; - écrire des fonctions typées: paramètres facultatifs et par défaut, type de retour, rétrécissement avec
typeof; - décrire les données de l'application avec des interfaces et des alias de
type, et les organiser en modules; - écrire des classes: propriétés de paramètre,
private,readonly,static, accesseurs ; - utiliser l'héritage, une classe abstraite,
overrideetimplements; - écrire une classe générique et utiliser les types utilitaires
OmitetPartial; - gérer les résultats d'une opération avec une union discriminée
Resultat<T>, et « programmer vers une interface » ; - écrire et appliquer des décorateurs de classe et de méthode ;
- lire les messages d'erreur du compilateur et les corriger ;
- versionner son travail avec Git et le publier sur GitHub dès le départ : dépôt, commit à chaque partie, push,
README.mdet version finale étiquetée.
- créer et configurer un projet TypeScript avec Node.js :
- Prérequis : avoir suivi les leçons du chapitre, en particulier Les classes en TypeScript et Les interfaces de classe en TypeScript, ainsi que les rappels ECMAScript (classes et modules, spread et rest).
- Avant la séance :
- Node.js en version LTS (vérifier avec
node -v) et VS Code; - un compte GitHub gratuit et Git installé (vérifier avec
git --version).
- Node.js en version LTS (vérifier avec
- Version de TypeScript : ce TP a été réalisé avec TypeScript 7.0, installé par
npm install -D typescript. Il fonctionne aussi avec TypeScript 6.
-
Description
- Le centre de formation FormaPlus veut un petit programme pour gérer les inscriptions à ses sessions de formation : enregistrer les stagiaires, les inscrire à une session en tenant compte des places, encaisser les paiements selon trois modes (espèces, carte, paiement en trois fois), prévenir les stagiaires et afficher un tableau de bord.
- Avant de construire l'interface web avec Angular, on écrit et on teste toute cette logique métier en TypeScript, exécutée avec Node.js dans le terminal. C'est exactement le code qu'on retrouvera plus tard dans les modèles et les services d'une application Angular.
- Le programme est organisé en modules :
| Dossier | Fichiers | Contenu | Notions TypeScript | Partie |
|---|---|---|---|---|
utils/ |
format.ts |
fonctions de mise en forme (montants, dates, mentions) | fonctions typées, unions, typeof |
02 |
models/ |
personne.ts, formation.ts, inscription.ts |
forme des données | interfaces, extends, types littéraux, union discriminée |
03 |
data/ |
donnees.ts |
jeu de données de départ | tableaux typés | 03 |
classes/ |
session.ts |
une session de formation et ses places | classe, private, readonly, static, accesseurs |
04 |
classes/ |
paiement.ts |
les trois modes de paiement | classe abstraite, héritage, override, implements |
05 |
classes/ |
depot.ts |
stockage générique des données | génériques, Omit, Partial |
06 |
services/ |
notificateur.ts, inscription.service.ts |
logique d'inscription et de paiement | programmer vers une interface, switch exhaustif |
07 |
decorateurs/ |
journal.ts |
journalisation et mesure du temps | décorateurs de classe et de méthode | 08 |
src/ |
main.ts |
programme principal et tableau de bord | intégration de tout le reste | 07 et 09 |
- Résultats attendus : chaque partie se termine par une capture « Résultat attendu » : l'affichage du terminal que vous devez obtenir. Comparez votre écran avec cette capture avant de faire votre commit.
- Fichier
reponses.md: les questions marquées (R) demandent une réponse écrite, à rédiger au fil du TP dans le fichierreponses.mdà la racine du projet. - Répartition du temps :
| Partie | Notions travaillées | Durée |
|---|---|---|
| Partie 00 – Mise en place du projet TypeScript et du dépôt GitHub | npm, tsconfig.json, scripts, Git |
25 min |
| Partie 01 – Types de base, inférence et unions | types primitifs, tableaux, tuples, unions, unknown |
35 min |
| Partie 02 – Fonctions typées | paramètres, retour, typeof, modules |
30 min |
| Partie 03 – Modèles de données | interfaces, extends, types littéraux |
35 min |
| Partie 04 – La classe Session | classe, private, readonly, static, get |
40 min |
| Partie 05 – Les paiements : héritage et abstraction | abstract, extends, override, implements |
40 min |
| Partie 06 – Un dépôt générique | <T>, contraintes, Omit, Partial |
40 min |
| Partie 07 – Le service d'inscription | union discriminée, interface, switch |
40 min |
| Partie 08 – Les décorateurs | décorateurs de méthode et de classe | 30 min |
| Partie 09 – Programme principal et vérifications | intégration, tsc --noEmit, source maps |
25 min |
| Partie 10 – Finalisation et version 1.0 sur GitHub | README.md, git tag |
20 min |
| Total | 6 h |
-
Travail à réaliser
-
Partie 00 Mise en place du projet TypeScript et du dépôt GitHub
- Q0.1. Créer le dossier du projet, l'ouvrir dans VS Code et installer TypeScript dans le projet (dépendance de développement) :
mkdir gestion-formaplus
cd gestion-formaplus
npm init -y
npm install -D typescript
npx tsc -v
code .
- Q0.2. Créer l'arborescence ci-dessous (à gauche) avec des fichiers vides, sauf
src/main.tsqui contient pour l'instant une seule ligne :console.log('FormaPlus – projet TypeScript prêt');. Créer aussi le fichier.gitignore(à droite).
gestion-formaplus/
├── .gitignore
├── package.json
├── tsconfig.json
├── README.md (Partie 10)
├── reponses.md (au fil du TP)
├── captures/ (au fil du TP)
└── src/
├── main.ts
├── utils/format.ts
├── models/
│ ├── personne.ts
│ ├── formation.ts
│ └── inscription.ts
├── data/donnees.ts
├── classes/
│ ├── session.ts
│ ├── paiement.ts
│ └── depot.ts
├── services/
│ ├── notificateur.ts
│ └── inscription.service.ts
├── decorateurs/journal.ts
└── exercices/ (fichiers de test p01 à p08)
# .gitignore
node_modules/
dist/
# Fichiers système
.DS_Store
Thumbs.db
- Q0.3. Créer le fichier
tsconfig.jsonsuivant (à gauche), puis modifierpackage.json: ajouter"type": "module"et remplacer la sectionscripts(à droite). (R) À quoi servent les optionsstrict,outDiretrootDir? Que fait chacun des quatre scripts ?
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"outDir": "dist",
"rootDir": "src",
"sourceMap": true
},
"include": ["src"]
}
{
"name": "gestion-formaplus",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"dev": "tsc --watch",
"verifier": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^7.0.2"
}
}
Les imports se terminent par
.js: avec "module": "NodeNext", on écrit import { calculerMoyenne } from '../utils/format.js'; même si le fichier source s'appelle format.ts. TypeScript ne modifie pas les chemins d'import : c'est le fichier .js compilé que Node.js chargera. Dans Angular, on omet l'extension, car c'est l'outil de compilation d'Angular qui retrouve les fichiers.- Q0.4. Compiler puis exécuter :
npm run build, puisnpm start. (R) Quel dossier est apparu ? Ouvrirdist/main.js: en quoi diffère-t-il desrc/main.ts? Que contientdist/main.js.map? - Q0.5. Créer
reponses.md(titre « TP TypeScript – Réponses » et votre nom) et le dossiercaptures/avec un fichier videcaptures/.gitkeep. -
Publier le projet sur GitHub dès le départ
- Q0.6. Sur github.com/new, créer un dépôt nommé
gestion-formaplus, sans README,.gitignoreni licence. Si le dépôt est privé, inviter l'enseignant comme collaborateur. - Q0.7. Configurer Git si ce n'est pas déjà fait, initialiser le dépôt local, faire le premier commit et l'envoyer :
# Identité (une seule fois par poste)
git config --global user.name "Prénom Nom"
git config --global user.email "adresse@exemple.com"
# Premier envoi du projet
git init
git add .
git commit -m "Partie 00 : création du projet TypeScript FormaPlus"
git branch -M main
git remote add origin https://github.com/VOTRE-COMPTE/gestion-formaplus.git
git push -u origin main
# Vérifications utiles
git status # fichiers modifiés
git log --oneline # historique des commits
git remote -v # adresse du dépôt GitHub
- Q0.8. (R) Sur GitHub, vérifier que
node_modules/etdist/n'apparaissent pas. Pourquoi ne versionne-t-on ni l'un ni l'autre ? Comment un camarade qui clone le dépôt obtiendra-t-il TypeScript et le dossierdist/? Envoyer le lien du dépôt à l'enseignant avant de commencer la partie 01.
Authentification : lors du premier
git push, GitHub n'accepte plus le mot de passe du compte. Connectez-vous par le navigateur quand Git le propose (Git Credential Manager ou VS Code), ou utilisez un Personal Access Token à la place du mot de passe. Un mot de passe ou un jeton ne doit jamais être écrit dans le code ni dans un fichier envoyé sur GitHub.Règle du TP : un commit et un push à la fin de chaque partie.
Chaque partie se termine par un encadré « Git » qui donne le message de commit attendu. L'historique des commits fait partie de la note : un dépôt qui ne contient qu'un seul commit déposé en fin de séance n'obtient aucun point sur le critère Git.
Question de réflexion (R) : le compilateur vous signale une erreur que vous ne comprenez pas après la partie 05. Comment l'historique des commits vous aide-t-il à retrouver ce qui a changé depuis la dernière version qui compilait ?
Chaque partie se termine par un encadré « Git » qui donne le message de commit attendu. L'historique des commits fait partie de la note : un dépôt qui ne contient qu'un seul commit déposé en fin de séance n'obtient aucun point sur le critère Git.
Question de réflexion (R) : le compilateur vous signale une erreur que vous ne comprenez pas après la partie 05. Comment l'historique des commits vous aide-t-il à retrouver ce qui a changé depuis la dernière version qui compilait ?
-
Résultat attendu à la fin de la partie 00
-
Partie 01 Types de base, inférence et unions
- Objectif : manipuler les types de TypeScript et observer ce que le compilateur vérifie.
- Q1.1. Créer
src/exercices/p01-types.tsavec le code ci-dessous, compiler avecnpx tscpuis exécuternode dist/exercices/p01-types.js.
// Partie 01 – Types de base et inférence
const centre: string = 'FormaPlus';
const anneeOuverture: number = 2010;
let ouvert: boolean = true;
let effectif = 120; // type déduit : number
const modules: string[] = ['HTML', 'CSS', 'TypeScript'];
const notes: Array<number> = [12, 15.5, 9];
const coordonnees: [string, number] = ['Tunis', 1002]; // tuple
let identifiant: string | number = 'STG-001'; // union
identifiant = 42;
type Niveau = 'Débutant' | 'Intermédiaire' | 'Avancé'; // types littéraux
const niveau: Niveau = 'Intermédiaire';
let donneeInconnue: unknown = JSON.parse('{"nom":"Amira"}');
if (typeof donneeInconnue === 'object' && donneeInconnue !== null && 'nom' in donneeInconnue) {
console.log('Nom reçu :', (donneeInconnue as { nom: string }).nom);
}
console.log(`${centre} (ouvert en ${anneeOuverture}) : ${effectif} stagiaires, ouvert = ${ouvert}`);
console.log(`Modules : ${modules.join(', ')} – moyenne des notes : ${(notes.reduce((s, n) => s + n, 0) / notes.length).toFixed(2)}`);
console.log(`Ville : ${coordonnees[0]}, code postal : ${coordonnees[1]}`);
console.log(`Identifiant : ${identifiant} (${typeof identifiant}) – niveau : ${niveau}`);
ouvert = false;
effectif += 5;
- Q1.2. (R) Survoler chaque variable dans VS Code : quel type TypeScript a-t-il déduit pour
effectif? Et pourniveaus'il était déclaréconst niveau = 'Intermédiaire';sans annotation ? Expliquer la différence. - Q1.3. Ajouter une par une les lignes du tableau ci-dessous à la fin du fichier, compiler, et (R) noter le message obtenu. Supprimer ou mettre en commentaire chaque ligne avant la suivante.
| Ligne ajoutée | Message attendu |
|---|---|
effectif = 'cent vingt'; |
TS2322: Type 'string' is not assignable to type 'number'. |
const niveau2: Niveau = 'Expert'; |
TS2322: Type '"Expert"' is not assignable to type 'Niveau'. |
const lieu: [string, number] = [1002, 'Tunis']; |
deux erreurs TS2322, à relever |
let u: unknown = 'abc'; u.toUpperCase(); |
TS18046: 'u' is of type 'unknown'. |
let x: any = 5; x.toUpperCase(); |
aucune erreur à la compilation : exécuter le programme et relever l'erreur obtenue |
- Q1.4. (R) D'après la dernière ligne du tableau, pourquoi faut-il éviter
any? Dans quel cas utilise-t-onunknownà la place ? Que fait le blocifplacé avant l'affichage du nom reçu ? - Q1.5. Ajouter un type
Groupequi n'accepte que'DEV1','DEV2'et'RES1', une variable de ce type, et l'afficher.
-
Résultat attendu à la fin de la partie 01
Git – fin de la partie 01 :
git add .
git commit -m "Partie 01 : types de base, unions et inférence"
git push
-
Partie 02 Fonctions typées
- Objectif : écrire des fonctions dont les paramètres et la valeur de retour sont typés, et les partager dans un module.
- Q2.1. Dans
src/utils/format.ts, recopier le code fourni (deux fonctions et le typeMention), puis écrire les quatre fonctions manquantes d'après le tableau.
export type Mention = 'Très bien' | 'Bien' | 'Assez bien' | 'Passable' | 'Ajourné';
export function formaterMontant(montant: number, devise: string = 'DT'): string {
return `${montant.toFixed(3)} ${devise}`;
}
export function calculerMoyenne(notes: readonly number[]): number {
if (notes.length === 0) return 0;
return notes.reduce((somme, note) => somme + note, 0) / notes.length;
}
// À vous : formaterDate, mention, formaterIdentifiant, initiales
| Fonction | Signature | Comportement |
|---|---|---|
formaterDate |
(date: Date): string |
renvoie la date au format jj/mm/aaaa (utiliser padStart) |
mention |
(moyenne: number): Mention |
≥ 16 « Très bien », ≥ 14 « Bien », ≥ 12 « Assez bien », ≥ 10 « Passable », sinon « Ajourné » |
formaterIdentifiant |
(id: string | number): string |
un nombre devient STG-007 (3 chiffres) ; une chaîne est mise en majuscules |
initiales |
(prenom: string, nom?: string): string |
A.B. si le nom est fourni, sinon A. |
- Q2.2. Créer
src/exercices/p02-fonctions.tsavec le code de test suivant, compiler et exécuter. Le résultat doit correspondre à la capture.
// Partie 02 – Fonctions typées
import { calculerMoyenne, formaterDate, formaterIdentifiant, formaterMontant, initiales, mention } from '../utils/format.js';
const notesAmira = [14, 16, 17];
const moyenne = calculerMoyenne(notesAmira);
console.log(`Moyenne : ${moyenne.toFixed(2)} – mention : ${mention(moyenne)}`);
console.log(`Moyenne sans note : ${calculerMoyenne([])} – mention : ${mention(0)}`);
console.log(`Frais : ${formaterMontant(1200)} – ${formaterMontant(350, '€')}`);
console.log(`Date de rentrée : ${formaterDate(new Date(2026, 8, 15))}`);
console.log(`Identifiants : ${formaterIdentifiant(7)} – ${formaterIdentifiant('stg-042')}`);
console.log(`Initiales : ${initiales('Amira', 'Ben Ali')} – ${initiales('Sami')}`);
type Operation = (a: number, b: number) => number;
const operations: Record<string, Operation> = {
addition: (a, b) => a + b,
moyenne: (a, b) => (a + b) / 2,
};
console.log(`Opérations : ${operations['addition'](12, 15)} et ${operations['moyenne'](12, 15)}`);
- Q2.3. (R) Dans
formaterIdentifiant, pourquoi peut-on appelerid.toUpperCase()après leif (typeof id === 'number')alors queidest de typestring | number? Comment s'appelle ce mécanisme ? - Q2.4. (R) Appeler
mention('15'): quel message obtient-on ? Supprimer une branchereturndemention: quel message obtient-on (optionnoImplicitReturns) ? Remettre le code correct. - Q2.5. (R) À quoi sert le mot-clé
readonlydans le paramètrenotes: readonly number[]? Essayer d'écrirenotes.push(20)dans la fonction.
-
Résultat attendu à la fin de la partie 02
utils/format.ts testées depuis un autre module.Git – fin de la partie 02 :
git add .
git commit -m "Partie 02 : fonctions typées (utils/format.ts)"
git push
-
Partie 03 Modèles de données
- Objectif : décrire la forme des données de l'application avec des interfaces et des alias de type, puis créer un jeu de données typé.
- Q3.1. Créer
src/models/personne.ts:
export interface Identifiable {
readonly id: number;
}
export interface Personne extends Identifiable {
nom: string;
prenom: string;
email: string;
telephone?: string;
}
export interface Stagiaire extends Personne {
groupe: string;
notes: number[];
dateInscription: Date;
}
export interface Formateur extends Personne {
specialites: string[];
tarifHoraire: number;
}
- Q3.2. Écrire vous-même les deux autres modèles :
src/models/formation.ts: un typeNiveau('Débutant' | 'Intermédiaire' | 'Avancé') et une interfaceFormationqui étendIdentifiableavectitre,niveau,dureeHeures,prixHTetformateurId;src/models/inscription.ts: les typesStatutInscription('en attente' | 'validée' | 'annulée') etModePaiement('especes' | 'carte' | 'facilite'), une interfaceInscription(stagiaireId,sessionId,statut,modefacultatif) et le type génériqueResultat<T>ci-dessous.
export type Resultat<T> =
| { succes: true; valeur: T }
| { succes: false; erreur: string };
Une union discriminée :
Resultat<T> vaut soit un succès avec une valeur, soit un échec avec un message d'erreur. La propriété succes permet à TypeScript de savoir lequel des deux cas on manipule : après if (r.succes), il sait que r.valeur existe. Les opérations du TP (inscrire, payer) renvoient un Resultat au lieu de lever une exception.- Q3.3. Créer
src/data/donnees.tsavec le code fourni, puis compléter le tableaustagiairesavec les quatre stagiaires suivants (attention :new Date(2026, 8, 16)correspond au 16 septembre, car les mois commencent à 0) :
import type { Formateur, Stagiaire } from '../models/personne.js';
import type { Formation } from '../models/formation.js';
export const formateurs: Formateur[] = [
{ id: 1, nom: 'Gharbi', prenom: 'Ali', email: 'ali.gharbi@formaplus.tn', specialites: ['Angular', 'TypeScript'], tarifHoraire: 45 },
{ id: 2, nom: 'Mansour', prenom: 'Leila', email: 'leila.mansour@formaplus.tn', specialites: ['Réseaux', 'Linux'], tarifHoraire: 40 },
];
export const formations: Formation[] = [
{ id: 1, titre: 'Développement Angular', niveau: 'Intermédiaire', dureeHeures: 120, prixHT: 1200, formateurId: 1 },
{ id: 2, titre: 'TypeScript pour débutants', niveau: 'Débutant', dureeHeures: 40, prixHT: 450, formateurId: 1 },
{ id: 3, titre: 'Administration Linux', niveau: 'Avancé', dureeHeures: 80, prixHT: 900, formateurId: 2 },
];
export const stagiaires: Stagiaire[] = [
{ id: 1, nom: 'Ben Ali', prenom: 'Amira', email: 'amira@exemple.tn', groupe: 'DEV1', notes: [14, 16, 17], dateInscription: new Date(2026, 8, 15) },
// À vous : les stagiaires 2 à 5 (tableau ci-dessous)
];
| id | nom | prenom | telephone | groupe | notes | dateInscription | |
|---|---|---|---|---|---|---|---|
| 2 | Trabelsi | Sami | sami@exemple.tn | +216 20 000 000 | DEV1 | 8, 11, 9 | 16/09/2026 |
| 3 | Jaziri | Yasmine | yasmine@exemple.tn | — | DEV2 | 12, 10, 15 | 18/09/2026 |
| 4 | Haddad | Karim | karim@exemple.tn | +216 50 000 000 | RES1 | 18, 16, 19 | 20/09/2026 |
| 5 | Bouazizi | Nour | nour@exemple.tn | — | RES1 | aucune | 01/10/2026 |
- Q3.4. Créer
src/exercices/p03-modeles.tsavec le code ci-dessous, compiler et exécuter.
// Partie 03 – Modèles de données
import { formateurs, formations, stagiaires } from '../data/donnees.js';
import type { Personne } from '../models/personne.js';
import { calculerMoyenne, formaterMontant, mention } from '../utils/format.js';
const contact = (p: Personne): string => `${p.prenom} ${p.nom.toUpperCase()} <${p.email}>${p.telephone ? ' – ' + p.telephone : ''}`;
console.log('--- Formateurs ---');
formateurs.forEach((f) => console.log(`${contact(f)} – ${f.specialites.join(', ')}`));
console.log('--- Formations ---');
for (const f of formations) {
const formateur = formateurs.find((x) => x.id === f.formateurId);
console.log(`${f.titre} (${f.niveau}, ${f.dureeHeures} h) – ${formaterMontant(f.prixHT)} HT – ${formateur?.prenom ?? '?'}`);
}
console.log('--- Stagiaires ---');
for (const s of stagiaires) {
const moy = calculerMoyenne(s.notes);
const resultat = s.notes.length ? `${moy.toFixed(2)} (${mention(moy)})` : 'aucune note';
console.log(`${contact(s)} – ${s.groupe} – ${resultat}`);
}
- Q3.5. (R) Pourquoi la fonction
contact, qui attend unePersonne, accepte-t-elle aussi bien un formateur qu'un stagiaire ? Pourquoi écrit-onimport typepour les interfaces ? - Q3.6. (R) Dans les données, changer un niveau en
'Expert', oublier l'emaild'un stagiaire, puis ajouter une propriétéage: relever les trois messages (TS2322,TS2741,TS2353) et corriger.
-
Résultat attendu à la fin de la partie 03
Git – fin de la partie 03 :
git add .
git commit -m "Partie 03 : modèles de données et jeu de données"
git push
-
Partie 04 La classe Session
- Objectif : écrire une classe qui protège ses données et expose des méthodes sûres.
- Q4.1. Créer
src/classes/session.tsavec le code fourni, puis ajouter:- l'accesseur
estComplete(booléen : plus aucune place) ; - l'accesseur
libelle:« Titre de la formation – jj/mm/aaaa »(utiliserformaterDate) ; - la méthode
inscrire(stagiaireId: number): Resultat<number>: échec« stagiaire X déjà inscrit »ou« session complète », sinon ajout de l'identifiant et succès avec le nombre de places restantes ; - la méthode
annuler(stagiaireId: number): boolean: retire le stagiaire et renvoietrues'il était inscrit.
- l'accesseur
import type { Formation } from '../models/formation.js';
import type { Resultat } from '../models/inscription.js';
import { formaterDate } from '../utils/format.js';
export class Session {
private static compteur = 0;
readonly id: number;
private inscrits: number[] = [];
constructor(
public readonly formation: Formation,
public readonly debut: Date,
private capacite: number,
) {
Session.compteur++;
this.id = Session.compteur;
}
static get nombre(): number {
return Session.compteur;
}
get placesRestantes(): number {
return this.capacite - this.inscrits.length;
}
// À vous : estComplete, libelle, inscrire(), annuler()
}
- Q4.2. Créer
src/exercices/p04-session.ts, compiler et exécuter :
// Partie 04 – Classes
import { Session } from '../classes/session.js';
import { formations } from '../data/donnees.js';
const angular = new Session(formations[0], new Date(2026, 10, 2), 2);
const linux = new Session(formations[2], new Date(2026, 10, 9), 10);
console.log(`Session ${angular.id} : ${angular.libelle} (${angular.placesRestantes} places)`);
console.log(`Session ${linux.id} : ${linux.libelle} (${linux.placesRestantes} places)`);
for (const id of [1, 2, 1, 3]) {
const r = angular.inscrire(id);
if (r.succes) {
console.log(`Stagiaire ${id} inscrit – reste ${r.valeur} place(s)`);
} else {
console.log(`Stagiaire ${id} refusé : ${r.erreur}`);
}
}
console.log(`Annulation du stagiaire 2 : ${angular.annuler(2)} – places : ${angular.placesRestantes}`);
console.log(`Nombre de sessions créées : ${Session.nombre}`);
- Q4.3. (R) Dans la boucle, pourquoi peut-on lire
r.valeurdans le premier bloc etr.erreurdans le second, mais pasr.valeuravant leif? Écrireconsole.log(r.valeur);juste aprèsconst r = …et relever le message. - Q4.4. (R) Écrire à la fin du fichier
angular.capacite = 50;puisangular.id = 7;: relever les deux messages. Ouvrirdist/classes/session.js: que sont devenusprivateetreadonly? Que se passerait-il si un fichier JavaScript (non compilé par TypeScript) modifiaitcapacite? - Q4.5. (R) Pourquoi le compteur est-il
static? Quelle valeur afficheraitSession.nombresi le compteur n'était pas statique ?
-
Résultat attendu à la fin de la partie 04
Git – fin de la partie 04 :
git add .
git commit -m "Partie 04 : classe Session"
git push
-
Partie 05 Les paiements : héritage et abstraction
- Objectif : modéliser plusieurs modes de paiement qui partagent un comportement commun, avec une classe abstraite et une interface.
- Q5.1. Créer
src/classes/paiement.tsavec le code fourni : une interfaceExportable, la classe abstraitePaiementet la classePaiementEspeces.
import { formaterMontant } from '../utils/format.js';
export interface Exportable {
exporterCsv(): string;
}
export abstract class Paiement implements Exportable {
static readonly TVA = 0.19;
constructor(protected readonly montantHT: number) {}
abstract get libelle(): string;
abstract frais(): number;
get montantTTC(): number {
return this.montantHT * (1 + Paiement.TVA);
}
total(): number {
return this.montantTTC + this.frais();
}
recu(): string {
return `${this.libelle.padEnd(22)} TTC ${formaterMontant(this.montantTTC)} + frais ${formaterMontant(this.frais())} = ${formaterMontant(this.total())}`;
}
exporterCsv(): string {
return [this.libelle, this.montantHT, this.total().toFixed(3)].join(';');
}
}
export class PaiementEspeces extends Paiement {
get libelle(): string {
return 'Espèces';
}
frais(): number {
return 0;
}
}
// À vous : PaiementCarte et PaiementFacilite
- Q5.2. Écrire les deux classes manquantes :
PaiementCarte: libellé « Carte bancaire », frais de 1,5 % du montant TTC ;PaiementFacilite: reçoit en plus le nombre demensualites(propriété de paramètre privée, en lecture seule) et appellesuper(montantHT); libellé « Facilité N fois », frais de 10 DT par mensualité, une méthodeecheance()(total ÷ mensualités) et une redéfinition derecu()qui ajoute(3 × 486.000 DT)au reçu parent.
- Q5.3. Créer
src/exercices/p05-paiements.ts, compiler et exécuter :
// Partie 05 – Héritage, classes abstraites et interfaces
import { type Exportable, Paiement, PaiementCarte, PaiementEspeces, PaiementFacilite } from '../classes/paiement.js';
const paiements: Paiement[] = [new PaiementEspeces(1200), new PaiementCarte(1200), new PaiementFacilite(1200, 3)];
paiements.forEach((p) => console.log(p.recu()));
const aExporter: Exportable[] = paiements;
console.log('libelle;montantHT;total');
aExporter.forEach((e) => console.log(e.exporterCsv()));
- Q5.4. (R) Provoquer et relever les messages suivants, puis corriger :
- retirer
overridedevantrecu()dansPaiementFacilite(TS4114) ; - supprimer la méthode
frais()dePaiementCarte(TS2515) ; - écrire
new Paiement(100)dans le fichier de test (TS2511).
- retirer
- Q5.5. (R) La classe
PaiementimplémenteExportable: les classes enfants doivent-elles l'écrire elles aussi ? Pourquoi peut-on ranger les paiements dans un tableauExportable[]? Quel est l'intérêt deprotectedpourmontantHTplutôt queprivate?
-
Résultat attendu à la fin de la partie 05
recu(), puis export CSV grâce à l'interface Exportable. Git – fin de la partie 05 :
git add .
git commit -m "Partie 05 : paiements, classe abstraite et interface Exportable"
git push
-
Partie 06 Un dépôt générique
- Objectif : écrire une seule classe capable de stocker n'importe quel type de données identifiées (stagiaires, formations, inscriptions…).
- Q6.1. Créer
src/classes/depot.tsavec le code fourni, puis ajouter:filtrer(critere: (element: T) => boolean): T[];modifier(id: number, changements: Partial<Omit<T, 'id'>>): T | undefined: crée une nouvelle version de l'élément avec spread et remplace l'ancienne avecmap;supprimer(id: number): boolean.
import type { Identifiable } from '../models/personne.js';
export class Depot<T extends Identifiable> {
private elements: T[] = [];
constructor(elementsInitiaux: T[] = []) {
this.elements = [...elementsInitiaux];
}
get taille(): number {
return this.elements.length;
}
tous(): readonly T[] {
return this.elements;
}
trouver(id: number): T | undefined {
return this.elements.find((e) => e.id === id);
}
ajouter(donnees: Omit<T, 'id'>): T {
const id = Math.max(0, ...this.elements.map((e) => e.id)) + 1;
const element = { ...donnees, id } as T;
this.elements.push(element);
return element;
}
// À vous : filtrer(), modifier(), supprimer()
}
- Q6.2. Créer
src/exercices/p06-depot.ts, compiler et exécuter :
// Partie 06 – Génériques et types utilitaires
import { Depot } from '../classes/depot.js';
import { formations, stagiaires } from '../data/donnees.js';
import type { Formation } from '../models/formation.js';
import type { Stagiaire } from '../models/personne.js';
const depotStagiaires = new Depot<Stagiaire>(stagiaires);
const depotFormations = new Depot<Formation>(formations);
const nouveau = depotStagiaires.ajouter({
nom: 'Khelifi', prenom: 'Ines', email: 'ines@exemple.tn', groupe: 'DEV2', notes: [], dateInscription: new Date(2026, 9, 20),
});
console.log(`Ajout : ${nouveau.prenom} avec l'id ${nouveau.id} – total : ${depotStagiaires.taille}`);
const modifie = depotStagiaires.modifier(2, { groupe: 'DEV2', telephone: '+216 21 111 111' });
console.log(`Modification : ${modifie?.prenom} est maintenant en ${modifie?.groupe}`);
console.log(`Modification d'un id inconnu : ${depotStagiaires.modifier(99, { groupe: 'X' }) ?? 'introuvable'}`);
const dev2 = depotStagiaires.filtrer((s) => s.groupe === 'DEV2').map((s) => s.prenom);
console.log(`Groupe DEV2 : ${dev2.join(', ')}`);
console.log(`Suppression de l'id 5 : ${depotStagiaires.supprimer(5)} – puis encore : ${depotStagiaires.supprimer(5)}`);
const debutant = depotFormations.filtrer((f) => f.niveau === 'Débutant')[0];
console.log(`Formation débutant : ${debutant?.titre}`);
console.log(`Stagiaires : ${depotStagiaires.tous().map((s) => s.id).join(', ')}`);
- Q6.3. (R) Expliquer le rôle de
T extends Identifiable. Écrirenew Depot<string>()et relever le message (TS2344). - Q6.4. (R) Pourquoi la méthode
ajouterreçoit-elle unOmit<T, 'id'>? AppelerdepotStagiaires.ajouter({ nom: 'X' })et relever le message (TS2739). Pourquoimodifierutilise-t-ilPartial? - Q6.5. (R) La méthode
tous()renvoie unreadonly T[]: essayerdepotStagiaires.tous().push(…)depuis le fichier de test. Pourquoi est-ce une bonne protection ?
-
Résultat attendu à la fin de la partie 06
Depot<Stagiaire>. Git – fin de la partie 06 :
git add .
git commit -m "Partie 06 : dépôt générique et types utilitaires"
git push
-
Partie 07 Le service d'inscription
- Objectif : assembler les classes dans un service qui applique les règles métier, en dépendant d'une interface pour les notifications.
- Q7.1. Créer
src/services/notificateur.ts:
export interface Notificateur {
readonly canal: string;
envoyer(destinataire: string, message: string): void;
}
export class NotificateurConsole implements Notificateur {
readonly canal = 'console';
envoyer(destinataire: string, message: string): void {
console.log(` ✉ [${this.canal}] à ${destinataire} : ${message}`);
}
}
export class NotificateurJournal implements Notificateur {
readonly canal = 'journal';
private messages: string[] = [];
envoyer(destinataire: string, message: string): void {
this.messages.push(`${destinataire} – ${message}`);
}
get historique(): readonly string[] {
return this.messages;
}
}
- Q7.2. Créer
src/services/inscription.service.tsavec le code fourni, puis ajouter:payer(inscriptionId: number, mode: ModePaiement): Resultat<Paiement>: échec si l'inscription est introuvable ou n'est plus « en attente » ; sinon création du paiement, passage du statut à « validée » avecmodifier(), notification du stagiaire et succès avec le paiement ;- une méthode privée
creerPaiement(mode: ModePaiement, montantHT: number): Paiementqui utilise unswitchsur les trois modes (la facilité se fait en 3 fois) ; parStatut(): Record<string, number>qui compte les inscriptions de chaque statut.
import { Depot } from '../classes/depot.js';
import { type Paiement, PaiementCarte, PaiementEspeces, PaiementFacilite } from '../classes/paiement.js';
import { Session } from '../classes/session.js';
import type { Inscription, ModePaiement, Resultat } from '../models/inscription.js';
import type { Stagiaire } from '../models/personne.js';
import { formaterMontant } from '../utils/format.js';
import type { Notificateur } from './notificateur.js';
export class InscriptionService {
private readonly inscriptions = new Depot<Inscription>();
constructor(
private readonly stagiaires: Depot<Stagiaire>,
private readonly sessions: Session[],
private readonly notificateur: Notificateur,
) {}
inscrire(stagiaireId: number, sessionId: number): Resultat<Inscription> {
const stagiaire = this.stagiaires.trouver(stagiaireId);
const session = this.sessions.find((s) => s.id === sessionId);
if (!stagiaire) return { succes: false, erreur: `stagiaire ${stagiaireId} introuvable` };
if (!session) return { succes: false, erreur: `session ${sessionId} introuvable` };
const place = session.inscrire(stagiaireId);
if (!place.succes) return { succes: false, erreur: place.erreur };
const inscription = this.inscriptions.ajouter({ stagiaireId, sessionId, statut: 'en attente' });
this.notificateur.envoyer(stagiaire.email, `inscription en attente pour « ${session.libelle} »`);
return { succes: true, valeur: inscription };
}
// À vous : payer(), creerPaiement() (privée) et parStatut()
}
- Q7.3. Remplacer le contenu de
src/main.tspar le programme ci-dessous, puis lancernpm run buildetnpm start.
import { Depot } from './classes/depot.js';
import { Session } from './classes/session.js';
import { formateurs, formations, stagiaires } from './data/donnees.js';
import type { Stagiaire } from './models/personne.js';
import { InscriptionService } from './services/inscription.service.js';
import { NotificateurConsole } from './services/notificateur.js';
import { calculerMoyenne, formaterMontant, mention } from './utils/format.js';
console.log('=== FormaPlus – gestion des inscriptions ===');
const depotStagiaires = new Depot<Stagiaire>(stagiaires);
const sessions = [
new Session(formations[0], new Date(2026, 10, 2), 2),
new Session(formations[1], new Date(2026, 10, 4), 12),
new Session(formations[2], new Date(2026, 10, 9), 10),
];
const inscriptions = new InscriptionService(depotStagiaires, sessions, new NotificateurConsole());
console.log('\n--- Inscriptions ---');
const demandes: [number, number][] = [[1, 1], [2, 1], [3, 1], [4, 3], [9, 2]];
for (const [stagiaireId, sessionId] of demandes) {
const r = inscriptions.inscrire(stagiaireId, sessionId);
console.log(r.succes ? ` ✔ inscription n° ${r.valeur.id} créée` : ` ✘ refus : ${r.erreur}`);
}
console.log('\n--- Paiements ---');
const reglements: [number, 'especes' | 'carte' | 'facilite'][] = [[1, 'carte'], [3, 'facilite'], [1, 'especes']];
for (const [id, mode] of reglements) {
const r = inscriptions.payer(id, mode);
console.log(r.succes ? ` ✔ ${r.valeur.recu()}` : ` ✘ refus : ${r.erreur}`);
}
- Q7.4. (R) Le
switchdecreerPaiementn'a pas dedefault, et pourtant TypeScript ne signale aucun oubli dereturn. Pourquoi ? Supprimer lecase 'facilite'et relever le message obtenu (TS2366). Ajouter'virement'au typeModePaiement: où le compilateur vous signale-t-il qu'il faut compléter le code ? - Q7.5. (R) Le service ne connaît que l'interface
Notificateur. Dansmain.ts, remplacernew NotificateurConsole()par une instance deNotificateurJournal, puis afficher sonhistoriqueà la fin du programme. Avez-vous modifiéInscriptionService? Quel est l'intérêt de cette organisation ? Revenir ensuite auNotificateurConsole.
-
Résultat attendu à la fin de la partie 07
Git – fin de la partie 07 :
git add .
git commit -m "Partie 07 : service d'inscription et notificateurs"
git push
-
Partie 08 Les décorateurs
- Objectif : comprendre ce qu'est un décorateur en écrivant les siens, avant de retrouver ceux d'Angular (
@Component,@Injectable…). - Un décorateur est une fonction appliquée à une classe ou à une méthode avec la syntaxe
@nom. Il peut ajouter un comportement (journaliser un appel, mesurer une durée…) sans modifier le code de la méthode décorée. - Q8.1. Créer
src/decorateurs/journal.tsavec deux décorateurs :journaliser(méthode) affiche le nom et les arguments de chaque appel ;service(classe) affiche un message quand la classe est définie.
export function journaliser<This, Args extends unknown[], Retour>(
methode: (this: This, ...args: Args) => Retour,
contexte: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Retour>,
) {
const nom = String(contexte.name);
return function (this: This, ...args: Args): Retour {
console.log(` ▶ ${nom}(${args.map((a) => JSON.stringify(a)).join(', ')})`);
const resultat = methode.call(this, ...args);
return resultat;
};
}
export function service(nom: string) {
return function <T extends abstract new (...args: any[]) => unknown>(classe: T, contexte: ClassDecoratorContext<T>): T {
console.log(`[décorateur] service « ${nom} » enregistré (classe ${String(contexte.name)})`);
return classe;
};
}
// À vous : mesurer
- Q8.2. Sur le même modèle que
journaliser, écrire le décorateur de méthodemesurerqui exécute la méthode, puis affiche sa durée d'exécution :⏱ nom : 0.056 ms(utiliserperformance.now()avant et après l'appel). - Q8.3. Créer
src/exercices/p08-decorateurs.ts, compiler et exécuter :
// Partie 08 – Décorateurs
import { journaliser, mesurer, service } from '../decorateurs/journal.js';
@service('calculs')
class Calculatrice {
@journaliser
@mesurer
moyenne(...notes: number[]): number {
return notes.reduce((s, n) => s + n, 0) / notes.length;
}
}
const calc = new Calculatrice();
console.log(`Résultat : ${calc.moyenne(14, 16, 17).toFixed(2)}`);
- Q8.4. Dans
inscription.service.ts, importer les décorateurs et les appliquer :@service('inscriptions')sur la classe,@journalisersurinscrire()etpayer(). Relancernpm run buildetnpm start. - Q8.5. (R) Le message « service enregistré » s'affiche avant le titre du programme : pourquoi ? Dans
p08-decorateurs.ts, inverser l'ordre de@journaliseret@mesurer: l'affichage change-t-il ? Dans quel ordre les décorateurs sont-ils appliqués ? - Q8.6. (R) Citer trois décorateurs d'Angular et ce qu'ils indiquent au framework.
Deux systèmes de décorateurs : ce TP utilise les décorateurs standard d'ECMAScript, pris en charge par TypeScript depuis la version 5.0, sans option particulière. Angular utilise encore l'ancienne version « expérimentale » (option
experimentalDecorators dans le tsconfig.json d'un projet Angular), dont la signature est différente. Dans Angular, vous utiliserez des décorateurs fournis par le framework, sans avoir à les écrire.-
Résultat attendu à la fin de la partie 08
Git – fin de la partie 08 :
git add .
git commit -m "Partie 08 : décorateurs journaliser, mesurer et service"
git push
-
Partie 09 Programme principal et vérifications
- Objectif : terminer le programme avec un tableau de bord, et vérifier la qualité du projet.
- Q9.1. Compléter
src/main.ts: ajouter l'import deformateurset decalculerMoyenne,formaterMontant,mention, puis, à la fin du fichier, le tableau de bord suivant :
console.log('\n--- Tableau de bord ---');
for (const s of sessions) {
const formateur = formateurs.find((f) => f.id === s.formation.formateurId);
console.log(`${s.libelle.padEnd(45)} ${String(s.placesRestantes).padStart(2)} place(s) – ${formateur?.prenom} ${formateur?.nom}`);
}
console.log('Inscriptions par statut :', inscriptions.parStatut());
const classement = depotStagiaires
.filtrer((s) => s.notes.length > 0)
.map((s) => ({ nom: `${s.prenom} ${s.nom}`, moyenne: calculerMoyenne(s.notes) }))
.toSorted((a, b) => b.moyenne - a.moyenne);
console.log('Classement :');
classement.forEach((c, i) => console.log(` ${i + 1}. ${c.nom.padEnd(16)} ${c.moyenne.toFixed(2)} (${mention(c.moyenne)})`));
const chiffreAffaires = formations.reduce((t, f) => t + f.prixHT, 0);
console.log(`Catalogue : ${formations.length} formations, ${formaterMontant(chiffreAffaires)} HT au total`);
- Q9.2. Lancer
npm run verifier: (R) que fait ce script, et pourquoi est-il plus rapide quenpm run build? Dans quelle situation l'utiliser ? - Q9.3. (R) Le classement utilise
toSorted(), une méthode d'ES2023. Remplacer temporairement"target": "ES2023"par"ES2022"danstsconfig.jsonet compiler : quel message obtenez-vous ? Que vérifie TypeScript grâce à l'optiontarget? RemettreES2023. - Q9.4. (R) Provoquer une erreur à l'exécution : dans
payer(), remplacerthis.sessions.find(…)!par une recherche d'un identifiant inexistant, puis lancernode --enable-source-maps dist/main.js. La pile d'erreur indique-t-elle le fichier.tsou le fichier.js? À quoi sert le fichier.map? Que signifie le!aprèsfind(…), et pourquoi est-il risqué ? Revenir au code correct.
-
Résultat attendu à la fin de la partie 09
Git – fin de la partie 09 :
git add .
git commit -m "Partie 09 : programme principal et vérifications"
git push
-
Partie 10 Finalisation et version 1.0 sur GitHub
- Q10.1. Faire les captures dans
captures/:npm startcomplet, un exemple d'erreur de compilation relevé pendant le TP, et l'arborescence du projet dans VS Code. - Q10.2. Rédiger le
README.mden suivant le modèle ci-dessous (à gauche). - Q10.3. Vérifier que
reponses.mdest complet, et qu'aucun fichier denode_modules/ou dedist/n'apparaît dans le dépôt :git ls-files | findstr "node_modules dist"sous Windows (git ls-files | grep -E "node_modules|dist"sous Linux et macOS) ne doit rien afficher. - Q10.4. Faire le dernier commit, puis marquer la version finale avec l'étiquette
v1.0et l'envoyer sur GitHub (à droite).
# Gestion des inscriptions FormaPlus
Programme TypeScript réalisé dans le cadre du TP TypeScript :
gestion des stagiaires, des sessions, des inscriptions et des paiements.
## Notions utilisées
- Types, unions, types littéraux, unknown
- Interfaces, extends, Resultat<T> (union discriminée)
- Classes, private, readonly, static, accesseurs
- Classe abstraite, héritage, override, implements
- Classe générique Depot<T>, Omit, Partial
- Décorateurs de classe et de méthode
## Lancer le projet
npm install
npm run build
npm start
## Captures

## Auteur
Prénom Nom – Groupe
git add .
git commit -m "Partie 10 : README, captures et version 1.0"
git push
git tag v1.0
git push origin v1.0
-
Résultat attendu à la fin de la partie 10
v1.0 envoyée sur GitHub, aucun fichier de node_modules/ ni de dist/ versionné.-
Exercices complémentaires (bonus)
- Annulation : ajouter une méthode
annuler(inscriptionId)au service, qui libère la place dans la session (méthodeannulerdeSession), passe le statut à « annulée » et prévient le stagiaire. Une inscription validée ne peut pas être annulée. - Nouveau mode de paiement : ajouter
'virement'àModePaiementet une classePaiementVirement(frais fixes de 2,500 DT). Laisser le compilateur vous guider vers tous les endroits à compléter. - Décorateur avec paramètre : écrire un décorateur
@limiter(3)qui refuse d'exécuter une méthode plus de 3 fois et renvoie un échecResultat. - Sauvegarde : écrire une classe
StockageFichierqui enregistre les inscriptions dans un fichierinscriptions.jsonavec le modulenode:fs(writeFileSync,readFileSync) et les relit au démarrage. - Tests automatiques : installer Vitest (
npm install -D vitest) et écrire des tests pourmention(),Session.inscrire()etDepot.modifier().
-
Compte rendu
- À rendre :
- le lien de votre dépôt GitHub
gestion-formaplus, envoyé dès la partie 00 : c'est ce dépôt qui est corrigé, dans l'état de l'étiquettev1.0; - dans le dépôt, le fichier
reponses.md: réponses aux questions marquées (R), soit Q0.3, Q0.4, Q0.8, Q1.2, Q1.3, Q1.4, Q2.3, Q2.4, Q2.5, Q3.5, Q3.6, Q4.3, Q4.4, Q4.5, Q5.4, Q5.5, Q6.3, Q6.4, Q6.5, Q7.4, Q7.5, Q8.5, Q8.6, Q9.2, Q9.3, Q9.4 et les questions de réflexion ; - dans le dépôt, le code complet dans
src/, leREADME.mdet les captures du dossiercaptures/.
- le lien de votre dépôt GitHub
- Barème indicatif (/20) :
| Critère | Points |
|---|---|
Partie 00 – projet TypeScript, tsconfig.json, scripts npm, dépôt GitHub |
1,5 |
Partie 01 – types de base, unions, tuples, any / unknown |
1,5 |
| Partie 02 – fonctions typées, paramètres facultatifs, rétrécissement | 1,5 |
Partie 03 – interfaces, extends, types littéraux, données complètes |
2 |
Partie 04 – classe Session: encapsulation, accesseurs, Resultat |
2 |
Partie 05 – classe abstraite, héritage, override, implements |
2,5 |
Partie 06 – dépôt générique, contrainte, Omit / Partial |
2 |
Partie 07 – service, union discriminée, interface Notificateur |
2,5 |
Partie 08 – décorateurs journaliser, mesurer et service |
1,5 |
| Partie 09 – programme principal complet, vérifications | 1 |
Git / GitHub – un commit par partie avec un message clair, ni node_modules/ ni dist/ versionnés, README.md, étiquette v1.0 |
2 |
| Total | 20 |
| Bonus | +2 max |
Question de réflexion (à rédiger en 5 lignes) :
Pendant ce TP, le compilateur TypeScript a refusé plusieurs de vos programmes avant même leur exécution. Choisissez une erreur précise rencontrée pendant la séance, expliquez ce qui se serait passé à l'exécution si le même code avait été écrit en JavaScript, et dites en quoi cela vous sera utile dans une application Angular.
Pendant ce TP, le compilateur TypeScript a refusé plusieurs de vos programmes avant même leur exécution. Choisissez une erreur précise rencontrée pendant la séance, expliquez ce qui se serait passé à l'exécution si le même code avait été écrit en JavaScript, et dites en quoi cela vous sera utile dans une application Angular.