type vs interface en TypeScript : quelle différence ?
C’est l’une des questions récurrentes quand on apprend TypeScript : faut-il déclarer ses structures avec type ou avec interface ? Les deux permettent de décrire la forme d’un objet, et dans la majorité des cas, ils sont interchangeables. Mais il existe des différences réelles qui, selon le contexte, penchent en faveur de l’un ou de l’autre. Ce guide fait le tri, sans dogmatisme.
Le point de départ : deux façons de décrire un objet
Pour typer un objet, type et interface produisent un résultat quasi identique.
interface UtilisateurI {
id: number;
nom: string;
actif?: boolean;
}
type UtilisateurT = {
id: number;
nom: string;
actif?: boolean;
};
Les deux déclarations sont utilisables exactement de la même manière, et TypeScript signalera les mêmes erreurs si vous oubliez une propriété ou vous trompez de type. Si vous découvrez le typage statique, notre guide TypeScript explique d’abord comment poser ces bases.
La question n’est donc pas « lequel fonctionne », mais « lequel est le plus adapté selon ce que vous voulez faire ». Voyons les vraies divergences.
Différence 1 : type gère les unions et les primitifs
C’est la distinction la plus importante. Un type peut représenter bien plus qu’un objet : une union, un type primitif renommé, un tuple, une valeur littérale. Une interface, elle, décrit toujours une forme d’objet (ou de fonction/classe).
// Ces déclarations sont IMPOSSIBLES avec interface
type Statut = "brouillon" | "publie" | "archive"; // union de littéraux
type Id = string | number; // union de primitifs
type Coordonnees = [number, number]; // tuple
type Nom = string; // alias d'un primitif
// interface ne peut pas faire ceci
// interface Statut = "brouillon" | "publie"; ❌ syntaxe invalide
Dès que vous avez besoin d’une union, d’un tuple ou d’un alias de primitif, type est la seule option. C’est de loin le cas d’usage qui tranche le plus souvent la décision.
Différence 2 : la fusion de déclaration (declaration merging)
Une interface possède une capacité que type n’a pas : si vous la déclarez deux fois avec le même nom, TypeScript fusionne les deux définitions. C’est ce qu’on appelle la fusion de déclaration.
interface Fenetre {
titre: string;
}
interface Fenetre {
largeur: number;
}
// Les deux déclarations fusionnent automatiquement
const f: Fenetre = { titre: "Accueil", largeur: 1024 }; // ✅
Avec type, redéclarer le même nom provoque une erreur :
type Fenetre = { titre: string };
type Fenetre = { largeur: number }; // ❌ Erreur : identifiant dupliqué
Cette fusion peut sembler anecdotique, mais elle est essentielle pour étendre des types tiers ou globaux : ajouter une propriété à Window, enrichir les types d’une bibliothèque via un fichier de déclaration, etc. C’est la raison historique pour laquelle beaucoup de bibliothèques exposent leurs contrats sous forme d’interface.
Différence 3 : l’extension
Les deux permettent de composer, mais avec une syntaxe différente. interface utilise extends, tandis que type utilise l’intersection &.
// Avec interface
interface Animal {
nom: string;
}
interface Chien extends Animal {
race: string;
}
// Avec type
type AnimalT = {
nom: string;
};
type ChienT = AnimalT & {
race: string;
};
Le résultat est équivalent. Une interface peut même étendre un type (et inversement, un type peut composer une interface), donc les deux mondes cohabitent sans friction.
type Base = { id: number };
interface Enrichi extends Base { // interface peut étendre un type objet
nom: string;
}
Une nuance de performance existe : extends sur des interfaces produit un « cache » que TypeScript exploite, ce qui peut rendre la vérification plus rapide sur de très grandes hiérarchies de types que les intersections & répétées. En pratique, cela ne se ressent que sur des bases de code massives.
Différence 4 : les types calculés
type va plus loin dès qu’il s’agit de transformer un type existant. Types mappés, types conditionnels, opérateurs sur les clés : tout cela relève du territoire de type.
interface Utilisateur {
id: number;
nom: string;
email: string;
}
// Type mappé : rendre tout optionnel (impossible avec interface)
type UtilisateurPartiel = {
[K in keyof Utilisateur]?: Utilisateur[K];
};
// Type conditionnel
type NonNul<T> = T extends null | undefined ? never : T;
Ces constructions n’ont pas d’équivalent en interface. Toute la « programmation au niveau des types » repose donc sur type.
Tableau récapitulatif
| Capacité | interface | type |
|---|---|---|
| Décrire un objet | ✅ | ✅ |
Unions (A | B) | ❌ | ✅ |
| Tuples, primitifs, littéraux | ❌ | ✅ |
| Fusion de déclaration | ✅ | ❌ |
| Extension | extends | & |
| Types mappés / conditionnels | ❌ | ✅ |
| Étendre un type tiers global | ✅ (idéal) | limité |
Quand utiliser l’un ou l’autre
Plutôt que de trancher dogmatiquement, voici une règle pragmatique adoptée par une grande partie de l’écosystème.
Utilisez interface par défaut pour les objets et les contrats publics. Notamment les props de composants, les formes de données d’API, les contrats d’une bibliothèque. Vous bénéficiez de messages d’erreur souvent plus lisibles, de la fusion de déclaration si un tiers doit étendre votre type, et d’une intention claire (« ceci est une structure »).
// Contrat clair d'un composant : interface
interface ButtonProps {
label: string;
onClick: () => void;
variante?: "primaire" | "secondaire";
}
Utilisez type dès que vous sortez du simple objet. Union, tuple, alias de primitif, type calculé, ou combinaison de plusieurs types.
// Union et transformation : type
type Theme = "clair" | "sombre" | "auto";
type Resultat<T> = { ok: true; valeur: T } | { ok: false; erreur: string };
L’essentiel
Dans 80 % des cas, type et interface sont interchangeables et le choix relève surtout de la convention d’équipe. Les critères qui tranchent réellement :
- Besoin d’une union, d’un tuple ou d’un primitif ? →
type, obligatoire. - Besoin de fusion de déclaration ou d’étendre un type global ? →
interface. - Besoin de types calculés (mappés, conditionnels) ? →
type. - Simple objet ou props de composant ? →
interfacepar convention, maistypefonctionne aussi.
Le plus important n’est pas de choisir « le bon », mais de rester cohérent dans un même projet. Fixez une règle d’équipe et tenez-vous-y : c’est ce qui rend le code lisible sur la durée.