Skip to content

14. Les classes

Nous introduisons ici les classes ECMAScript, telles que typées en TypeScript. Avant de les présenter, rappelons (script class-00, déjà vu au chapitre « Les fonctions » sous le nom func-04) que les fonctions peuvent déjà se comporter comme des classes — c'est ce mécanisme historique que le mot clé [class] vient simplifier.

Les scripts de ce chapitre se trouvent dans le dossier [classes] du projet.

14.1. script [class-00]

Rappel du mécanisme historique (fonctions utilisées comme des classes) :

'use strict';
// une fonction peut être utilisée comme un objet

// une coquille vide
// [any] : le type de [this] est ici volontairement souple, car on attribue
// des propriétés à la fonction elle-même APRÈS sa définition (cf plus bas) -
// un schéma que TypeScript ne peut pas déduire tout seul
function f(this: any): void {

}
// à qui on attribue des propriétés de l'extérieur
(f as any).prop1 = "val1";
(f as any).show = function (this: any) {
  console.log(this.prop1);
};
// utilisation de f
(f as any).show();

// une fonction g fonctionnant comme une classe
function g(this: any): void {
  this.prop2 = "val2";
  this.show = function (this: any) {
    console.log(this.prop2);
  };
}
// instanciation de la fonction avec [new]
new (g as any)().show();
npx tsx classes/class-00.ts

Résultat de l'exécution :

val1
val2
Remarque : ES6 a introduit le mot clé class, qui permet d'obtenir ce même résultat de façon bien plus lisible et sûre, sans passer par ce détour fonctionnel — c'est ce que montrent les scripts suivants.

14.2. script [class-01]

Ce script présente une classe [Personne], avec constructeur, getters/setters et méthode toString :

// classe
class Personne {

  private _nom!: string;
  private _prénom!: string;
  private _âge!: number;

  // constructeur
  constructor(nom: string, prénom: string, âge: number) {
    this.nom = nom;
    this.prénom = prénom;
    this.âge = âge;
  }

  // getters et setters
  get nom(): string {
    return this._nom;
  }
  set nom(value: string) {
    this._nom = value;
  }

  get prénom(): string {
    return this._prénom;
  }
  set prénom(value: string) {
    this._prénom = value;
  }

  get âge(): number {
    return this._âge;
  }
  set âge(value: number) {
    this._âge = value;
  }

  // toString en JSON
  toString(): string {
    return JSON.stringify(this);
  }
}

// appel de la classe
function main(): void {
  const personne = new Personne("Poirot", "Hercule", 66);
  console.log("personne=", personne.toString(), typeof (personne), personne instanceof (Personne));
}

// appel de main
main();
  • ligne 2 : [class] introduit la classe ;
  • ligne 9 : [constructor] son constructeur — il ne peut y en avoir qu'un seul par classe ;
  • lignes 4-6 : les champs _nom, _prénom, _âge sont déclarés avec le modificateur [private] — accessibles uniquement depuis l'intérieur de la classe, contrairement à une simple convention de nommage. Le ! (*definite assignment assertion*) indique à TypeScript que ces champs seront bien affectés avant toute lecture, même si ce n'est pas visible directement dans leur déclaration (c'est le constructeur, via les setters, qui s'en charge) ;
  • lignes 16-35 : getters et setters — syntaxe identique à celle vue pour les objets littéraux (chapitre « Les objets littéraux », script obj-03), mais intégrée à la classe ;
  • ligne 46 : personne instanceof (Personne) — on peut connaître le type exact d'une instance de classe, contrairement aux objets littéraux.
npx tsx classes/class-01.ts

Résultat de l'exécution :

personne= {"_nom":"Poirot","_prénom":"Hercule","_âge":66} object true

14.3. script [class-02] — héritage

Ce script montre l'héritage avec le mot clé [extends]. La classe Personne est d'abord isolée dans son propre fichier, Personne.ts :

// classe
class Personne {

  // propriétés privées (accédées via les getters/setters ci-dessous)
  // [!] : on affirme à TypeScript qu'elles seront affectées avant lecture
  // (ici, via le constructeur qui appelle les setters)
  private _nom!: string;
  private _prénom!: string;
  private _âge!: number;

  // constructeur
  constructor(nom: string, prénom: string, âge: number) {
    this.nom = nom;
    this.prénom = prénom;
    this.âge = âge;
  }

  // getters et setters
  get nom(): string {
    return this._nom;
  }
  set nom(value: string) {
    this._nom = value;
  }

  get prénom(): string {
    return this._prénom;
  }
  set prénom(value: string) {
    this._prénom = value;
  }

  get âge(): number {
    return this._âge;
  }
  set âge(value: number) {
    this._âge = value;
  }

  // toString en JSON
  toString(): string {
    return JSON.stringify(this);
  }
}
// export classe
export default Personne;

Le script [class-02] crée une classe [Enseignant] dérivée de Personne :

// imports
// [mise à jour 2026] avec l'ESM natif de node, l'extension .js est obligatoire
// dans les chemins relatifs (elle était optionnelle avec le chargeur [esm] utilisé en 2019)
import Personne from './Personne.js';

// classe
class Enseignant extends Personne {

  private _discipline!: string;

  // constructeur
  constructor(nom: string, prénom: string, âge: number, discipline: string) {
    super(nom, prénom, âge);
    this.discipline = discipline;
  }

  // getters et setters
  get discipline(): string {
    return this._discipline;
  }
  set discipline(value: string) {
    this._discipline = value;
  }

}

// appel de la classe
function main(): void {
  const enseignant = new Enseignant("Poirot", "Hercule", 66, "détective");
  console.log("enseignant=", enseignant.toString(), typeof (enseignant), enseignant instanceof Enseignant);
}

// appel de main
main();
  • ligne 4 : import Personne from './Personne.js' — on importe la classe depuis son module. L'extension .js (et non .ts) est obligatoire avec la résolution de modules moderne de node, même si le fichier source est un .ts (voir chapitre « Installation ») ;
  • ligne 7 : class Enseignant extends PersonneEnseignant hérite de tous les membres de Personne, et ajoute une propriété _discipline avec son getter/setter ;
  • ligne 13 : super(nom, prénom, âge) appelle le constructeur de la classe parent, qui initialise _nom, _prénom, _âge ;
  • ligne 30 : enseignant instanceof Enseignant — TypeScript/JavaScript connaît le type exact de l'instance, même après héritage.
npx tsx classes/class-02.ts

Résultat de l'exécution :

enseignant= {"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"} object true

14.4. script [class-03] — redéfinition de méthode

Ce script montre qu'une classe fille peut redéfinir une méthode de sa classe parent — ici, toString :

// imports
// [mise à jour 2026] avec l'ESM natif de node, l'extension .js est obligatoire
// dans les chemins relatifs (elle était optionnelle avec le chargeur [esm] utilisé en 2019)
import Personne from './Personne.js';

// classe
class Enseignant extends Personne {

  private _discipline!: string;

  // constructeur
  constructor(nom: string, prénom: string, âge: number, discipline: string) {
    super(nom, prénom, âge);
    this.discipline = discipline;
  }

  // getters et setters
  get discipline(): string {
    return this._discipline;
  }
  set discipline(value: string) {
    this._discipline = value;
  }

  // redéfinition de toString
  toString(): string {
    return "[Enseignant]" + JSON.stringify(this);
  }
}

// appel de la classe
function main(): void {
  const enseignant = new Enseignant("Poirot", "Hercule", 66, "détective");
  console.log("enseignant=", enseignant.toString(), typeof (enseignant), enseignant instanceof Enseignant);
}

// appel de main
main();
npx tsx classes/class-03.ts

Résultat de l'exécution :

enseignant= [Enseignant]{"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"} object true

14.5. script [class-04] — polymorphisme

Ce script montre le polymorphisme à l'œuvre : là où une fonction attend un paramètre de type Personne, on peut passer un type dérivé tel que Enseignant, puisque celui-ci possède tous les attributs de Personne. On isole d'abord Enseignant dans son propre module, Enseignant.ts :

// imports
// [mise à jour 2026] avec l'ESM natif de node, l'extension .js est obligatoire
// dans les chemins relatifs (elle était optionnelle avec le chargeur [esm] utilisé en 2019)
import Personne from './Personne.js';

// classe
class Enseignant extends Personne {

  // propriété privée (accédée via le getter/setter ci-dessous)
  private _discipline!: string;

  // constructeur
  constructor(nom: string, prénom: string, âge: number, discipline: string) {
    super(nom, prénom, âge);
    this.discipline = discipline;
  }

  // getters et setters
  get discipline(): string {
    return this._discipline;
  }
  set discipline(value: string) {
    this._discipline = value;
  }

}

// export classe
export default Enseignant;

Le script [class-04] utilise ce type de façon polymorphe :

// imports
// [mise à jour 2026] avec l'ESM natif de node, l'extension .js est obligatoire
// dans les chemins relatifs (elle était optionnelle avec le chargeur [esm] utilisé en 2019)
import Enseignant from './Enseignant.js';
import Personne from './Personne.js';

// fonction acceptant une personne comme paramètre
function show(personne: Personne): void {
  // dans tous les cas
  console.log("paramètre=", personne.toString(), typeof (personne));
  // instance de Personne
  if (personne instanceof Personne) {
    console.log("personne=", personne.toString());
  }
  // instance de Enseignant
  if (personne instanceof Enseignant) {
    console.log("enseignant=", personne.toString());
  }
}

// appel de show avec un enseignant
show(new Enseignant("Poirot", "Hercule", 66, "détective"));
show(new Personne("Marple", "Miss", 70));
  • ligne 8 : function show(personne: Personne): void — la fonction attend un paramètre typé Personne, mais accepte tout type dérivé (Enseignant) — c'est le principe de substitution, fondamental en programmation orientée objet ;
  • lignes 11 -18 : instanceof permet d'adapter le comportement au type réel de l'objet reçu, au-delà de son type déclaré.
npx tsx classes/class-04.ts

Résultat de l'exécution :

1
2
3
4
5
paramètre= {"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"} object
personne= {"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"}
enseignant= {"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"}
paramètre= {"_nom":"Marple","_prénom":"Miss","_âge":70} object
personne= {"_nom":"Marple","_prénom":"Miss","_âge":70}

14.6. script [class-05] — champs privés natifs

[NOUVEAU depuis 2019] Les scripts précédents utilisent le modificateur TypeScript private (_nom, _prénom...), une protection qui n'existe qu'à la compilation — à l'exécution, en JavaScript pur, ces champs restent de simples propriétés normales. Depuis ECMAScript 2022, il existe de vrais champs privés, préfixés par #, invisibles et inaccessibles même à l'exécution :

'use strict';
// ========================================================================
// [NOUVEAU depuis 2019] champs privés et blocs statiques  (ECMAScript 2022)
// ========================================================================

// dans class-01.js, l'encapsulation était simulée par convention : une propriété
// nommée [_nom] (avec un underscore) que l'on accédait via un getter/setter [nom].
// MAIS rien n'empêchait vraiment d'écrire personne._nom = "xyz" depuis l'extérieur !
// depuis ECMAScript 2022, on peut déclarer de VRAIS champs privés avec un [#]
// devant leur nom : ils ne sont accessibles que depuis l'intérieur de la classe.

class CompteBancaire {

  // champ privé : n'existe et n'est visible QUE dans les méthodes de cette classe
  #solde: number;

  // champ privé avec valeur par défaut
  #historique: string[] = [];

  // champ public normal (pas de #) : accessible depuis l'extérieur, comme avant
  titulaire: string;

  constructor(titulaire: string, soldeInitial: number = 0) {
    this.titulaire = titulaire;
    this.#solde = soldeInitial;
  }

  // méthode publique qui donne un accès contrôlé au champ privé
  get solde(): number {
    return this.#solde;
  }

  déposer(montant: number): void {
    this.#solde += montant;
    // #ajouterHistorique est une méthode privée, appelable seulement d'ici
    this.#ajouterHistorique(`dépôt de ${montant}`);
  }

  retirer(montant: number): void {
    if (montant > this.#solde) {
      throw new Error("solde insuffisant");
    }
    this.#solde -= montant;
    this.#ajouterHistorique(`retrait de ${montant}`);
  }

  // méthode privée : le [#] fonctionne aussi sur les méthodes
  #ajouterHistorique(opération: string): void {
    this.#historique.push(opération);
  }

  afficherHistorique(): void {
    console.log(`historique de ${this.titulaire} :`, this.#historique);
  }
}

// utilisation normale
const compte = new CompteBancaire("Dupont", 100);
compte.déposer(50);
compte.retirer(30);
console.log("solde =", compte.solde); // via le getter public
compte.afficherHistorique();

// on ne peut pas accéder directement au champ privé depuis l'extérieur
console.log("compte.#solde direct : impossible, provoque une erreur de syntaxe si on essaie");
// console.log(compte.#solde); // décommenter cette ligne provoque une erreur à la compilation TypeScript

// on ne peut pas non plus le lire avec la notation [ ] comme pour une propriété normale
console.log("compte['#solde'] =", (compte as any)["#solde"]); // -> undefined, ce n'est pas la même chose

// ------------------------------------------------------------------------
// les champs et méthodes STATIQUES privés existent aussi
// ------------------------------------------------------------------------
class GénérateurIdentifiant {
  // champ statique privé : partagé par toutes les instances, invisible de l'extérieur
  static #dernierId: number = 0;

  // bloc d'initialisation statique : exécuté une seule fois, au chargement de la classe
  // utile pour une initialisation complexe qu'un simple "= valeur" ne permet pas
  static {
    console.log("[bloc statique] initialisation de GénérateurIdentifiant");
    GénérateurIdentifiant.#dernierId = 1000; // valeur de départ personnalisée
  }

  // méthode statique publique donnant un accès contrôlé au champ statique privé
  static prochainId(): number {
    GénérateurIdentifiant.#dernierId++;
    return GénérateurIdentifiant.#dernierId;
  }
}

console.log("prochainId() =", GénérateurIdentifiant.prochainId());
console.log("prochainId() =", GénérateurIdentifiant.prochainId());
console.log("prochainId() =", GénérateurIdentifiant.prochainId());
  • [#solde] (ligne 15), [#historique] (ligne 18) : vrais champs privés — contrairement à _nom au script class-01, il est impossible d'y accéder depuis l'extérieur de la classe, même via compte['#solde'] (ligne 69), qui rend undefined plutôt que la valeur réelle ;
  • [#ajouterHistorique] (ligne 48) : une méthode privée, appelable uniquement depuis l'intérieur de la classe ;
  • [static { ... }] (lignes 80-83) : un bloc d'initialisation statique (ES2022), exécuté une seule fois au chargement de la classe — utile pour une initialisation plus complexe qu'une simple affectation ;
  • [static #dernierId] (ligne 76) : un champ statique privé, partagé par toutes les instances mais invisible de l'extérieur.
npx tsx classes/class-05.ts

Résultat de l'exécution :

1
2
3
4
5
6
7
8
solde = 120
historique de Dupont : [ 'dépôt de 50', 'retrait de 30' ]
compte.#solde direct : impossible, provoque une erreur de syntaxe si on essaie
compte['#solde'] = undefined
[bloc statique] initialisation de GénérateurIdentifiant
prochainId() = 1001
prochainId() = 1002
prochainId() = 1003