Pour décrire la forme d'un objet, interface et type font le même travail, et les valeurs de l'un sont assignables à l'autre quand les formes correspondent. Les différences se situent aux marges : type peut nommer des choses qu'une interface ne peut pas (unions, tuples, types calculés), et une interface sait faire quelques choses qu'un alias de type ne sait pas faire (fusion, extends vérifié).
TypeScript compare les types objet par leur structure : le nom de la déclaration ne compte donc pas pour l'assignabilité. Voici la liste des endroits où le choix, lui, compte.
Tableau comparatif
| Fonctionnalité | interface | type |
|---|---|---|
| Formes d'objets | oui | oui |
| Optionnel, readonly, méthodes, signatures d'index | oui | oui |
| Génériques | oui | oui |
Unions (A | B) | non | oui |
| Tuples, primitives, types de fonctions seuls | non (types de fonctions uniquement via des signatures d'appel) | oui |
| Types mappés et conditionnels | non | oui |
| Extension | extends, les conflits sont des erreurs | &, les conflits deviennent never |
| Fusion de déclarations | oui | non (identifiant dupliqué) |
Assignable à Record<string, T> | non | oui, quand les propriétés correspondent |
implements dans une classe | oui | oui, si c'est un type objet |
| Définitions récursives | oui | oui |
Ce que seul type sait faire
Tout ce qui n'est pas une forme d'objet unique demande un alias de type :
Aucun de ces types ne peut s'écrire avec interface (les deux derniers sont traités dans types mappés et types conditionnels). C'est la raison pratique pour laquelle toute base de code finit par utiliser type quelque part, quel que soit son choix pour les formes d'objets.
Ce que seule interface sait faire : la fusion de déclarations
Deux déclarations interface du même nom dans la même portée se combinent en une seule. Deux déclarations type du même nom provoquent l'erreur TS2300, Duplicate identifier.
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
const s: Settings = { theme: "dark", fontSize: 14 }; // needs both
type Options = { a: number };
type Options = { b: number }; // error TS2300: Duplicate identifier 'Options'.
La fusion permet d'étendre de l'extérieur les types d'une bibliothèque : ajouter une propriété au Window global, au Request d'Express, ou au type de thème d'une bibliothèque. Si vous publiez des types que les utilisateurs peuvent avoir besoin d'augmenter, utilisez des interfaces. Dans votre propre code applicatif, une fusion accidentelle (deux fichiers de script déclarant le même nom d'interface globale) se produit en silence, sauf si les deux déclarent la même propriété avec des types différents : c'est l'un des arguments de certaines équipes pour préférer type.
extends ou intersection
Une interface s'étend avec extends, un alias de type avec &. Le résultat est en général le même, mais ils traitent différemment une propriété contradictoire. extends signale le conflit au niveau de la déclaration :
index.ts(7,11): error TS2430: Interface 'Broken' incorrectly extends interface 'Base'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'.
Une intersection accepte le même conflit en silence et transforme la propriété en never (un string qui serait aussi un number). L'erreur n'apparaît que plus tard, quand vous essayez de créer une valeur :
L'erreur désigne l'objet, loin de la déclaration qui l'a causée. Pour construire des types objet à partir d'autres types objet, extends donne le meilleur message.
Signatures d'index : une différence facile à manquer
Un alias de type pour un type objet reçoit une signature d'index implicite, il peut donc être passé là où un Record<string, unknown> est attendu. Une interface, non. C'est voulu : l'explication de l'équipe TypeScript (issue #15300 sur le dépôt TypeScript) est qu'une interface peut être augmentée par des déclarations ultérieures, qu'il est donc moins sûr de lui inférer une signature d'index, et que changer la règle aujourd'hui casserait trop de code.
L'appel marqué @ts-expect-error s'exécute quand même et affiche les champs d'Alan : la règle n'agit qu'à la compilation. C'est la cause habituelle d'une erreur déroutante quand une valeur typée par une interface est passée à un helper de journalisation, de sérialisation ou de requête typé avec Record<string, ...>. Passez cette déclaration à type, étalez la valeur, ou typez plutôt le paramètre du helper avec une interface ou un générique.
Performances et messages d'erreur
La page Performance du wiki TypeScript (section « Preferring Interfaces Over Intersections ») suggère interface Foo extends Bar, Baz { ... } plutôt que type Foo = Bar & Baz & { ... } pour composer des types objet. Ses raisons : une interface est un type objet plat unique qui détecte les conflits de propriétés, les relations de types entre interfaces sont mises en cache (les intersections dans leur ensemble ne le sont pas), et vérifier une valeur par rapport à une intersection vérifie chaque constituant avant le type aplati. La différence compte dans les grosses bases de code avec beaucoup de types composés ; pour quelques formes d'objets ordinaires, vous ne la mesurerez pas.
La même page note que les interfaces s'affichent mieux. Une interface apparaît sous son nom au survol et dans les messages d'erreur, alors qu'un alias d'intersection est souvent affiché développé en ses parties, ce qui rend les longs messages plus difficiles à lire.
Lequel utiliser
Une règle qui fonctionne :
- Formes d'objets :
interface. Vous obtenez unextendsvérifié, des erreurs plus claires sur les grosses compositions, et les utilisateurs d'une bibliothèque peuvent l'augmenter. Cela correspond à l'heuristique du handbook TypeScript : « utilisezinterfacejusqu'à ce que vous ayez besoin de fonctionnalités detype». - Tout le reste :
type. Unions, tuples, types de fonctions, types littéraux, et tout ce qui se construit avec des types mappés, conditionnels ou template literal. - Exception : utilisez
typepour une forme d'objet qui doit correspondre à des paramètresRecord<string, ...>, ou quand vous voulez volontairement empêcher la fusion.
Utiliser type partout est aussi un choix cohérent, et beaucoup de bases de code le font. Mélanger les deux au hasard est la seule option à éviter : les lecteurs se demandent alors si une différence était voulue.
Questions fréquentes
Quelle est la différence entre type et interface en TypeScript ?
Les deux décrivent des formes d'objets et sont interchangeables pour cet usage. type peut aussi nommer des unions, des tuples, des primitives, et des types mappés ou conditionnels, ce que interface ne peut pas. interface permet la fusion de déclarations et extends, qui vérifie les propriétés contradictoires. Les types objet écrits avec type sont aussi assignables à des types à signature d'index comme Record<string, unknown>, ce qui n'est pas le cas des interfaces.
Faut-il utiliser type ou interface ?
L'heuristique du handbook TypeScript est la suivante : utilisez interface jusqu'à ce que vous ayez besoin d'une fonctionnalité propre à type. En pratique, cela donne des interfaces pour les formes d'objets et type pour les unions, les tuples, les types de fonctions et les types calculés. Les équipes qui utilisent type partout s'en sortent aussi très bien ; l'important est d'avoir une règle cohérente.
interface est-il plus rapide que type en TypeScript ?
Pour composer des types objet, oui dans certains cas. Les recommandations de performance de l'équipe TypeScript préconisent interface ... extends plutôt que de grosses intersections (A & B & { ... }), car les relations entre interfaces sont mises en cache et une interface est un type plat unique. Pour une simple forme d'objet, il n'y a pas de différence notable.
Une classe peut-elle implémenter un alias de type ?
Oui, si l'alias est un type objet (ou une intersection de types objet) : class Point implements PointType { ... } fonctionne. Une classe ne peut pas implémenter un type union ; c'est l'erreur TS2422.
Une interface peut-elle étendre un alias de type ?
Oui, tant que l'alias est un type objet : type Base = { id: string }; interface User extends Base { name: string } est valide. Elle ne peut pas étendre un alias d'union. Dans l'autre sens, un alias de type peut s'appuyer sur une interface avec &.