Menu

Interface vs type en TypeScript : différences et quand les utiliser

interface et type peuvent tous deux décrire des formes d'objets, et la plupart du temps l'un ou l'autre fonctionne. Découvrez les vraies différences : fusion de déclarations, unions et types mappés, extends contre intersections, signatures d'index implicites, messages d'erreur et performances du compilateur, plus une règle claire pour choisir.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

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éinterfacetype
Formes d'objetsouioui
Optionnel, readonly, méthodes, signatures d'indexouioui
Génériquesouioui
Unions (A | B)nonoui
Tuples, primitives, types de fonctions seulsnon (types de fonctions uniquement via des signatures d'appel)oui
Types mappés et conditionnelsnonoui
Extensionextends, les conflits sont des erreurs&, les conflits deviennent never
Fusion de déclarationsouinon (identifiant dupliqué)
Assignable à Record<string, T>nonoui, quand les propriétés correspondent
implements dans une classeouioui, si c'est un type objet
Définitions récursivesouioui

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 :

  1. Formes d'objets : interface. Vous obtenez un extends vé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 : « utilisez interface jusqu'à ce que vous ayez besoin de fonctionnalités de type ».
  2. 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.
  3. Exception : utilisez type pour une forme d'objet qui doit correspondre à des paramètres Record<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 &.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER