Menu

Interface en TypeScript : syntaxe, propriétés optionnelles, méthodes

Une interface nomme la forme d'un objet : ses propriétés et les types qu'elles contiennent. Apprenez à en déclarer une, les propriétés optionnelles et readonly, les méthodes, les signatures d'index, l'extension, l'implémentation dans une classe, la fusion de déclarations, les interfaces génériques et comment donner des valeurs par défaut à une interface.

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

Une interface donne un nom à la forme d'un objet : les propriétés qu'il doit avoir et le type de chacune. Une fois déclarée, vous utilisez son nom comme un type, et le compilateur vérifie chaque objet que vous passez, renvoyez ou assignez par rapport à elle.

Le dernier appel provoque l'erreur de compilation TS2741. Les interfaces sont effacées à la compilation : la sortie JavaScript ne garde aucune trace de User, et rien ne vérifie la forme à l'exécution.

Déclarer une interface

La syntaxe est le mot-clé interface, un nom (en PascalCase par convention) et un corps qui liste les membres. Les membres peuvent être séparés par des points-virgules, des virgules ou de simples retours à la ligne ; les points-virgules sont le style courant.

interface Product {
  sku: string;               // required property
  price: number;
  tags: string[];            // array property
  dimensions: {              // nested object type
    width: number;
    height: number;
  };
  discount?: number;         // optional property
  readonly createdAt: Date;  // cannot be reassigned
  label(): string;           // method
}

Une interface est un type, pas une valeur. Elle ne peut pas être instanciée avec new, n'a pas de valeurs par défaut, et obj instanceof Product provoque l'erreur TS2693 ('Product' only refers to a type, but is being used as a value here). Pour vérifier une forme à l'exécution, écrivez un type guard.

Typage structurel et vérification des propriétés en trop

TypeScript compare des formes, pas des noms. Tout objet qui possède les propriétés requises correspond à l'interface, qu'il ait été déclaré avec elle ou non. Les propriétés supplémentaires sont acceptées, à une exception près : un littéral objet écrit directement là où l'interface est attendue subit une vérification des propriétés en trop, car une clé inconnue à cet endroit est presque toujours une faute de frappe.

C'est cette erreur (TS2353) qui détecte { id: 1, name: "a", emial: "x" } pour un User : le compilateur suggère même Did you mean to write 'email'? (TS2561).

Propriétés optionnelles et readonly

Un ? après le nom rend une propriété optionnelle : l'objet peut l'omettre, et la lire donne T | undefined. readonly interdit de réassigner la propriété après la création de l'objet.

Deux limites apparaissent ici. D'abord, readonly n'agit qu'à la compilation : les deux lignes marquées @ts-expect-error s'exécutent quand même quand vous cliquez sur Run, et elles réussissent, et les dernières lignes modifient apiUrl via une référence typée sans readonly. Ensuite, il est superficiel : readonly hosts: string[] empêcherait de réassigner hosts mais autoriserait toujours hosts.push(...), c'est pourquoi le tableau lui-même est typé readonly string[]. Il documente et impose une intention dans le code typé ; il ne gèle rien. Le type utilitaire Readonly<T> rend readonly d'un coup toutes les propriétés d'une interface existante.

Méthodes et propriétés de type fonction

Une méthode peut s'écrire comme une signature de méthode, name(params): ReturnType, ou comme une propriété contenant une fonction, name: (params) => ReturnType. Les appelants utilisent les deux de la même façon.

La différence est subtile : avec strictFunctionTypes (qui fait partie de strict), les paramètres des propriétés de type fonction sont vérifiés strictement, alors que les signatures de méthode le sont de façon plus souple (bivariante), donc la forme propriété détecte quelques erreurs de plus. La syntaxe de méthode est plus courte et plus répandue ; les deux conviennent.

Une interface peut aussi décrire quelque chose d'appelable ou de constructible, avec une signature d'appel ou une signature de construction :

interface Formatter {
  (value: number): string; // call signature: the object is a function
  locale: string;          // and it also has a property
}

interface PointConstructor {
  new (x: number, y: number): { x: number; y: number }; // construct signature
}

Signatures d'index

Quand les noms des propriétés ne sont pas connus à l'avance, une signature d'index les décrit tous d'un coup : [key: string]: T signifie « n'importe quelle clé de type string, chacune contenant un T ».

Les dernières lignes montrent le piège : lire une clé qui n'existe pas est typé number, et non number | undefined. L'option de compilation noUncheckedIndexedAccess ajoute | undefined à chacune de ces lectures.

Des propriétés nommées peuvent côtoyer une signature d'index, mais elles doivent lui correspondre. interface Dict { [key: string]: number; name: string } provoque l'erreur TS2411, Property 'name' of type 'string' is not assignable to 'string' index type 'number'. Élargissez le type d'index ([key: string]: number | string) ou déplacez la partie dynamique dans sa propre propriété. Pour les tables clé/valeur simples, Record<string, number> dit la même chose en une ligne.

Étendre une interface

extends construit une nouvelle interface à partir d'une ou plusieurs interfaces existantes. L'enfant possède tous les membres du parent, plus les siens :

interface Animal {
  name: string;
}

interface Pet extends Animal {
  owner: string;
}

interface Trained {
  commands: string[];
}

interface ServiceDog extends Pet, Trained {
  certifiedUntil: Date;
}

// ServiceDog requires: name, owner, commands, certifiedUntil

Un enfant ne peut redéclarer une propriété du parent qu'avec un type compatible (plus étroit), comme kind: "dog" là où le parent indique kind: string. Les règles, et la manière d'étendre des alias de type, sont sur la page extends.

Implémenter une interface dans une classe

class X implements Shape demande au compilateur de vérifier que la classe possède tout ce que l'interface exige. Un membre manquant est une erreur au niveau de la déclaration de la classe :

index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
  Property 'area' is missing in type 'Circle' but required in type 'Shape'.

Une fois area() ajouté, plusieurs classes, et même un objet simple, peuvent toutes être utilisées comme un Shape :

implements n'est qu'une vérification. Il n'ajoute aucun membre à la classe, et il ne type pas pour vous les paramètres de ses méthodes : greet(name) {} dans une classe qui implémente greet(name: string): string provoque toujours l'erreur TS7006, Parameter 'name' implicitly has an 'any' type. Une classe peut implémenter plusieurs interfaces : class A implements B, C.

Fusion de déclarations

Déclarer deux fois une interface du même nom dans la même portée fusionne les deux en une seule. C'est quelque chose que les alias de type ne savent pas faire (un second type du même nom est une erreur d'identifiant dupliqué).

interface Settings {
  theme: string;
}

interface Settings {
  fontSize: number;
}

// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };

Dans du code applicatif, c'est rarement ce que vous voulez, et une fusion accidentelle peut prêter à confusion. Son véritable usage est d'ajouter des membres à des types qui ne vous appartiennent pas : les options d'une bibliothèque, ou un global comme Window. Depuis un module, entourez la déclaration de declare global :

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}

export {};

Ensuite, window.analytics.track("signup") passe la vérification des types partout dans le projet. Les paquets de définitions de types reposent sur le même mécanisme ; voir fichiers de déclaration.

Valeurs par défaut des propriétés d'une interface

Une interface ne peut pas contenir de valeurs par défaut, car elle décrit des types et est effacée à l'exécution. size?: "sm" | "md" = "md" provoque l'erreur TS1246, An interface property cannot have an initializer. Rendez la propriété optionnelle et appliquez la valeur par défaut là où l'objet est utilisé :

Les valeurs par défaut de déstructuration sont le choix le plus sûr : elles s'appliquent chaque fois que la valeur est undefined, y compris un size: undefined explicite. La version avec étalement recopie ce undefined explicite par-dessus la valeur par défaut, et son résultat reste typé comme si size était toujours défini. Si vous voulez cette garantie au niveau des types, activez exactOptionalPropertyTypes, qui fait de size: undefined une erreur de compilation pour un size?: ... optionnel. Une classe avec des champs initialisés est l'autre option quand l'objet a aussi besoin d'un comportement.

Interfaces génériques

Une interface peut prendre des paramètres de type, ce qui permet à une seule déclaration de servir pour de nombreux types de données :

interface ApiResponse<T> {
  ok: boolean;
  data: T;
  error?: string;
}

interface Page<T> {
  items: T[];
  nextCursor?: string;
}

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

const res: ApiResponse<Page<User>> = {
  ok: true,
  data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};

ApiResponse<Page<User>> se lit « une réponse dont les données sont une page d'utilisateurs ». La bibliothèque standard en est remplie : Array<T>, Promise<T>, Map<K, V> sont toutes des interfaces génériques.

Interface ou alias de type

Un alias type peut décrire la même forme d'objet, et pour les types objet simples les deux sont interchangeables. Seule une interface peut fusionner ; seul un alias de type peut nommer une union, un tuple, ou un type mappé ou conditionnel. La règle empirique du handbook TypeScript est d'utiliser interface jusqu'à ce que vous ayez besoin d'une fonctionnalité propre à type. La page interface ou type présente la comparaison complète, y compris la différence avec Record<string, ...> qui surprend la plupart des gens.

Questions fréquentes

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

Une interface est une description nommée de la forme d'un objet : les noms de ses propriétés, leurs types, celles qui sont optionnelles ou readonly, et ses méthodes. Le compilateur vérifie que les valeurs utilisées comme cette interface ont bien cette forme. Les interfaces n'existent qu'à la compilation ; elles ne produisent aucun JavaScript.

Comment définir une valeur par défaut dans une interface TypeScript ?

Ce n'est pas possible : une interface décrit des types, pas des valeurs, donc size: "md" = ... n'est pas une syntaxe valide. Rendez la propriété optionnelle (size?: "sm" | "md") et appliquez la valeur par défaut là où l'objet est utilisé, en général avec des valeurs par défaut de déstructuration dans les paramètres de la fonction : function render({ size = "md" }: Options). Étaler un objet de valeurs par défaut ({ ...DEFAULTS, ...options }) fonctionne aussi, mais un undefined explicite dans options écrase la valeur par défaut.

Comment vérifier à l'exécution qu'un objet implémente une interface ?

Il n'existe aucun moyen intégré, car les interfaces sont effacées à la compilation : obj instanceof User provoque l'erreur TS2693 ('User' only refers to a type, but is being used as a value here). Écrivez une fonction type guard qui vérifie les propriétés, function isUser(x: unknown): x is User { ... }, ou validez avec une bibliothèque de schémas.

Une interface peut-elle étendre plusieurs interfaces ?

Oui. Listez-les après extends, séparées par des virgules : interface ServiceDog extends Pet, Trained { ... }. La nouvelle interface possède tous les membres de chaque parent, plus les siens. Si deux parents déclarent la même propriété avec des types incompatibles, la déclaration est une erreur.

Quelle est la différence entre une interface et une classe en TypeScript ?

Une classe existe à l'exécution : elle a un constructeur, des implémentations de méthodes, et new crée des objets à partir d'elle. Une interface ne fait que décrire une forme pour le compilateur et est effacée de la sortie JavaScript. Une classe peut déclarer implements SomeInterface pour que le compilateur vérifie qu'elle correspond, et n'importe quel objet simple ayant la bonne forme correspond aussi à l'interface.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER