À propos de ce cours
Cette leçon montre ce que TypeScript ajoute aux classes JavaScript : le typage des propriétés, des paramètres et des valeurs de retour, les règles d'initialisation du mode strict, les modificateurs public, protected, private et readonly, et les propriétés de paramètre qui raccourcissent les constructeurs. Vous utiliserez l'héritage avec le mot-clé override, créerez des classes abstraites et des classes génériques, et verrez ce que le compilateur efface dans le JavaScript produit. La leçon se termine par un service et un composant Angular annotés, avec les conventions private, protected et readonly. Des schémas et six activités pratiques accompagnent chaque notion.
-
Objectifs
- Typer les propriétés, les paramètres et les valeurs de retour d'une classe
- Comprendre les règles d'initialisation des propriétés en mode strict
- Utiliser les modificateurs d'accès
public,protected,privateet le modificateurreadonly - Écrire des classes plus courtes grâce aux propriétés de paramètre
- Utiliser l'héritage avec le mot-clé
override, et créer des classes abstraites - Écrire une classe générique
- Appliquer ces notions dans les composants et les services Angular
-
Présentation
- TypeScript reprend les classes d'ECMAScript (voir Les classes et les modules ES6) et y ajoute ce qui manque à JavaScript pour les grands projets :
- les types des propriétés, des paramètres et des valeurs de retour ;
- les modificateurs d'accès (
public,protected,private) etreadonly; - les propriétés de paramètre, qui déclarent et remplissent une propriété en un seul mot ;
- les classes et méthodes abstraites, le mot-clé
override, les classes génériques.
- Toutes ces vérifications ont lieu à la compilation. Le compilateur
tscproduit ensuite un JavaScript ordinaire, d'où les types et les modificateurs ont disparu. - Dans Angular, tout est classe : composants, services, pipes, directives, guards. Lire et écrire une classe TypeScript est donc la compétence de base du développeur Angular.
- TypeScript reprend les classes d'ECMAScript (voir Les classes et les modules ES6) et y ajoute ce qui manque à JavaScript pour les grands projets :

-
Préparer l'environnement de test
- Créer un dossier
rappel-typescript, l'initialiser avec Git et npm, et installer TypeScript dans le projet :
- Créer un dossier
mkdir rappel-typescript
cd rappel-typescript
git init
npm init -y
npm install -D typescript
mkdir src
- Créer à la racine le fichier
tsconfig.jsonci-dessous, et ajouter"type": "module"danspackage.json. Les optionsstrictetnoImplicitOverridesont les mêmes que dans un projet Angular.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"noImplicitOverride": true,
"outDir": "dist",
"rootDir": "src"
}
}
- On écrit les fichiers
.tsdanssrc/, on compile tout le projet avecnpx tsc, puis on exécute le JavaScript produit dansdist/:
npx tsc # compile src/*.ts vers dist/*.js et affiche les erreurs
node dist/stagiaire.js # exécute le fichier compilé
npx tsc --watch # recompile automatiquement à chaque sauvegarde
- Git : ajouter un fichier
.gitignorecontenantnode_modules/etdist/, puis publier le dossier sur un dépôt GitHubrappel-typescript.
Typer une classe
- Chaque propriété est déclarée avec son type ; chaque méthode indique le type de ses paramètres et de sa valeur de retour (
voidsi elle ne renvoie rien). - Une propriété optionnelle s'écrit avec
?: son type devientstring | undefined.
// src/stagiaire.ts
class Stagiaire {
nom: string;
prenom: string;
groupe: string = 'Non affecté'; // valeur par défaut : le type pourrait être déduit
notes: number[] = [];
telephone?: string; // optionnelle
constructor(nom: string, prenom: string) {
this.nom = nom;
this.prenom = prenom;
}
ajouterNote(note: number): this { // renvoie l'objet lui-même (enchaînement)
this.notes.push(note);
return this;
}
get moyenne(): number {
if (this.notes.length === 0) return 0;
return this.notes.reduce((s, n) => s + n, 0) / this.notes.length;
}
presenter(): string {
return `${this.prenom} ${this.nom} (${this.groupe}) – ${this.moyenne.toFixed(2)}`;
}
}
const amira = new Stagiaire('Ben Ali', 'Amira');
amira.groupe = 'DEV1';
amira.ajouterNote(14).ajouterNote(16).ajouterNote(17);
console.log(amira.presenter()); // Amira Ben Ali (DEV1) – 15.67
console.log(amira.telephone ?? 'pas de téléphone'); // pas de téléphone
- Le compilateur signale alors les erreurs avant l'exécution :
| Code | Message de tsc |
|---|---|
amira.notes.push('18'); |
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. |
new Stagiaire('Ben Ali'); |
TS2554: Expected 2 arguments, but got 1. |
amira.age = 20; |
TS2339: Property 'age' does not exist on type 'Stagiaire'. |
amira.moyenne = 20; (accesseur sans set) |
TS2540: Cannot assign to 'moyenne' because it is a read-only property. |
- Une classe est aussi un type : on peut écrire
const liste: Stagiaire[] = [];oufunction afficher(s: Stagiaire): void.
L'initialisation des propriétés en mode strict
- Avec
"strict": true, chaque propriété doit recevoir une valeur, soit à sa déclaration, soit dans le constructeur. Sinon :
class Stagiaire {
nom: string;
groupe: string; // ni valeur par défaut, ni affectation dans le constructeur
constructor(nom: string) {
this.nom = nom;
}
}
// TS2564: Property 'groupe' has no initializer and is not definitely assigned in the constructor.
- Trois solutions, selon le sens de la propriété :
- lui donner une valeur par défaut:
groupe = 'Non affecté';; - la rendre optionnelle si elle peut vraiment être absente :
groupe?: string;; - utiliser l'assertion d'affectation
!si elle est remplie plus tard par un autre mécanisme :groupe!: string;. À utiliser avec prudence : TypeScript vous fait alors confiance.
- lui donner une valeur par défaut:
Les modificateurs d'accès et readonly
- TypeScript permet de préciser qui peut utiliser chaque membre d'une classe :
public: accessible partout. C'est la valeur par défaut, le mot-clé est donc facultatif ;protected: accessible dans la classe et dans ses classes enfants ;private: accessible uniquement dans la classe ;readonly: lisible partout, mais modifiable seulement à la déclaration ou dans le constructeur.

class Compte {
private solde = 0;
protected titulaire = 'Amira Ben Ali';
readonly numero = 'TN59-0001';
deposer(montant: number): void {
this.solde += montant; // autorisé : on est dans la classe
}
}
const c = new Compte();
c.deposer(100);
c.solde = 100; // TS2341: Property 'solde' is private and only accessible within class 'Compte'.
c.titulaire; // TS2445: Property 'titulaire' is protected and only accessible within class
// 'Compte' and its subclasses.
c.numero = 'TN02'; // TS2540: Cannot assign to 'numero' because it is a read-only property.
privateou#?privateest une règle de TypeScript, vérifiée seulement à la compilation : dans le JavaScript produit, la propriété reste accessible. Le champ#d'ECMAScript est protégé à l'exécution. Dans Angular, on utilise surtoutprivate, plus lisible et suffisant puisque tout le code passe par le compilateur.
Les propriétés de paramètre
- Placer un modificateur (
public,protected,privateoureadonly) devant un paramètre du constructeur déclare la propriété et lui affecte automatiquement la valeur reçue.

// src/formation.ts
class Formation {
static readonly TVA = 0.19;
private static nombre = 0;
private inscrits: string[] = [];
constructor(
public readonly titre: string,
private capacite: number,
protected prixHT: number = 0,
) {
Formation.nombre++;
}
static get total(): number {
return Formation.nombre;
}
get placesRestantes(): number {
return this.capacite - this.inscrits.length;
}
get prixTTC(): number {
return +(this.prixHT * (1 + Formation.TVA)).toFixed(3);
}
inscrire(nom: string): boolean {
if (this.placesRestantes === 0) return false;
this.inscrits.push(nom);
return true;
}
}
const web = new Formation('Développement web', 2, 1200);
console.log(web.inscrire('Amira'), web.inscrire('Sami'), web.inscrire('Karim')); // true true false
console.log(web.titre, web.placesRestantes, web.prixTTC); // Développement web 0 1428
- C'est exactement l'écriture que vous rencontrerez dans de nombreux projets Angular pour recevoir un service :
constructor(private formationService: FormationService) {}.
L'héritage et le mot-clé override
- L'héritage fonctionne comme en JavaScript (
extends,super). TypeScript vérifie en plus que les types restent compatibles. - Avec l'option
noImplicitOverride(activée dans les projets Angular), toute méthode qui redéfinit une méthode de la classe parente doit porter le mot-cléoverride. Cela évite deux erreurs : redéfinir une méthode sans le vouloir, et croire redéfinir une méthode qui n'existe pas (faute de frappe).
class FormationEnLigne extends Formation {
constructor(titre: string, public plateforme: string) {
super(titre, 100, 300);
}
override get prixTTC(): number { // redéfinition : override obligatoire
return +(super.prixTTC * 0.8).toFixed(3); // 20 % de réduction en ligne
}
lien(): string {
return `https://${this.plateforme}/${this.titre.toLowerCase().replaceAll(' ', '-')}`;
}
}
const design = new FormationEnLigne('Infographie et design', 'moodle.formaplus.tn');
console.log(design.prixTTC); // 285.6 : prixHT est protected, la classe enfant peut l'utiliser
console.log(design.lien()); // https://moodle.formaplus.tn/infographie-et-design
console.log(Formation.total); // 2
| Oubli | Message de tsc |
|---|---|
override absent sur une redéfinition |
TS4114: This member must have an 'override' modifier because it overrides a member in the base class 'Formation'. |
override sur une méthode qui n'existe pas dans le parent |
TS4113: This member cannot have an 'override' modifier because it is not declared in the base class 'Formation'. |
-
Les classes abstraites
- Une classe
abstractsert uniquement de modèle commun à d'autres classes : on ne peut pas créer d'objet directement à partir d'elle. - Elle peut contenir des méthodes abstraites, déclarées sans code : chaque classe enfant est obligée de les écrire.
- Une classe

// src/paiement.ts
abstract class Paiement {
constructor(protected montant: number) {}
abstract frais(): number; // pas de code : à écrire dans chaque classe enfant
total(): number { // méthode commune, qui utilise frais()
return this.montant + this.frais();
}
recu(): string {
return `${this.constructor.name} : ${this.montant.toFixed(3)} + ${this.frais().toFixed(3)} = ${this.total().toFixed(3)} DT`;
}
}
class PaiementEspeces extends Paiement {
frais(): number {
return 0;
}
}
class PaiementCarte extends Paiement {
frais(): number {
return this.montant * 0.015;
}
}
class PaiementVirement extends Paiement {
frais(): number {
return 2.5;
}
}
const paiements: Paiement[] = [new PaiementEspeces(120), new PaiementCarte(120), new PaiementVirement(120)];
paiements.forEach((p) => console.log(p.recu()));
// PaiementEspeces : 120.000 + 0.000 = 120.000 DT
// PaiementCarte : 120.000 + 1.800 = 121.800 DT
// PaiementVirement : 120.000 + 2.500 = 122.500 DT
- Implémenter une méthode abstraite ne demande pas
override: il n'y a pas de code parent à remplacer.
| Code | Message de tsc |
|---|---|
new Paiement(120); |
TS2511: Cannot create an instance of an abstract class. |
class PaiementCheque extends Paiement {} |
TS2515: Non-abstract class 'PaiementCheque' does not implement inherited abstract member frais from class 'Paiement'. |
-
Les classes génériques
- Une classe générique reçoit un type en paramètre, noté entre chevrons
<T>. On écrit la classe une seule fois, et on l'utilise avec des types différents, sans perdre la vérification des types.
- Une classe générique reçoit un type en paramètre, noté entre chevrons
// src/liste.ts
class Liste<T> {
private elements: T[] = [];
ajouter(element: T): void {
this.elements.push(element);
}
get taille(): number {
return this.elements.length;
}
trouver(critere: (element: T) => boolean): T | undefined {
return this.elements.find(critere);
}
}
const noms = new Liste<string>();
noms.ajouter('Amira');
noms.ajouter('Sami');
// noms.ajouter(42); // TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
console.log(noms.taille, noms.trouver((n) => n.startsWith('S'))); // 2 Sami
interface Produit {
id: number;
nom: string;
prix: number;
}
const catalogue = new Liste<Produit>();
catalogue.ajouter({ id: 1, nom: 'Clavier', prix: 45 });
console.log(catalogue.trouver((p) => p.prix < 50)?.nom); // Clavier
- Vous utilisez déjà des génériques sans le savoir :
Array<string>(oustring[]),Promise<Produit>, et dans Angularsignal<Formation[]>([])ouObservable<Produit[]>. - On peut contraindre le type reçu :
class Repertoire<T extends { id: number }>n'accepte que des types possédant une propriétéidnumérique (sinon :TS2344: Type 'string' does not satisfy the constraint '{ id: number; }'.).
Les classes TypeScript dans Angular
- Un service et un composant récents, tels que la CLI d'Angular les génère et que vous les écrirez :
// formation.service.ts
import { Injectable } from '@angular/core';
export interface Formation {
id: number;
titre: string;
placesRestantes: number;
}
@Injectable({ providedIn: 'root' })
export class FormationService {
private readonly formations: Formation[] = [
{ id: 1, titre: 'Développement web', placesRestantes: 6 },
{ id: 2, titre: 'Réseaux informatiques', placesRestantes: 0 },
];
lister(): Formation[] {
return [...this.formations];
}
}
// catalogue.ts
import { Component, OnInit, computed, inject, signal } from '@angular/core';
import { Formation, FormationService } from './formation.service';
@Component({
selector: 'app-catalogue',
template: `
<h2>{{ titre }} ({{ nombre() }})</h2>
@for (f of formations(); track f.id) {
<p>{{ f.titre }} – {{ f.placesRestantes }} places</p>
}
`,
})
export class Catalogue implements OnInit {
private readonly service = inject(FormationService); // privé : usage interne
protected readonly titre = 'Nos formations'; // protected : utilisé par le template
protected readonly formations = signal<Formation[]>([]); // classe générique : signal<T>
protected readonly nombre = computed(() => this.formations().length);
ngOnInit(): void {
this.formations.set(this.service.lister());
}
protected reserver(id: number): void {
this.formations.update((liste) =>
liste.map((f) => (f.id === id ? { ...f, placesRestantes: f.placesRestantes - 1 } : f)),
);
}
}
- Les conventions à retenir pour Angular :
privatepour ce que seule la classe utilise (les services injectés, les données internes) ;protectedpour ce que le template affiche ou appelle : c'est la recommandation du guide de style d'Angular, qui indique clairement que le membre sert au template, sans l'ouvrir aux autres classes ;public(sans mot-clé) pour ce que d'autres classes doivent utiliser, par exemple les méthodes d'un service ;readonlypour tout ce qui n'est jamais réaffecté : services injectés, signals, constantes.
- Dans un code plus ancien, le service est reçu par une propriété de paramètre, avec la même signification :
export class CatalogueAncien implements OnInit {
formations: Formation[] = [];
constructor(private formationService: FormationService) {}
ngOnInit(): void {
this.formations = this.formationService.lister();
}
}
- À venir :
implements OnInitutilise une interface (leçon suivante), et@Component/@Injectablesont des décorateurs (leçon Présentation des décorateurs).
Activité 1 : typer une classe JavaScript
- Dans le projet
rappel-typescript, créersrc/a1-stagiaire.tset y recopier la classeStagiaireécrite en JavaScript dans l'activité 1 de la leçon Les classes et les modules ES6. - Lancer
npx tsc: relever en commentaire les erreurs affichées (propriétés non déclarées, paramètres sans type…). - Ajouter tous les types nécessaires : propriétés, paramètres, valeurs de retour (y compris
thispourajouterNote), accesseurs. Compiler jusqu'à n'avoir plus aucune erreur, puis exécuternode dist/a1-stagiaire.js: le résultat doit être identique à celui de la version JavaScript. - Ajouter à la fin du fichier les quatre lignes du tableau « Typer une classe » et vérifier que
tscaffiche bien les quatre messages. Les supprimer ensuite.
Activité 2 : une salle de formation
- Dans
src/a2-salle.ts, écrire une classeSalleavec uniquement des propriétés de paramètre dans le constructeur :code(public, en lecture seule),capacite(public) etequipee(privé,falsepar défaut). - Ajouter : un compteur statique privé lisible par l'accesseur statique
total, une liste privée de créneaux réservés, l'accesseurnombreReservations, une méthodereserver(creneau: string, effectif: number): stringet une méthodedescription(): string. - Tester :
const b12 = new Salle('B12', 24, true);
const a3 = new Salle('A3', 16);
console.log(b12.description());
console.log(a3.description());
console.log(b12.reserver('lundi 8h', 20));
console.log(b12.reserver('lundi 8h', 18));
console.log(a3.reserver('mardi 14h', 20));
console.log(b12.nombreReservations, Salle.total);
// Résultat attendu :
// Salle B12 – 24 places – équipée
// Salle A3 – 16 places
// B12 : réservée le lundi 8h
// B12 : lundi 8h déjà réservé
// A3 : refusé, 20 > 16 places
// 1 2
- Écrire ensuite
b12.code = 'B13';etb12.equipee;: noter les deux messages du compilateur. Ouvrirdist/a2-salle.js: que sont devenusreadonlyetprivate?
Activité 3 : héritage avec protected et override
- Dans
src/a3-personnes.ts, écrire une classePersonneavec les propriétés de paramètreprotected nometprotected prenom, et une méthodepresenter(): string. - Écrire les classes
Stagiaire(propriétégroupe) etFormateur(propriétéspecialite) qui redéfinissentpresenter()en réutilisantsuper.presenter(). - Compiler sans le mot-clé
override, noter l'erreurTS4114, puis l'ajouter. Écrire volontairementoverride presentr()(faute de frappe) : quel message obtenez-vous ? En quoioverridevous protège-t-il ? - Essayer d'afficher
stagiaire.nomdepuis l'extérieur : quel message ? Comment exposer le nom en lecture seulement, sans le rendrepublic? (indice : un accesseurget).
Activité 4 : une classe abstraite de notification
- Dans
src/a4-notifications.ts, écrire une classe abstraiteNotificationavec une propriété de paramètreprotected readonly destinataire: string, une méthode abstraiteenvoyer(message: string): stringet une méthode protégéehorodatage()qui renvoie'[29/10 09:30]'. - Écrire trois classes enfants :
NotificationEmail,NotificationSms(message coupé à 20 caractères suivis de « … ») etNotificationPush(message en majuscules). - Ranger une instance de chaque classe dans un tableau de type
Notification[], puis envoyer le même message à tous :
for (const n of notifications) {
console.log(n.envoyer('Votre séance de demain est reportée à 10h'));
}
// Résultat attendu :
// [29/10 09:30] E-mail à amira@exemple.tn : Votre séance de demain est reportée à 10h
// [29/10 09:30] SMS au +216 20 000 000 : Votre séance de dema…
// [29/10 09:30] Notification sur l'appareil de Sami : VOTRE SÉANCE DE DEMAIN EST REPORTÉE À 10H
- Ajouter une classe
NotificationCourriersans méthodeenvoyer()et noter le messageTS2515. Essayernew Notification('x')et noter le messageTS2511.
Activité 5 : un répertoire générique
- Dans
src/a5-repertoire.ts, écrire une classe génériqueRepertoire<T extends { id: number }>avec une liste privée et :ajouter(...nouveaux: T[]): this(paramètre rest, enchaînable) ;trouverParId(id: number): T | undefined;supprimer(id: number): boolean(renvoietruesi un élément a été supprimé) ;- un accesseur
tousde typereadonly T[].
- Déclarer deux interfaces
Stagiaire(id,nom,groupe) etLivre(id,titre,auteur), puis tester :
const stagiaires = new Repertoire<Stagiaire>().ajouter(
{ id: 1, nom: 'Amira', groupe: 'DEV1' },
{ id: 2, nom: 'Sami', groupe: 'DEV2' },
);
const livres = new Repertoire<Livre>().ajouter({ id: 10, titre: 'Apprendre Angular', auteur: 'Ali Gharbi' });
console.log(stagiaires.trouverParId(2)?.nom, stagiaires.trouverParId(5)?.nom ?? 'introuvable');
console.log(stagiaires.supprimer(1), stagiaires.supprimer(1), stagiaires.tous.length);
console.log(livres.trouverParId(10)?.titre);
// Résultat attendu :
// Sami introuvable
// true false 1
// Apprendre Angular
- Essayer
new Repertoire<string>()etstagiaires.ajouter({ id: 3, titre: 'x', auteur: 'y' }): expliquer les deux messages du compilateur en commentaire.
Activité 6 : retour sur le projet Angular FormaPlus
- Ouvrir le projet
forma-plusdu TP 1. Dans chaque composant, ajouter les modificateurs adaptés :protected readonlypour les données affichées par le template et jamais réaffectées (chiffres,formations,slogan…),privatepour ce qui ne sert qu'à la classe. - Lancer
ng build: l'application doit compiler sans erreur. - Passer ensuite
sloganenprivatedansEnteteet relancerng build. Avec les versions récentes d'Angular (le TP a été réalisé avec Angular 22), la compilation réussit : le template a accès aux membres privés. Expliquer en commentaire pourquoi le guide de style d'Angular recommande pourtantprotectedpour les membres utilisés uniquement par le template (indice : que comprend une personne qui litprivatedans la classe ?). Remettreprotected readonly. - Faire un commit dans chaque dépôt :
git commit -m "Classes TypeScript : activité N", puis un push.
Erreurs fréquentes
| Message | Cause | Solution |
|---|---|---|
TS2564: Property 'x' has no initializer … |
Propriété jamais initialisée (mode strict) | Valeur par défaut, affectation dans le constructeur, ? ou ! |
TS2339: Property 'x' does not exist on type 'Y'. |
Propriété non déclarée dans la classe, ou faute de frappe | Déclarer la propriété avec son type |
TS2341: Property 'x' is private … |
Accès à un membre private depuis l'extérieur |
Passer par une méthode ou un accesseur, ou revoir le modificateur |
TS2445: Property 'x' is protected … |
Accès à un membre protected hors de la classe et de ses enfants |
Idem |
TS2540: Cannot assign to 'x' because it is a read-only property. |
Modification d'une propriété readonly ou d'un accesseur sans set |
Créer un nouvel objet, ou retirer readonly si la valeur doit vraiment changer |
TS4114: This member must have an 'override' modifier … |
Redéfinition sans override (noImplicitOverride) |
Ajouter override devant la méthode |
TS2511: Cannot create an instance of an abstract class. |
new sur une classe abstraite |
Instancier une classe enfant |
TS2515: Non-abstract class 'X' does not implement inherited abstract member … |
Méthode abstraite non écrite dans la classe enfant | Écrire la méthode dans la classe enfant |
TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value. |
Type de retour annoncé, mais aucun return |
Ajouter le return, ou déclarer void |
-
À retenir
- En TypeScript, on type les propriétés, les paramètres et les valeurs de retour ; une classe est aussi un type.
- En mode strict, chaque propriété doit être initialisée, ou déclarée optionnelle avec
?. public(par défaut),protected,privateetreadonlysont vérifiés à la compilation, puis effacés.- Une propriété de paramètre (
constructor(private x: T)) déclare et remplit une propriété en une fois. overridesignale une redéfinition ; une classeabstractimpose ses méthodes abstraites à ses enfants ; une classe générique<T>s'adapte au type reçu.- Dans Angular :
private readonlypour les services injectés,protectedpour ce qu'affiche le template.
TypeScript ne change pas ce que fait une classe : il vérifie, avant l'exécution, que vous l'utilisez comme prévu. Chaque erreur signalée par tsc est un bug évité dans le navigateur.