Le guide définitif : quand utiliser type vs interface en TypeScript
J’ai épluché toutes les sources récentes sur type vs interface. Voici ce qu’il faut retenir ainsi que la règle que j’applique depuis plusieurs mois.
Combien de fois avez-vous hésité entre type et interface en écrivant du TypeScript ? Et combien de fois avez-vous fini par choisir au hasard, en vous disant que de toute façon, c’est pareil ?
Alors autant le dire d’emblée : non, ce n’est pas pareil. Mais la bonne nouvelle, c’est que le consensus commence à émerger.
Pourquoi ce débat existe encore
La documentation officielle de TypeScript recommande encore aujourd’hui d’utiliser interface par défaut. Son heuristique exacte : utilisez interface jusqu’à ce que vous ayez besoin des fonctionnalités de type. Mais alors, quel est son argument ? Selon la doc, les interfaces seraient plus extensibles et donneraient de meilleurs messages d’erreur.
Sauf que TypeScript a beaucoup évolué. Les type alias sont devenus incroyablement puissants. Et une partie de la communauté a commencé à remettre en question ce conseil historique, sans pour autant que la doc officielle ne change de position.
Matt Pocock, une référence dans l’écosystème TypeScript, a résumé la situation : utilisez type par défaut, et interface uniquement quand vous avez besoin d’une fonctionnalité spécifique comme extends.
Mais pourquoi exactement ? C’est ce qu’on va voir.
Ce que type fait que interface ne peut pas faire
Les unions : la killer feature
Voici quelque chose d’impossible avec une interface :
type Status = “loading” | “success” | “error”;
type ID = string | number;Ces unions de types sont partout dans le code moderne : états de chargement, identifiants polymorphes, réponses d’API... Et interface ne sait tout simplement pas les exprimer.
Vous pouvez créer des unions à partir d’interfaces, mais vous ne pouvez pas définir une interface qui EST une union :
// Ceci fonctionne
interface Success { status: “success”; data: any }
interface Error { status: “error”; message: string }
type Result = Success | Error; // OK
// Mais pas ça
interface Result = Success | Error; // Erreur de syntaxeLes discriminated unions : la vraie puissance
C’est ici que les types brillent. Imaginez une gestion d’état pour une requête API :
type RequestState<T> =
| { status: “idle” }
| { status: “loading” }
| { status: “success”; data: T }
| { status: “error”; error: string };Quand vous vérifiez state.status === “success”, TypeScript sait automatiquement que state.data existe. Pas besoin de cast, pas de as, pas de ! : le compilateur comprend.
Les tuples et les types primitifs
Besoin d’un alias pour un type primitif ?
type UserID = string;
type Timestamp = number;
type Coordinates = [number, number];Avec une interface ? C’est tout simplement impossible parce qu’elle ne sait décrire que des objets.
Et vous allez me dire
Mais
type UserID = string, c’est juste un alias, TypeScript ne fait aucune vérification supplémentaire !
C’est vrai. Mais le code devient plus lisible. Quand je vois UserID au lieu de string, je sais immédiatement de quoi on parle.
Les mapped types et conditional types
Et là, cramponnez-vous, on part sur quelque chose de plus avancé :
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Nullable<T> = T | null;
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };Ces utility types sont impossibles à exprimer avec des interfaces. Si vous faites du TypeScript poussé, vous en aurez besoin tôt ou tard.
Ce que interface fait mieux (ou différemment)
La declaration merging : utile, mais attention !
Voici le super-pouvoir unique des interfaces :
interface User {
id: string;
name: string;
}
interface User {
email: string;
}
// TypeScript fusionne automatiquement
const user: User = {
id: “1”,
name: “Alice”,
email: “alice@example.com”
}; // Valide !Au premier abord, ça ressemble à une erreur n’est-ce pas ? Pourquoi voudrait-on déclarer deux fois la même interface ?
En fait, c’est très utile pour étendre des types de bibliothèques tierces. Par exemple, pour ajouter des propriétés à l’objet Request d’Express :
declare global {
namespace Express {
interface Request {
user?: { id: string; role: string };
}
}
}Sans cette fonctionnalité, il faudrait forker les types ou utiliser des hacks peu élégants.
Mais attention : cette fusion automatique peut créer des surprises. Si vous déclarez accidentellement deux interfaces avec le même nom dans le même scope, TypeScript les fusionne. Une nuance importante toutefois : si les propriétés portant le même nom ont des types incompatibles, le compilateur émettra une erreur. La fusion n’est donc “silencieuse” que lorsque les types sont compatibles, ce qui peut quand même mener à des comportements inattendus si vous n’avez pas conscience que deux interfaces se sont fusionnées.
Les types, eux, refusent catégoriquement d’être redéclarés. Plus prévisible, plus explicite.
La performance avec extends
Quand vous héritez d’un type, il y a une différence de performance :
// Avec interface (légèrement plus rapide)
interface Employee extends Person {
employeeId: string;
}
// Avec type (légèrement plus lent)
type Employee = Person & {
employeeId: string;
};TypeScript met en cache les relations d’héritage entre interfaces. Avec les intersections de types, il doit recalculer à chaque fois.
En pratique, vous ne verrez la différence que sur des projets vraiment massifs avec des milliers de types.
Les messages d’erreur
Les interfaces produisent souvent des messages d’erreur plus clairs :
interface Bird { wings: 2 }
type BirdType = { wings: 2 }
// Les erreurs avec Bird seront généralement plus lisibles que celles avec BirdTypeAvant TypeScript 4.2, les alias de type pouvaient apparaître sous leur forme anonyme dans les messages d’erreur, ce qui rendait le debugging pénible. Depuis, cette situation s’est nettement améliorée, mais les interfaces conservent un léger avantage : leur nom apparaît toujours sous leur forme originale.
C’est particulièrement vrai quand vous combinez plusieurs types avec des intersections. Le message peut devenir un pavé de texte incompréhensible. Avec des interfaces et extends, TypeScript affiche le nom de l’interface directement.
La règle que j’applique en 2026
Après avoir lu des dizaines d’articles, testé les deux approches sur plusieurs projets client ou perso et observé les tendances de la communauté, voici ma règle :
typepar défaut etinterfacepour les cas spécifiques
Donc oui, ça va à l’encontre de la recommandation officielle de TypeScript (qui pour rappel préconise toujours interface par défaut). Mais cette position est défendable : les fonctionnalités de type couvrent plus de cas d’usage et je préfère la cohérence d’utiliser un seul outil quand c’est possible.
Pour résumer :
Utilisez
typepour tout ce qui n’est pas un objet pur destiné à être étendu : unions, tuples, utility types, alias de primitifs et même la plupart des shapes d’objetsUtilisez
interfacelorsque vous avez besoin de declaration merging (extension de types tiers), quand vous définissez un contrat pour des classes ou quand vous construisez une API publique que d’autres développeurs pourraient vouloir étendre.
Pour vous donner un exemple, voici comment je structure un module typique :
// Types pour les états et les unions
type LoadingState = “idle” | “loading” | “success” | “error”;
type ApiResponse<T> =
| { status: “success”; data: T }
| { status: “error”; message: string };
// Type pour un objet simple (pas besoin d’extension)
type UserDTO = {
id: string;
name: string;
email: string;
};
// Interface pour un contrat de service (pourrait être implémenté par plusieurs classes)
interface UserRepository {
findById(id: string): Promise<UserDTO | null>;
save(user: UserDTO): Promise<void>;
}Pour autant, est-ce que cette distinction est gravée dans le marbre ? Non. Certaines équipes avec lesquelles j’ai travaillé préfèrent utiliser interface pour tous les objets, même sans héritage. L’essentiel, c’est d’être cohérent dans votre codebase.
Le vrai conseil : soyez cohérent
Au final, la différence entre type et interface est suffisamment faible pour que le choix dépende de vos préférences et de celles de votre équipe.
Vous pouvez suivre la recommandation officielle (qui je le rappelle est interface par défaut) ou adopter l’approche inverse (type par défaut). Les deux positions sont défendables.
Ce qui compte vraiment c’est :
de choisir une convention
de la documenter
et de l’appliquer partout
Un projet où tout le monde utilise interface sera plus maintenable qu’un projet où chacun fait à sa sauce.
Et si vous démarrez un nouveau projet ? Essayez type par défaut. Vous verrez, c’est libérateur de ne plus se poser la question à chaque fois.
Cette semaine, faites le test
Ouvrez un de vos projets TypeScript. Comptez combien de fois vous (vous et vos collègues) avez utilisé interface alors qu’un type aurait suffi. Je parie que le ratio va vous surprendre.


