~3 min de lecture
type vs interface en TypeScript : il est temps de trancher
Si tu as récemment mis à jour tes règles ESLint, tu es peut-être tombé sur
@typescript-eslint/consistent-type-definitions. Cette règle te force à choisir entre type et interface pour tes
définitions de types objets.
Nous l'avons aperçu lors de notre montée de version vers Angular v21.
Mais comment choisir ? Et surtout, pourquoi ce choix a-t-il autant d'importance aujourd'hui ?
La règle ESLint en question
{
"rules": {
"@typescript-eslint/consistent-type-definitions": [
"error",
"type"
]
// ou "interface"
}
}
L'objectif : garantir la cohérence dans ta codebase. Fini le mélange arbitraire entre type et interface selon
l'humeur du développeur.
Les vraies différences techniques
Avant de choisir, clarifions ce que chacun peut faire.
Ce que seul type peut faire
// Union types
type Status = 'pending' | 'active' | 'closed';
type Result<T> = Success<T> | Error;
// Mapped types
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Conditional types
type Unwrap<T> = T extends Promise<infer U> ? U : T;
// Tuples et primitives
type Coordinates = [number, number];
type ID = string | number;
Ce que seul interface peut faire
// Declaration merging
interface Window {
myCustomProperty: string;
}
// Fusionne automatiquement avec l'interface Window existante
Le declaration merging est utile pour étendre des types de librairies tierces sans modifier leur code source.
Pour les objets simples : équivalents
// Ces deux définitions sont fonctionnellement identiques
type User = {
id: string;
name: string;
};
interface User {
id: string;
name: string;
}
Pourquoi interface a dominé pendant des années
La réponse est historique. Regardons l'évolution de type dans TypeScript :
| Version | Année | Capacités de type |
|---|---|---|
| 1.0 | 2014 | Alias basiques uniquement (type ID = string) |
| 1.4 | 2015 | Union types (A | B) |
| 1.6 | 2015 | Intersection types (A & B) |
| 2.1 | 2016 | Mapped types (keyof, in) |
| 2.8 | 2018 | Conditional types |
En TypeScript 1.x, type ne pouvait tout simplement pas décrire des objets complexes :
// TypeScript 1.0 - Ce qui était possible
type ID = string;
type Callback = () => void;
// Ce qui était IMPOSSIBLE
type User = { name: string }; // ❌
type Admin = User & { role: string }; // ❌
type Status = 'on' | 'off'; // ❌
Angular 2 est sorti en 2016. Sa documentation a été écrite à une époque où interface était le seul choix viable
pour les modèles de données. Cette convention s'est perpétuée par habitude, même si elle n'a plus de justification
technique.
L'argument sémantique : type pour les données
Au-delà des capacités techniques, il y a un argument de sens :
interface→ un contrat, un comportement (ce qu'un objet peut faire)type→ une description de structure (ce qu'une donnée est)
Un modèle de données (DTO, entité) décrit ce que c'est, pas un contrat d'implémentation :
// Données → type (ce que c'est)
type User = {
id: string;
name: string;
email: string;
};
type UserRole = 'admin' | 'editor' | 'viewer';
type UserWithRole = User & {
role: UserRole;
};
// Contrats → interface (ce que ça fait)
interface UserRepository {
findById(id: string): Promise<User>;
save(user: User): Promise<void>;
}
interface Serializable {
serialize(): string;
}
Cette distinction n'est pas juste théorique. Elle rend le code plus lisible : quand tu vois interface, tu sais
immédiatement qu'il s'agit d'un contrat à implémenter.
Notre recommandation
Configure ESLint avec "type" par défaut :
{
"rules": {
"@typescript-eslint/consistent-type-definitions": [
"error",
"type"
]
}
}
Puis utilise interface uniquement quand tu en as besoin :
- Declaration merging (extension de types externes)
- Contrats explicites que des classes doivent implémenter
// Vos modèles de données
type Product = {
id: string;
name: string;
price: number;
};
type CartItem = Product & {
quantity: number;
};
type PaymentStatus = 'pending' | 'completed' | 'failed';
// Vos contrats de service
interface PaymentGateway {
process(amount: number): Promise<PaymentStatus>;
refund(transactionId: string): Promise<void>;
}
// Extension d'un type externe
interface ImportMeta {
env: Record<string, string>;
}
Et les performances ?
Tu liras parfois que interface est plus performant à la compilation. C'est techniquement vrai : le compilateur
TypeScript peut mettre en cache les interfaces plus efficacement.
En pratique ? La différence est négligeable sauf sur des codebases de plusieurs millions de lignes. Ne laisse pas cet argument guider ton choix.
En résumé
| Usage | Choix | Raison |
|---|---|---|
| Modèles de données | type |
Sémantique : décrit ce que c'est |
| Unions, intersections | type |
Seul capable |
| Mapped/Conditional types | type |
Seul capable |
| Contrats de service | interface |
Sémantique : décrit un comportement |
| Extension de types externes | interface |
Declaration merging |
La règle @typescript-eslint/consistent-type-definitions te force à prendre cette décision une fois pour toutes.
Fais le bon choix : type par défaut, interface quand c'est justifié.