À 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.
-
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,
typeet classe abstraite, et reconnaître les interfaces d'Angular (OnInit,OnDestroy,PipeTransform)
-
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 commeOnInitouPipeTransform.

-
Préparer l'environnement de test
- On continue dans le projet
rappel-typescriptde la leçon Les classes en TypeScript : les fichiers.tsvont danssrc/, on compile avecnpx tscet on exécute avecnode dist/nom-du-fichier.js.
- On continue dans le projet
-
Déclarer et utiliser une interface
- Une interface se déclare avec le mot-clé
interfacesuivi 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.
- Une interface se déclare avec le mot-clé
// 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 unif: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
-
É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.
- Une interface peut reprendre les membres d'une ou de plusieurs autres interfaces avec

// 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>
-
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).
implementsn'ajoute aucun code: c'est une vérification. C'est à la classe d'écrire les méthodes.
- Avec
// 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
privateouprotectedde 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(): stringest unAffichable, même s'il n'a jamais écritimplements:
const carte = { afficher: () => 'Carte du stagiaire Yasmine' };
const encore: Affichable = carte; // accepté : la forme correspond
console.log(encore.afficher()); // Carte du stagiaire Yasmine
-
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.

// 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
PreferencesServicefonctionne avec les deux stockages. Demain, on pourra écrire unStockageNavigateur(aveclocalStorage) ou unStockageApi(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 }
-
Interface, type ou classe abstraite ?
- Le mot-clé
typepermet 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.
- Le mot-clé

- Une interface n'existe pas à l'exécution : on ne peut pas écrire
if (f instanceof Formation)quandFormationest 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/(commeFormationdans le TP 1). - Les interfaces du cycle de vie: un composant qui doit agir à sa création ou à sa destruction implémente
OnInitetOnDestroy, 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 appelleraitngOnInit()même sans. Maisimplementsprotège des fautes de frappe : une méthode nomméengOninit()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éthodetransform():
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');
}
-
Activité 1 : un modèle de données
- Dans le projet
rappel-typescript, créersrc/b1-modele.tset écrire une interfaceStagiaireavec :id(nombre, lecture seule),nom,prenom,email(chaînes),telephone(chaîne, facultative),groupe(chaîne),notes(tableau de nombres) et une méthodemoyenne(): number. - Créer deux objets conformes, dont un sans téléphone, et une fonction
fiche(s: Stagiaire): stringqui 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,TS2540etTS18048. Noter chaque message en commentaire, puis mettre la ligne en commentaire.
- Dans le projet
-
Activité 2 : une hiérarchie d'interfaces
- Dans
src/b2-personnes.ts, recopier les interfacesIdentifiable,Horodate,Personne,StagiaireetFormateurde la leçon, puis ajouter une interfaceAdministratifqui étendPersonneetHorodateavec 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 fonctioncontacter. - Expliquer en commentaire pourquoi on ne peut pas lire
p.groupedans la boucle, alors que le premier objet en possède un. Comment tester si une personne est un stagiaire ? (indice :'groupe' in p)
- Dans
-
Activité 3 : une facture avec implements
- Dans
src/b3-facture.ts, écrire une interfaceFacturableavec une propriétéreadonly libelle: stringet une méthodecalculerTotal(): number. - Écrire deux classes qui l'implémentent :
Commande(prix unitaire × quantité) etAbonnement(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 (utiliserpadEnd(20)pour aligner) :
- Dans
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éclarerprivate: noter les deux messagesTS2420.
Activité 4 : programmer vers une interface
- Dans
src/b4-notifications.ts, écrire une interfaceNotificateur(propriétéreadonly canal: string, méthodeenvoyer(destinataire: string, message: string): string) et deux classes qui l'implémentent :NotificateurEmailetNotificateurSms(message coupé à 25 caractères suivis de « … »). - Écrire une classe
InscriptionServicequi reçoit unNotificateurdans son constructeur (propriété de paramètre privée) et possède une méthodeinscrire(nom, contact, formation): stringqui 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
NotificateurConsolequi affiche le message avecconsole.loget renvoie « envoyé ». Avez-vous dû modifierInscriptionService? 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'interfaceReponseApi<T>de la leçon avec l'interfaceStagiairede l'activité 1 (sans la méthode). - Écrire une fonction
creerStagiaire(donnees: Omit<Stagiaire, 'id'>): ReponseApi<Stagiaire>qui attribue unidautomatique (compteur), et une fonctionmodifierStagiaire(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'
idde l'autre, et afficher chaque réponse avecafficherReponse.
Activité 6 : retour sur le projet Angular FormaPlus
- Dans le projet
forma-plusdu TP 1, créer une interfaceChiffreCle(valeur,libelle,icone) danssrc/app/models/chiffre-cle.tsavecng g i models/chiffre-cle, puis typer le tableau du composantChiffresCles:protected readonly chiffres: ChiffreCle[] = [...]. - Générer un composant
layout/horlogequi implémenteOnInitetOnDestroy(code de la leçon) et l'afficher dans le pied de page. - Renommer volontairement
ngOnInitenngOninit: quelle erreur afficheng serve? Retirer ensuiteimplements 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 |
-
À 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.
extendspermet à une interface d'en reprendre une ou plusieurs autres.implementsoblige 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,OmitetPickévitent de réécrire des interfaces proches.- Dans Angular : modèles de données,
OnInit,OnDestroy,PipeTransformet réponses HTTP typées.
- Une interface décrit la forme d'un objet : propriétés obligatoires, facultatives (
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.