Les interfaces en TypeScript : guide complet et pratique

Dans cet article

  • Une interface TypeScript définit un contrat de structure que chaque objet doit respecter pour garantir la cohérence du code
  • Le mot-clé interface permet de déclarer des propriétés obligatoires, optionnelles et en lecture seule sur un même type
  • L’héritage d’interfaces avec extends réduit la duplication et couvre plus de 80 % des cas de composition de types
  • La différence entre type et interface tient principalement à la fusion de déclarations et à l’extensibilité
  • Placer ses interfaces dans un dossier types/ ou interfaces/ dédié simplifie la maintenance sur les projets de plus de 50 fichiers
  • Implémenter une interface dans une classe avec implements garantit le respect du contrat à la compilation

Quand j’ai commencé à travailler avec TypeScript sur mes projets professionnels, la première fonctionnalité qui m’a réellement convaincu d’abandonner le JavaScript pur, c’est l’interface typescript. Cette notion, empruntée aux langages orientés objet comme Java ou C#, permet de décrire précisément la forme que doit prendre un objet. Résultat : les erreurs sont détectées avant même l’exécution, directement dans l’éditeur de code. Dans ce guide, je vous explique tout ce qu’il faut savoir pour maîtriser les interfaces, de la syntaxe de base jusqu’aux patterns avancés que j’utilise au quotidien.

Qu’est-ce qu’une interface en TypeScript

Une interface en TypeScript est un contrat de structure. Elle décrit les propriétés et les méthodes qu’un objet doit posséder, sans fournir d’implémentation. Concrètement, elle ne génère aucun code JavaScript à la compilation : elle sert uniquement au compilateur pour vérifier que votre code respecte les formes attendues.

Prenons un exemple simple. Si je développe une application qui gère des utilisateurs, je peux définir une interface User :

interface User {
  id: number;
  name: string;
  email: string;
}

À partir de ce moment, chaque variable typée User devra obligatoirement contenir un id numérique, un name et un email de type chaîne. Si j’oublie une propriété ou que je me trompe de type, le compilateur me le signale immédiatement. C’est ce mécanisme que la documentation officielle TypeScript appelle le structural typing : TypeScript ne vérifie pas le nom du type, mais sa structure.

Dans la pratique, cette approche élimine une grande partie des bugs que l’on rencontre habituellement en JavaScript. Sur un projet React ou Vue.js, par exemple, les props d’un composant peuvent être décrites par une interface. Le compilateur s’assure alors que chaque appel au composant fournit les bonnes données, au bon format.

Pourquoi utiliser une interface plutôt qu’un type

C’est la question que je reçois le plus souvent quand je forme des développeurs à TypeScript. Le mot-clé type (alias de type) et le mot-clé interface semblent faire la même chose. En réalité, ils partagent environ 90 % de fonctionnalités, mais divergent sur quelques points importants.

Le premier avantage de l’interface, c’est la fusion de déclarations (declaration merging). Si vous déclarez deux fois la même interface dans le même scope, TypeScript fusionne automatiquement les deux définitions. Ce comportement est très utile pour enrichir des types provenant de bibliothèques tierces :

interface Window {
  analytics: AnalyticsService;
}

Avec un type, cette opération est impossible : vous obtiendriez une erreur de duplication. Le second avantage concerne la lisibilité des messages d’erreur. Le compilateur affiche le nom de l’interface dans ses diagnostics, ce qui facilite le débogage sur les projets volumineux.

En revanche, le type reste indispensable pour les unions, les intersections conditionnelles et les types primitifs. Ma règle personnelle est simple : j’utilise interface pour décrire la forme d’un objet, et type pour tout le reste (unions, tuples, types mappés). Cette convention est d’ailleurs celle recommandée par l’équipe TypeScript elle-même.

Syntaxe et exemples pratiques

La syntaxe d’une interface TypeScript est volontairement proche de celle d’un objet JavaScript. Voici les formes les plus courantes que j’utilise dans mes projets.

Interface avec propriétés simples

interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

const laptop: Product = {
  id: 1,
  name: "MacBook Pro",
  price: 2499,
  inStock: true
};

Interface avec méthode

Une interface peut aussi décrire des signatures de méthodes. C’est ce que l’on appelle une interface method en TypeScript. La méthode est déclarée sans corps : seuls le nom, les paramètres et le type de retour sont spécifiés.

interface CartService {
  addItem(product: Product, quantity: number): void;
  removeItem(productId: number): void;
  getTotal(): number;
}

Ce pattern est particulièrement utile quand on travaille avec des applications Node.js : on définit l’interface du service, puis on crée autant d’implémentations que nécessaire (mémoire, base de données, API externe).

Interface pour une fonction

interface FormatPrice {
  (amount: number, currency: string): string;
}

const formatEuro: FormatPrice = (amount, currency) => {
  return `${amount.toFixed(2)} ${currency}`;
};

Cette syntaxe permet de typer des callbacks ou des fonctions de rappel de manière explicite, ce qui sécurise les échanges entre modules.

Propriétés optionnelles, readonly et signatures d’index

TypeScript offre plusieurs modificateurs pour affiner le comportement de vos interfaces. Ce sont ces détails qui font toute la différence entre un typage basique et un typage robuste.

Propriétés optionnelles

Le point d’interrogation (?) rend une propriété facultative :

interface UserProfile {
  id: number;
  name: string;
  bio?: string;
  avatarUrl?: string;
}

Ici, bio et avatarUrl peuvent être absentes de l’objet sans provoquer d’erreur. J’utilise ce mécanisme systématiquement pour les champs de formulaire non obligatoires.

Propriétés en lecture seule

Le mot-clé readonly empêche toute réaffectation après l’initialisation :

interface DatabaseConfig {
  readonly host: string;
  readonly port: number;
  readonly database: string;
}

const config: DatabaseConfig = {
  host: "localhost",
  port: 5432,
  database: "app_prod"
};

// config.port = 3306; // Erreur : Cannot assign to 'port'

C’est un garde-fou précieux pour les objets de configuration que l’on ne souhaite pas modifier accidentellement en cours d’exécution.

Signatures d’index

Quand la liste exacte des propriétés n’est pas connue à l’avance, les signatures d’index permettent de décrire un dictionnaire :

interface TranslationMap {
  [key: string]: string;
}

const fr: TranslationMap = {
  greeting: "Bonjour",
  farewell: "Au revoir"
};

Ce pattern se retrouve souvent dans les fichiers de traduction i18n ou les systèmes de cache clé-valeur.

Héritage et extension d’interfaces

L’un des atouts majeurs des interfaces TypeScript, c’est la possibilité de les étendre avec le mot-clé extends. Ce mécanisme fonctionne comme l’héritage de classes, mais sans code d’implémentation.

interface BaseEntity {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

interface User extends BaseEntity {
  name: string;
  email: string;
}

interface Article extends BaseEntity {
  title: string;
  content: string;
  author: User;
}

Dans cet exemple, User et Article héritent automatiquement des propriétés id, createdAt et updatedAt. Ce pattern évite la duplication et centralise la définition des champs communs. Sur les projets où je gère plus de 30 entités, cette approche réduit considérablement le volume de code de typage.

TypeScript autorise aussi l’héritage multiple : une interface peut étendre plusieurs interfaces simultanément :

interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface SoftDeletable {
  deletedAt: Date | null;
}

interface BlogPost extends Timestamped, SoftDeletable {
  title: string;
  slug: string;
  body: string;
}

C’est un mécanisme que l’on retrouve dans le concept de polymorphisme en programmation orientée objet. Il permet de composer des types complexes à partir de briques simples et réutilisables.

Où placer ses interfaces dans un projet

L’organisation des fichiers d’interfaces est un sujet qui revient à chaque code review. Après avoir travaillé sur des dizaines de projets TypeScript, voici la structure que je recommande.

Pour les petits projets (moins de 20 fichiers), un simple fichier types.ts à la racine du dossier src/ suffit. On y regroupe toutes les interfaces et types :

src/
  types.ts
  index.ts
  components/
  services/

Pour les projets moyens à grands, je préfère un dossier types/ ou interfaces/ avec un fichier par domaine métier :

src/
  types/
    user.ts
    product.ts
    order.ts
    index.ts   // re-exporte tout
  components/
  services/

Le fichier index.ts dans le dossier types/ sert de point d’entrée unique. Il utilise export pour rendre les interfaces disponibles dans tout le projet :

// types/index.ts
export { User, UserProfile } from './user';
export { Product, ProductCategory } from './product';
export { Order, OrderStatus } from './order';

Cette approche facilite l’import des interfaces depuis n’importe quel fichier du projet. On écrit simplement import { User } from '@/types' au lieu de naviguer dans l’arborescence. Si vous travaillez sur un projet React ou Vue.js, cette convention s’intègre naturellement avec les alias de chemins configurés dans le tsconfig.json.

Concernant l’export des interfaces, je recommande de toujours utiliser des named exports plutôt que des default exports. Cela simplifie l’autocomplétion dans les éditeurs et évite les incohérences de nommage entre fichiers.

Implémenter une interface dans une classe

Le mot-clé implements permet de lier une classe à une interface. La classe s’engage alors à fournir toutes les propriétés et méthodes déclarées dans l’interface :

interface Logger {
  info(message: string): void;
  error(message: string, stack?: string): void;
  warn(message: string): void;
}

class ConsoleLogger implements Logger {
  info(message: string): void {
    console.log(`[INFO] ${message}`);
  }

  error(message: string, stack?: string): void {
    console.error(`[ERROR] ${message}`);
    if (stack) console.error(stack);
  }

  warn(message: string): void {
    console.warn(`[WARN] ${message}`);
  }
}

Si je supprime la méthode warn de ma classe, le compilateur lève immédiatement une erreur. Ce mécanisme garantit la cohérence du contrat à chaque modification du code. Une classe peut aussi implémenter plusieurs interfaces simultanément :

class AppService implements Logger, Configurable {
  // doit fournir toutes les méthodes de Logger ET Configurable
}

Ce pattern est au cœur de l’injection de dépendances, une technique courante dans les frameworks comme les applications Node.js avec NestJS ou dans les projets Angular. On code contre l’interface, pas contre l’implémentation, ce qui facilite les tests unitaires et le remplacement de modules. La page Wikipédia sur le principe d’inversion des dépendances explique en détail cette philosophie de conception.

Interface ou type : le comparatif détaillé

Pour trancher définitivement le débat type vs interface TypeScript, voici un tableau récapitulatif des différences que j’ai constatées en pratique :

Fonctionnalité interface type
Décrire la forme d’un objet Oui Oui
Fusion de déclarations Oui Non
Extension avec extends Oui Oui (via intersection &)
Implémentation avec implements Oui Oui (avec limitations)
Types union (A | B) Non Oui
Types primitifs (string, number) Non Oui
Types conditionnels Non Oui
Types mappés Non Oui
Tuples Non Oui
Performance de compilation Légèrement meilleure Comparable
Messages d’erreur Plus lisibles Parfois opaques

En résumé, l’interface est le choix par défaut pour décrire des objets. Le type alias entre en jeu dès qu’il faut manipuler des unions, des tuples ou des types conditionnels. Dans un contexte React avec TypeScript, les deux coexistent naturellement : les interfaces décrivent les props des composants, tandis que les types gèrent les states complexes et les unions discriminées.

Bonnes pratiques pour organiser ses interfaces

Après des années à travailler sur des projets TypeScript de toutes tailles, voici les conventions que j’applique systématiquement.

Nommer ses interfaces clairement. J’utilise le PascalCase sans préfixe I. La convention IUser héritée de C# est déconseillée par l’équipe TypeScript. On écrit User, ProductService, ApiResponse.

Privilégier les interfaces granulaires. Plutôt qu’une interface monolithique de 30 propriétés, je découpe en interfaces spécialisées que je compose ensuite avec extends. Cela favorise la réutilisation et simplifie les tests.

Utiliser les génériques pour les patterns récurrents. Les types génériques TypeScript combinés aux interfaces permettent de créer des structures réutilisables :

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  total: number;
  page: number;
  perPage: number;
}

Ce pattern est devenu un standard dans mes projets. Il type les réponses API de manière cohérente sur l’ensemble du codebase, que ce soit pour une application frontend ou un backend Node.js.

Documenter les interfaces complexes. Pour les interfaces exposées à d’autres développeurs (librairies, SDK internes), j’ajoute des commentaires JSDoc au-dessus de chaque propriété :

interface PaymentConfig {
  /** Clé publique du fournisseur de paiement */
  publicKey: string;
  /** Activer le mode sandbox pour les tests */
  sandbox: boolean;
  /** Délai d'expiration en millisecondes (défaut : 30000) */
  timeout?: number;
}

Exporter et importer proprement. Sur les projets structurés, je centralise toutes les interfaces dans un barrel file (index.ts). Cela permet d’écrire des imports concis et de contrôler ce qui est exposé publiquement. Pour approfondir la mise en place d’un environnement TypeScript, vous pouvez consulter mon guide sur l’installation de Node.js sur Ubuntu ou celui sur la mise à jour de Node.js.

Enfin, si vous travaillez avec des énumérations TypeScript, sachez que les enums se combinent très bien avec les interfaces pour typer des champs à valeurs contraintes, comme un statut de commande ou un rôle utilisateur.

Pour ceux qui envisagent de se spécialiser en développement TypeScript, le marché est porteur. Les missions freelance en informatique demandent de plus en plus cette compétence, et des plateformes comme Free-Work référencent régulièrement des offres qui exigent la maîtrise des interfaces et du typage avancé. L’adoption d’une méthodologie DevOps sur ces projets TypeScript renforce encore la qualité du code livré.

À retenir

  • Utilisez interface par défaut pour décrire la forme de vos objets et réservez type aux unions et types conditionnels
  • Structurez vos interfaces dans un dossier types/ dédié avec un fichier index.ts qui centralise les exports
  • Exploitez extends pour composer des interfaces granulaires et réutilisables plutôt qu’un type monolithique
  • Appliquez implements dans vos classes pour garantir le respect du contrat dès la compilation
  • Combinez readonly et les propriétés optionnelles (?) pour un typage précis et sécurisé

Questions fréquentes


Qu’est-ce qu’une interface en TypeScript ?

Une interface en TypeScript est un contrat de structure qui définit les propriétés et les méthodes qu’un objet doit posséder. Elle ne génère aucun code JavaScript à la compilation : elle sert uniquement à vérifier, au moment du développement, que les objets respectent la forme attendue. C’est le mécanisme central du typage structurel de TypeScript.


Pourquoi utiliser interface plutôt que type en TypeScript ?

L’interface offre deux avantages principaux par rapport au type alias : la fusion de déclarations (declaration merging), qui permet d’enrichir une interface existante sans la modifier, et des messages d’erreur plus lisibles lors de la compilation. En revanche, le type reste nécessaire pour les unions, les tuples et les types conditionnels. La règle recommandée est d’utiliser interface pour les objets et type pour tout le reste.


Où placer ses interfaces dans un projet TypeScript ?

Sur les petits projets, un fichier unique types.ts à la racine de src/ suffit. Pour les projets plus importants, créez un dossier types/ ou interfaces/ avec un fichier par domaine métier (user.ts, product.ts, order.ts) et un fichier index.ts qui centralise les exports. Cette organisation facilite les imports et la maintenance à long terme.


Comment implémenter une interface dans une classe TypeScript ?

On utilise le mot-clé implements après le nom de la classe. La classe doit alors fournir une implémentation concrète de toutes les propriétés et méthodes déclarées dans l’interface. Si une méthode manque ou si un type ne correspond pas, le compilateur lève une erreur immédiatement. Une classe peut implémenter plusieurs interfaces en les séparant par des virgules.


Peut-on étendre plusieurs interfaces en TypeScript ?

Oui, TypeScript autorise l’héritage multiple d’interfaces. On utilise le mot-clé extends suivi des noms d’interfaces séparés par des virgules. L’interface résultante hérite de toutes les propriétés et méthodes des interfaces parentes. Ce mécanisme permet de composer des types complexes à partir de briques simples et réutilisables.


Quelle est la différence entre export interface et export type en TypeScript ?

Sur le plan de l’export, les deux fonctionnent de manière identique : on préfixe la déclaration avec le mot-clé export pour la rendre disponible dans d’autres fichiers. La différence réside dans les capacités du type exporté. Une interface exportée peut être étendue avec extends et fusionnée via declaration merging, tandis qu’un type exporté permet de déclarer des unions, des tuples et des types conditionnels.


Damien Roux
Damien Roux

Ingénieur système et expert hébergement web. Fondateur de web-city.fr, il partage guides pratiques, comparatifs objectifs et outils gratuits pour choisir le bon hébergeur et créer son site WordPress.

Scroll to Top