frontend / typescript
Interfaces vs Type Aliases
Explication
Ce que vous allez apprendre
- Définir la forme d'un objet avec
interfaceet avectype - Étendre une interface avec
extendset combiner des types avec l'intersection& - Expliquer ce qu'est la fusion de déclarations (declaration merging) et à quoi elle sert
- Choisir entre
interfaceettypeselon une convention claire et justifiée - Repérer une fusion de déclarations involontaire qui modifie un type sans qu'on s'y attende
Dans quel contexte ?
Dans une équipe backend, un développeur ajoute une seconde déclaration interface Config { timeout: number; } dans un fichier séparé, sans savoir qu'une interface Config existe déjà ailleurs dans le projet avec un champ apiUrl: string. Grâce à la fusion de déclarations, TypeScript combine silencieusement les deux en un seul type Config avec les deux champs, ce qui peut être voulu (enrichir un type de bibliothèque externe) ou, comme ici, une source de confusion si ce n'était pas l'intention du développeur qui pensait créer un type totalement indépendant.
Deux façons de nommer une forme de données
Dès que vous manipulez des objets structurés en TypeScript, vous avez besoin de leur donner un nom réutilisable plutôt que de répéter leur définition partout. Deux outils permettent cela : interface et type. Beaucoup de débutants les confondent ou hésitent entre les deux — cette leçon clarifie quand utiliser lequel.
Prérequis
Cette leçon suppose les types de base (leçon 2) acquis, en particulier la notion d'objet typé. Aucune connaissance des classes n'est nécessaire, même si interface est souvent utilisée en lien avec elles.
Interface : un contrat extensible
Une interface décrit la forme d'un objet et se comporte comme un contrat : elle peut être étendue par héritage (extends) et, particularité unique, deux déclarations d'interface portant le même nom fusionnent automatiquement en une seule. Cette fusion de déclarations est très utile pour enrichir un type venant d'une bibliothèque externe, mais elle peut aussi surprendre si elle est involontaire.
Type alias : plus polyvalent
Un type peut nommer littéralement n'importe quoi : un objet, mais aussi une union de valeurs possibles, un tuple, ou même un simple alias sur une primitive. Sa contrepartie de l'héritage se fait via l'intersection (&) plutôt que extends. Contrairement à interface, deux type portant le même nom provoquent une erreur de compilation plutôt qu'une fusion.
| Aspect | interface | type |
|---|---|---|
| Objets | Oui | Oui |
| Unions, tuples, primitives | Non | Oui |
| Extension | extends | Intersection & |
| Deux déclarations du même nom | Fusionnent (declaration merging) | Erreur de compilation |
Piège fréquent
Deux interface du même nom dans des fichiers différents fusionnent silencieusement au lieu de provoquer une erreur, contrairement à type qui aurait immédiatement signalé le doublon. Si vous voulez qu'un nom de type réutilisé par erreur soit détecté à la compilation, préférez type.
Comment choisir
La règle pratique la plus répandue consiste à utiliser interface pour des objets destinés à être étendus ou implémentés par des classes (API publique, contrats), et type pour tout le reste : unions, tuples, alias de primitives. Ce choix n'est pas un dogme absolu, mais une convention qui facilite la lecture d'un code TypeScript par une autre personne.
Commandes & code
Interfaces vs Type Aliases
// Interface : orientée objets/contrats, extensible (declaration merging)
interface Utilisateur {
id: number;
nom: string;
email?: string; // propriété optionnelle
readonly creeLe: Date; // non réassignable après création
}
// Extension par héritage
interface Admin extends Utilisateur {
permissions: string[];
}
// Declaration merging : deux interfaces du même nom fusionnent
interface Fenetre {
titre: string;
}
interface Fenetre {
largeur: number;
}
// Fenetre possède maintenant { titre, largeur } — utile pour étendre des types de libs tierces
// Type alias : peut nommer N'IMPORTE QUEL type (union, tuple, primitif...)
type ID = string | number;
type Coordonnees = [number, number];
type StatutCommande = "en_attente" | "expediee" | "livree" | "annulee";
// Type alias avec objet (proche d'une interface pour ce cas précis)
type Produit = {
id: ID;
nom: string;
prix: number;
};
// Intersection avec &, équivalent "extends" pour les type aliases
type ProduitEnPromo = Produit & { pourcentageRemise: number };
const promo: ProduitEnPromo = { id: 1, nom: "Clavier", prix: 49, pourcentageRemise: 20 };
// PAS de declaration merging pour type — ceci est une ERREUR :
// type Fenetre = { titre: string };
// type Fenetre = { largeur: number }; // Error TS2300: Duplicate identifier 'Fenetre'
// Une classe peut implémenter une interface OU un type alias objet
class UtilisateurAdmin implements Admin {
constructor(
public id: number,
public nom: string,
public readonly creeLe: Date,
public permissions: string[]
) {}
}| Besoin | interface | type |
|---|---|---|
| Union / tuple / primitive | non | oui |
| Extension multiple | extends | & |
| Fusion automatique (merging) | oui | non |
| Implémentée par une classe | oui | oui (objet) |
Résumé
interfacepour des contrats d'objets amenés à être étendus (API publiques, libs).typepour tout le reste : unions, tuples, alias de primitives, intersections.- Le merging automatique n'existe que pour
interface— un piège classique en cas de doublon.
Exercices pratiques
Mission : un Config qui grossit tout seul
Objectif : Comprendre un cas de fusion de déclarations involontaire, puis choisir le bon outil (interface ou type) pour l'empêcher.
Contexte
Un développeur backend ajoute interface Config { timeout: number; } dans un nouveau fichier, sans savoir qu'une interface Config { apiUrl: string; } existe déjà ailleurs dans le projet. TypeScript ne signale rien : les deux fusionnent silencieusement en un seul type Config avec les deux champs. Ce n'était pas voulu, et personne ne l'a remarqué avant une revue de code approfondie.