Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Les interfaces de classe en TypeScript

Cette leçon explique comment décrire la forme d'un objet avec une interface TypeScript : propriétés obligatoires, facultatives et en lecture seule, méthodes, et les erreurs du compilateur quand un obj...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Cette leçon explique comment décrire la forme d'un objet avec une interface TypeScript : propriétés obligatoires, facultatives et en lecture seule, méthodes, et les erreurs du compilateur quand un objet ne respecte pas le contrat. Vous étendrez des interfaces avec extends, imposerez un contrat à des classes avec implements et apprendrez à programmer vers une interface pour rendre votre code indépendant d'une implémentation. Les interfaces génériques et les types utilitaires Partial, Omit et Pick sont présentés, ainsi qu'une comparaison avec type et les classes abstraites. La leçon se termine par les interfaces d'Angular : modèles de données, OnInit, OnDestroy et PipeTransform. Des schémas et six activités pratiques accompagnent chaque notion.
  1. Objectifs

    • Décrire la forme d'un objet avec une interface: propriétés obligatoires, facultatives et en lecture seule, méthodes
    • Comprendre les erreurs du compilateur quand un objet ne respecte pas une interface
    • Étendre une ou plusieurs interfaces avec extends
    • Obliger une classe à respecter un contrat avec implements
    • « Programmer vers une interface » pour rendre le code indépendant d'une implémentation
    • Écrire une interface générique et utiliser les types utilitaires Partial, Omit, Pick
    • Choisir entre interface, type et classe abstraite, et reconnaître les interfaces d'Angular (OnInit, OnDestroy, PipeTransform)
  2. Présentation

    • Une interface est un contrat: elle décrit la forme qu'un objet doit avoir, c'est-à-dire ses propriétés, leurs types et ses méthodes, sans contenir aucun code.
    • Elle a deux grands usages :
      • décrire des données: un stagiaire, une formation, la réponse d'une API… On parle de modèle de données;
      • imposer un contrat à des classes avec implements: toutes les classes qui « signent » le contrat doivent fournir les mêmes méthodes.
    • Une interface n'existe que pour le compilateur: elle est entièrement effacée du JavaScript produit. Elle ne coûte donc rien à l'exécution, mais elle apporte la vérification des types et l'autocomplétion dans VS Code.
    • Dans Angular, on rencontre des interfaces partout : modèles dans un dossier models/, réponses typées du client HTTP, et interfaces du framework comme OnInit ou PipeTransform.

Une interface TypeScript est un contrat de forme

  1. Préparer l'environnement de test

    • On continue dans le projet rappel-typescript de la leçon Les classes en TypeScript : les fichiers .ts vont dans src/, on compile avec npx tsc et on exécute avec node dist/nom-du-fichier.js.
  2. Déclarer et utiliser une interface

    • Une interface se déclare avec le mot-clé interface suivi d'un nom en PascalCase. Chaque membre est :
      • obligatoire par défaut ;
      • facultatif avec ?;
      • en lecture seule avec readonly;
      • une méthode, décrite par sa signature (paramètres et type de retour).
    • L'interface sert ensuite de type pour une variable, un paramètre ou une valeur de retour.
// src/modele.ts
interface Formation {
  readonly id: number;
  titre: string;
  dureeMois: number;
  niveau?: string;                          // facultative
  modules: string[];
  calculerPrixTTC(tva: number): number;     // méthode
}

const web: Formation = {
  id: 1,
  titre: 'Développement web',
  dureeMois: 24,
  modules: ['HTML', 'CSS', 'Angular'],
  calculerPrixTTC(tva) {                    // tva est déduit : number
    return 1200 * (1 + tva);
  },
};

function resume(f: Formation): string {
  const niveau = f.niveau ?? 'tous niveaux';
  return `${f.titre} – ${f.dureeMois} mois – ${niveau} – ${f.modules.length} modules`;
}

console.log(resume(web));                          // Développement web – 24 mois – tous niveaux – 3 modules
console.log(web.calculerPrixTTC(0.19).toFixed(3)); // 1428.000
  • Le compilateur vérifie que chaque objet respecte exactement le contrat :
Code Message de tsc
propriété obligatoire absente : { id: 1, titre: 'Web' } TS2741: Property 'dureeMois' is missing in type '{ id: number; titre: string; }' but required in type 'Formation'.
propriété inconnue : { …, prix: 1200 } TS2353: Object literal may only specify known properties, and 'prix' does not exist in type 'Formation'.
mauvais type : dureeMois: '12' TS2322: Type 'string' is not assignable to type 'number'.
modification d'un readonly: f4.id = 5; TS2540: Cannot assign to 'id' because it is a read-only property.
propriété facultative utilisée sans vérification : f4.niveau.toUpperCase() TS18048: 'f4.niveau' is possibly 'undefined'.
  • Le dernier cas est précieux : une propriété facultative peut être absente. TypeScript oblige à le prévoir, avec ?., ?? ou un if: f4.niveau?.toUpperCase() ?? 'tous niveaux'.
  • Interfaces de fonction et signatures d'index

    • Une interface peut aussi décrire une fonction (ses paramètres et son retour), ou un objet dont on ne connaît pas les noms de propriétés à l'avance (signature d'index) :
    interface Filtre {
      (formation: Formation): boolean;
    }
    const longue: Filtre = (f) => f.dureeMois >= 24;
    console.log(longue(web));   // true
    
    interface NotesParModule {
      [module: string]: number;   // n'importe quel nom de module, valeur numérique
    }
    const notes: NotesParModule = { Angular: 15, Flutter: 13.5 };
    notes['Bootstrap'] = 17;
    // notes['Git'] = 'bien';     // TS2322: Type 'string' is not assignable to type 'number'.
    console.log(Object.keys(notes).length, notes['Angular']);   // 3 15
    1. Étendre des interfaces avec extends

      • Une interface peut reprendre les membres d'une ou de plusieurs autres interfaces avec extends, puis en ajouter. On évite ainsi de répéter les propriétés communes.

    Héritage entre interfaces TypeScript avec extends

    // src/heritage.ts
    interface Identifiable {
      readonly id: number;
    }
    
    interface Horodate {
      creeLe: Date;
      modifieLe?: Date;
    }
    
    interface Personne extends Identifiable {
      nom: string;
      prenom: string;
      email: string;
    }
    
    interface Stagiaire extends Personne, Horodate {   // héritage multiple
      groupe: string;
      moyenne: number;
    }
    
    interface Formateur extends Personne {
      specialites: string[];
    }
    
    const amira: Stagiaire = {
      id: 1,
      nom: 'Ben Ali',
      prenom: 'Amira',
      email: 'amira@exemple.tn',
      groupe: 'DEV1',
      moyenne: 15.67,
      creeLe: new Date(2026, 8, 15),
    };
    
    const ali: Formateur = {
      id: 100,
      nom: 'Gharbi',
      prenom: 'Ali',
      email: 'ali@formaplus.tn',
      specialites: ['Angular', 'Flutter'],
    };
    
    // Une fonction qui attend une Personne accepte un Stagiaire et un Formateur
    const contacter = (p: Personne): string => `${p.prenom} ${p.nom} <${p.email}>`;
    console.log(contacter(amira));   // Amira Ben Ali <amira@exemple.tn>
    console.log(contacter(ali));     // Ali Gharbi <ali@formaplus.tn>
    1. Imposer un contrat à une classe : implements

      • Avec implements, une classe déclare qu'elle respecte une interface. Le compilateur vérifie alors qu'elle fournit tous les membres décrits, avec les bons types.
      • Une classe peut implémenter plusieurs interfaces (alors qu'elle ne peut hériter que d'une seule classe).
      • implements n'ajoute aucun code: c'est une vérification. C'est à la classe d'écrire les méthodes.
    // src/implements.ts
    interface Affichable {
      afficher(): string;
    }
    
    interface Exportable {
      readonly format: string;
      exporter(): string;
    }
    
    class Bulletin implements Affichable, Exportable {
      readonly format = 'CSV';
    
      constructor(
        private nom: string,
        private notes: number[],
      ) {}
    
      private get moyenne(): number {
        return this.notes.reduce((s, n) => s + n, 0) / this.notes.length;
      }
    
      afficher(): string {
        return `Bulletin de ${this.nom} : ${this.moyenne.toFixed(2)} / 20`;
      }
    
      exporter(): string {
        return [this.nom, ...this.notes, this.moyenne.toFixed(2)].join(';');
      }
    }
    
    class Attestation implements Affichable {
      constructor(private nom: string, private formation: string) {}
    
      afficher(): string {
        return `Attestation : ${this.nom} a suivi la formation ${this.formation}`;
      }
    }
    
    // Des objets de classes différentes, rangés grâce à leur interface commune
    const documents: Affichable[] = [new Bulletin('Amira', [14, 16, 17]), new Attestation('Sami', 'Développement web')];
    documents.forEach((d) => console.log(d.afficher()));
    // Bulletin de Amira : 15.67 / 20
    // Attestation : Sami a suivi la formation Développement web
    
    const aExporter: Exportable[] = [new Bulletin('Karim', [18, 16, 19])];
    aExporter.forEach((e) => console.log(`${e.format} → ${e.exporter()}`));
    // CSV → Karim;18;16;19;17.67
    Erreur Message de tsc
    méthode de l'interface absente de la classe TS2420: Class 'Produit' incorrectly implements interface 'Affichable'. Property 'afficher' is missing in type 'Produit' but required in type 'Affichable'.
    méthode de l'interface déclarée private dans la classe TS2420: … Property 'afficher' is private in type 'Produit2' but not in type 'Affichable'.
    • À retenir : une interface décrit la partie publique d'une classe. Les membres private ou protected de la classe ne peuvent pas répondre à une interface.
    • Typage structurel : TypeScript vérifie la forme, pas le nom. Un objet qui possède une méthode afficher(): string est un Affichable, même s'il n'a jamais écrit implements:
    const carte = { afficher: () => 'Carte du stagiaire Yasmine' };
    const encore: Affichable = carte;   // accepté : la forme correspond
    console.log(encore.afficher());     // Carte du stagiaire Yasmine
    1. Programmer vers une interface

      • Quand une classe dépend d'une interface plutôt que d'une classe précise, on peut lui fournir n'importe quelle implémentation sans modifier son code. C'est un principe fondamental de l'architecture logicielle, et la base de l'injection de dépendances d'Angular.

    Programmer vers une interface avec implements

    // src/stockage.ts
    interface Stockage {
      readonly nom: string;
      enregistrer(cle: string, valeur: string): void;
      lire(cle: string): string | undefined;
    }
    
    class StockageMemoire implements Stockage {
      readonly nom = 'mémoire';
      private donnees = new Map<string, string>();
    
      enregistrer(cle: string, valeur: string): void {
        this.donnees.set(cle, valeur);
      }
    
      lire(cle: string): string | undefined {
        return this.donnees.get(cle);
      }
    }
    
    class StockageJournal implements Stockage {
      readonly nom = 'journal';
      private journal: string[] = [];
      private donnees: Record<string, string> = {};
    
      enregistrer(cle: string, valeur: string): void {
        this.journal.push(`écriture ${cle}`);
        this.donnees[cle] = valeur;
      }
    
      lire(cle: string): string | undefined {
        this.journal.push(`lecture ${cle}`);
        return this.donnees[cle];
      }
    
      get historique(): string {
        return this.journal.join(' | ');
      }
    }
    
    // Le service ne connaît que l'interface Stockage
    class PreferencesService {
      constructor(private stockage: Stockage) {}
    
      changerTheme(theme: 'clair' | 'sombre'): void {
        this.stockage.enregistrer('theme', theme);
      }
    
      theme(): string {
        return this.stockage.lire('theme') ?? 'clair';
      }
    }
    
    const journal = new StockageJournal();
    for (const stockage of [new StockageMemoire(), journal]) {
      const service = new PreferencesService(stockage);
      console.log(`${stockage.nom} : avant = ${service.theme()}`);
      service.changerTheme('sombre');
      console.log(`${stockage.nom} : après = ${service.theme()}`);
    }
    console.log(journal.historique);
    
    // mémoire : avant = clair
    // mémoire : après = sombre
    // journal : avant = clair
    // journal : après = sombre
    // lecture theme | écriture theme | lecture theme
    • Le même PreferencesService fonctionne avec les deux stockages. Demain, on pourra écrire un StockageNavigateur (avec localStorage) ou un StockageApi (avec un serveur) sans changer une ligne du service.
  • Interfaces génériques et types utilitaires

    • Comme une classe, une interface peut recevoir un type en paramètre. C'est idéal pour décrire une enveloppe qui contient des données de types différents, comme la réponse d'une API :
    // src/generique.ts
    interface ReponseApi<T> {
      succes: boolean;
      donnees: T;
      message?: string;
    }
    
    interface Formation {
      id: number;
      titre: string;
      placesRestantes: number;
    }
    
    const reponseListe: ReponseApi<Formation[]> = {
      succes: true,
      donnees: [
        { id: 1, titre: 'Développement web', placesRestantes: 6 },
        { id: 2, titre: 'Réseaux informatiques', placesRestantes: 0 },
      ],
    };
    const reponseNombre: ReponseApi<number> = { succes: true, donnees: 2 };
    
    function afficherReponse<T>(r: ReponseApi<T>, formater: (d: T) => string): string {
      return r.succes ? formater(r.donnees) : `Erreur : ${r.message ?? 'inconnue'}`;
    }
    
    console.log(afficherReponse(reponseListe, (liste) => liste.map((f) => f.titre).join(', ')));
    // Développement web, Réseaux informatiques
    console.log(afficherReponse(reponseNombre, (n) => `${n} formations`));
    // 2 formations
    • TypeScript fournit des types utilitaires qui fabriquent un nouveau type à partir d'une interface existante, sans la réécrire :
    Type utilitaire Résultat Usage typique
    Partial<Formation> toutes les propriétés deviennent facultatives les champs à modifier lors d'une mise à jour
    Omit<Formation, 'id'> toutes les propriétés sauf id les données d'une création (l'id est donné par le serveur)
    Pick<Formation, 'id' | 'titre'> seulement id et titre un aperçu, une liste déroulante
    Readonly<Formation> toutes les propriétés en lecture seule des données qu'on ne doit pas modifier
    Record<string, number> objet dont les clés sont des chaînes et les valeurs des nombres un dictionnaire (comme une signature d'index)
    type NouvelleFormation = Omit<Formation, 'id'>;
    const aCreer: NouvelleFormation = { titre: 'Cybersécurité', placesRestantes: 15 };
    
    const mettreAJour = (f: Formation, changements: Partial<Formation>): Formation => ({ ...f, ...changements });
    console.log(mettreAJour(reponseListe.donnees[0], { placesRestantes: 5 }));
    // { id: 1, titre: 'Développement web', placesRestantes: 5 }
    1. Interface, type ou classe abstraite ?

      • Le mot-clé type permet aussi de décrire un objet (type Point = { x: number; y: number };). Il est surtout indispensable pour les unions: type Theme = 'clair' | 'sombre';.
      • Une classe abstraite (voir Les classes en TypeScript) décrit elle aussi un contrat, mais elle peut contenir du code commun et elle existe à l'exécution.

    Comparaison entre interface, type et classe abstraite

    • Une interface n'existe pas à l'exécution : on ne peut pas écrire if (f instanceof Formation) quand Formation est une interface (TS2693: 'Formation' only refers to a type, but is being used as a value here.). Pour tester un objet, on vérifie ses propriétés ('dureeMois' in f).
  • Les interfaces dans Angular

    • Les modèles de données: chaque type de donnée de l'application est décrit par une interface exportée, souvent rangée dans un dossier models/ (comme Formation dans le TP 1).
    • Les interfaces du cycle de vie: un composant qui doit agir à sa création ou à sa destruction implémente OnInit et OnDestroy, fournies par @angular/core:
    import { Component, OnDestroy, OnInit, signal } from '@angular/core';
    
    @Component({
      selector: 'app-horloge',
      template: `<p>Il est {{ heure() }}</p>`,
    })
    export class Horloge implements OnInit, OnDestroy {
      protected readonly heure = signal('');
      private minuterie?: ReturnType<typeof setInterval>;
    
      ngOnInit(): void {                 // appelée par Angular à la création du composant
        this.minuterie = setInterval(() => {
          this.heure.set(new Date().toLocaleTimeString('fr-FR'));
        }, 1000);
      }
    
      ngOnDestroy(): void {              // appelée par Angular quand le composant disparaît
        clearInterval(this.minuterie);
      }
    }
    • Pourquoi écrire implements OnInit? Angular appellerait ngOnInit() même sans. Mais implements protège des fautes de frappe : une méthode nommée ngOninit() ne serait jamais appelée, sans aucun message. Avec l'interface, le compilateur signale : TS2420: Class 'Mauvais' incorrectly implements interface 'OnInit'. Property 'ngOnInit' is missing in type 'Mauvais' but required in type 'OnInit'.
    • Les pipes implémentent l'interface PipeTransform, qui impose la méthode transform():
    import { Pipe, PipeTransform } from '@angular/core';
    
    @Pipe({ name: 'initiales' })
    export class InitialesPipe implements PipeTransform {
      transform(nomComplet: string): string {
        return nomComplet
          .split(' ')
          .map((mot) => mot[0]?.toUpperCase() ?? '')
          .join('.');
      }
    }
    // {{ 'amira ben ali' | initiales }}  →  A.B.A
    • Le client HTTP utilise une interface comme paramètre de type, pour annoncer la forme des données reçues :
    lister(): Observable<Formation[]> {
      return this.http.get<Formation[]>('/api/formations');
    }
    1. Activité 1 : un modèle de données

      • Dans le projet rappel-typescript, créer src/b1-modele.ts et écrire une interface Stagiaire avec : id (nombre, lecture seule), nom, prenom, email (chaînes), telephone (chaîne, facultative), groupe (chaîne), notes (tableau de nombres) et une méthode moyenne(): number.
      • Créer deux objets conformes, dont un sans téléphone, et une fonction fiche(s: Stagiaire): string qui affiche « Prénom NOM (groupe) – moyenne – téléphone ou « non renseigné » ».
      • Écrire ensuite, une par une, cinq lignes fautives qui provoquent les erreurs TS2741, TS2353, TS2322, TS2540 et TS18048. Noter chaque message en commentaire, puis mettre la ligne en commentaire.
    2. Activité 2 : une hiérarchie d'interfaces

      • Dans src/b2-personnes.ts, recopier les interfaces Identifiable, Horodate, Personne, Stagiaire et Formateur de la leçon, puis ajouter une interface Administratif qui étend Personne et Horodate avec une propriété service.
      • Créer un tableau de type Personne[] contenant un stagiaire, un formateur et un administratif, et afficher leurs contacts avec la fonction contacter.
      • Expliquer en commentaire pourquoi on ne peut pas lire p.groupe dans la boucle, alors que le premier objet en possède un. Comment tester si une personne est un stagiaire ? (indice : 'groupe' in p)
    3. Activité 3 : une facture avec implements

      • Dans src/b3-facture.ts, écrire une interface Facturable avec une propriété readonly libelle: string et une méthode calculerTotal(): number.
      • Écrire deux classes qui l'implémentent : Commande (prix unitaire × quantité) et Abonnement (mensualité × nombre de mois, avec une remise facultative en pourcentage).
      • Ranger dans un tableau Facturable[]: une commande de 2 claviers à 45 DT, un abonnement « Formation en ligne » de 60 DT pendant 6 mois avec 10 % de remise, et une commande d'un casque à 85 DT. Afficher chaque ligne puis le total (utiliser padEnd(20) pour aligner) :
    Clavier              90.000 DT
    Formation en ligne   324.000 DT
    Casque               85.000 DT
    TOTAL                499.000 DT
    • Supprimer la méthode calculerTotal() d'une classe, puis la déclarer private: noter les deux messages TS2420.
  • Activité 4 : programmer vers une interface

    • Dans src/b4-notifications.ts, écrire une interface Notificateur (propriété readonly canal: string, méthode envoyer(destinataire: string, message: string): string) et deux classes qui l'implémentent : NotificateurEmail et NotificateurSms (message coupé à 25 caractères suivis de « … »).
    • Écrire une classe InscriptionService qui reçoit un Notificateur dans son constructeur (propriété de paramètre privée) et possède une méthode inscrire(nom, contact, formation): string qui envoie une confirmation.
    • Tester :
    const parEmail = new InscriptionService(new NotificateurEmail());
    const parSms = new InscriptionService(new NotificateurSms());
    console.log(parEmail.inscrire('Amira', 'amira@exemple.tn', 'Angular'));
    console.log(parSms.inscrire('Sami', '+216 20 000 000', 'Flutter'));
    
    // Résultat attendu :
    // [e-mail] À : amira@exemple.tn – Bonjour Amira, votre inscription en Angular est confirmée.
    // [SMS] +216 20 000 000 : Bonjour Sami, votre inscr…
    • Ajouter une troisième classe NotificateurConsole qui affiche le message avec console.log et renvoie « envoyé ». Avez-vous dû modifier InscriptionService? Expliquer en commentaire l'intérêt de cette organisation.
  • Activité 5 : réponse d'API et types utilitaires

    • Dans src/b5-api.ts, réutiliser l'interface ReponseApi<T> de la leçon avec l'interface Stagiaire de l'activité 1 (sans la méthode).
    • Écrire une fonction creerStagiaire(donnees: Omit<Stagiaire, 'id'>): ReponseApi<Stagiaire> qui attribue un id automatique (compteur), et une fonction modifierStagiaire(s: Stagiaire, changements: Partial<Stagiaire>): ReponseApi<Stagiaire> qui renvoie un échec avec un message si on essaie de changer l'id.
    • Créer deux stagiaires, modifier le groupe de l'un, tenter de modifier l'id de l'autre, et afficher chaque réponse avec afficherReponse.
  • Activité 6 : retour sur le projet Angular FormaPlus

    • Dans le projet forma-plus du TP 1, créer une interface ChiffreCle (valeur, libelle, icone) dans src/app/models/chiffre-cle.ts avec ng g i models/chiffre-cle, puis typer le tableau du composant ChiffresCles: protected readonly chiffres: ChiffreCle[] = [...].
    • Générer un composant layout/horloge qui implémente OnInit et OnDestroy (code de la leçon) et l'afficher dans le pied de page.
    • Renommer volontairement ngOnInit en ngOninit: quelle erreur affiche ng serve? Retirer ensuite implements OnInit: l'erreur disparaît, mais l'horloge fonctionne-t-elle ? Conclure en commentaire, puis corriger.
    • Faire un commit dans chaque dépôt : git commit -m "Interfaces TypeScript : activité N", puis un push.
  • Erreurs fréquentes

  • Message Cause Solution
    TS2741: Property 'x' is missing in type … but required in type 'Y'. Propriété obligatoire absente Ajouter la propriété, ou la rendre facultative avec ? si c'est justifié
    TS2353: Object literal may only specify known properties … Propriété inconnue de l'interface, souvent une faute de frappe Corriger le nom, ou ajouter la propriété à l'interface
    TS18048: 'x' is possibly 'undefined'. Propriété facultative utilisée sans vérification ?., ?? ou un if
    TS2420: Class 'X' incorrectly implements interface 'Y'. Membre absent, mal typé ou private dans la classe Écrire le membre, public, avec le type prévu par l'interface
    TS2693: 'X' only refers to a type, but is being used as a value here. Interface utilisée comme une valeur (instanceof, new) Tester les propriétés avec in, ou utiliser une classe
    TS2540: Cannot assign to 'x' because it is a read-only property. Modification d'une propriété readonly Créer un nouvel objet avec spread
    1. À retenir

      • Une interface décrit la forme d'un objet : propriétés obligatoires, facultatives (?), en lecture seule (readonly), et méthodes.
      • Elle est effacée à la compilation : elle sert uniquement à la vérification et à l'autocomplétion.
      • extends permet à une interface d'en reprendre une ou plusieurs autres.
      • implements oblige une classe à respecter une ou plusieurs interfaces ; une interface ne décrit que la partie publique.
      • Programmer vers une interface rend le code indépendant d'une implémentation précise.
      • ReponseApi<T>, Partial, Omit et Pick évitent de réécrire des interfaces proches.
      • Dans Angular : modèles de données, OnInit, OnDestroy, PipeTransform et réponses HTTP typées.
    Une interface ne fait rien, elle promet : chaque objet et chaque classe qui l'utilisent doivent tenir cette promesse, et le compilateur y veille pour vous.