Interfaces

Références

L'actualité

Librairie

L'information

Introduction

Un des principes de base de TypeScript est que la vérification de type est centrée sur la forme des valeurs. Ceci est parfois appelé "typage de canard" ou "sous-typage structurel". Dans TypeScript, les interfaces remplissent le rôle de nommer ces types et constituent un moyen puissant de définir des contrats dans votre code ainsi que des contrats avec du code situé en dehors de votre projet.

Première interface

Le moyen le plus simple de voir le fonctionnement des interfaces est de commencer par un exemple simple :

Le vérificateur de type vérifie l'appel à printLabel. La fonction printLabel a un paramètre unique qui nécessite que l'objet transmis possède une propriété appelée label de type chaîne. Notez que notre objet a en réalité plus de propriétés que cela, mais le compilateur vérifie seulement qu'au moins celles requises sont présentes et correspondent aux types requis. Il existe des cas où TypeScript n'est pas aussi clément, et nous en parlerons.

Nous pouvons écrire le même exemple à nouveau, cette fois en utilisant une interface pour décrire l'exigence d'avoir la propriété label qui est une chaîne:

L'interface LabelledValue est un nom que nous pouvons maintenant utiliser pour décrire l'exigence de l'exemple précédent. Cela représente toujours d'avoir une seule propriété appelée label de type chaîne. Notez que nous n'avions pas besoin de dire explicitement que l'objet que nous passons à printLabel implémente cette interface comme nous pourrions le faire dans d'autres languages. Ici, seule la forme compte. Si l'objet que nous transmettons à la fonction remplit les conditions énumérées, il est autorisé.

Il convient de noter que le vérificateur de type ne nécessite pas que ces propriétés soient classées dans un ordre quelconque, mais uniquement que les propriétés requises par l'interface sont présentes et possèdent le type requis.

Propriétés facultatives

Toutes les propriétés d'une interface peuvent ne pas être requises. Certaines existent sous certaines conditions ou peuvent ne pas en être du tout. Ces propriétés facultatives sont populaires lors de la création de modèles tels que des "options", dans lesquelles vous transmettez un à objet une fonction qui ne contient que quelques propriétés.

Voici un exemple de ce modèle:

Les interfaces avec des propriétés facultatives sont écrites de la même manière que les autres interfaces, chaque propriété facultative étant désignée par le symbole ? à la fin du nom de la propriété dans la déclaration.

L'avantage des propriétés facultatives est que vous pouvez décrire ces propriétés éventuellement disponibles tout en empêchant l'utilisation de propriétés ne faisant pas partie de l'interface. Par exemple, si nous avions mal entré le nom de la propriété color dans createSquare, nous aurions reçu un message d'erreur nous informant :


Propriétés en lecture seule

Certaines propriétés ne devraient être modifiables que lorsqu'un objet est créé pour la première fois. Vous pouvez spécifier ceci en plaçant readonly avant le nom de la propriété :

Vous pouvez construire un point en assignant un littéral d'objet. Après l'affectation, x et y ne peuvent plus être modifiés.

TypeScript est fourni avec un type ReadonlyArray‹T› identique à Array‹T› avec toutes les méthodes de mutation supprimées. Vous pouvez ainsi vous assurer que vous ne modifiez pas vos tableaux après la création :

Sur la dernière ligne de l'extrait de code, vous pouvez constater que même réattribuer l'intégralité de ReadonlyArray à un tableau normal est illégal. Vous pouvez toujours le remplacer par une assertion de type, cependant :

readonly vs const

Le moyen le plus simple de se rappeler d'utiliser readonly ou const est de demander si vous l'utilisez sur une variable ou une propriété. Les variables utilisent const alors que les propriétés utilisent en lecture seule.

Excès de contrôles de propriété

Dans notre premier exemple utilisant des interfaces, TypeScript nous permet de passer {size: number; label: string;} à quelque chose qui n'attendait qu'un {label: string;}. Nous venons également d'apprendre les propriétés facultatives et leur utilité pour décrire ce que l'on appelle des "poches d'options".

Cependant, combiner les deux naïvement vous permettrait de vous tirer dans le pied de la même manière que vous le feriez en JavaScript. Par exemple, prenons notre dernier exemple en utilisant createSquare :

Notez que l'argument donné à createSquare est orthographié colour plutôt que color. En clair, ce genre de chose échoue en silence.

Vous pourriez faire valoir que ce programme est correctement typé, puisque les propriétés width sont compatibles, qu'il n'ya pas de propriété color, et que la propriété extra colour est non significative.

Cependant, TypeScript considère qu'il existe probablement un bogue dans ce code. Les littéraux d'objet reçoivent un traitement spécial et sont soumis à une vérification de propriété excessive lors de leur affectation à d'autres variables ou de leur transmission en tant qu'arguments. Si un littéral d'objet a des propriétés que le "type de cible" n'a pas, vous obtiendrez une erreur.

Contourner ces contrôles est en réalité très simple. La méthode la plus simple consiste simplement à utiliser une assertion de type :

Cependant, une meilleure approche pourrait être d'ajouter une signature d'index de chaîne si vous êtes certain que l'objet peut avoir des propriétés supplémentaires qui sont utilisées de manière spéciale. Si SquareConfig peut avoir des propriétés de color et de width avec les types ci-dessus, mais peut également avoir un nombre quelconque d'autres propriétés, nous pourrions le définir comme suit :

Nous discuterons un peu des signatures d'index, mais nous disons ici que SquareConfig peut avoir un nombre quelconque de propriétés, et tant qu'elles ne sont pas de color ou de width, leur type importe peu.

Une dernière façon de contourner ces vérifications, ce qui peut paraître un peu surprenant, consiste à affecter l'objet à une autre variable : puisque squareOptions ne subira pas de vérifications de propriétés excessives, le compilateur ne vous indiquera pas d'erreur.

N'oubliez pas que pour un code simple comme ci-dessus, vous ne devriez probablement pas essayer de "contourner" ces vérifications. Pour les littéraux d'objet plus complexes qui ont des méthodes et un état de maintien, vous devrez peut-être garder ces techniques à l'esprit, mais la majorité des erreurs de propriété en excès sont en fait des bogues. Cela signifie que si vous rencontrez des problèmes de vérification de propriétés excessives pour des objets tels que des options, vous devrez peut-être réviser certaines de vos déclarations de type. Dans ce cas, s'il est correct de passer un objet avec une color ou une propriété de colour à createSquare, vous devez corriger la définition de SquareConfig afin de refléter cela.

Types de fonction

Les interfaces sont capables de décrire le large éventail de formes que peuvent prendre les objets JavaScript. En plus de décrire un objet avec des propriétés, les interfaces sont également capables de décrire les types de fonction.

Pour décrire un type de fonction avec une interface, on donne à cette interface une signature d'appel. Cela ressemble à une déclaration de fonction avec uniquement la liste de paramètres et le type de retour donnés. Chaque paramètre de la liste de paramètres requiert à la fois un nom et un type.

Une fois défini, nous pouvons utiliser cette interface de type fonction comme nous le ferions avec d'autres interfaces. Nous montrons ici comment créer une variable d'un type de fonction et lui attribuer une valeur de fonction du même type.

Pour que les types de fonction vérifient correctement le type, les noms des paramètres ne doivent pas nécessairement correspondre. Nous aurions pu, par exemple, écrire l'exemple ci-dessus comme ceci :

Les paramètres de fonction sont vérifiés un à un, le type correspondant à chaque position de paramètre étant vérifié. Si vous ne souhaitez pas du tout spécifier de types, le typage contextuel de TypeScript peut en déduire les types d'argument puisque la valeur de la fonction est directement affectée à une variable de type SearchFunc. Ici aussi, le type de retour de notre expression de fonction est impliqué par les valeurs renvoyées (ici false et true). Si l'expression de la fonction avait renvoyé des nombres ou des chaînes, le vérificateur de type nous aurait averti que le type de retour ne correspond pas au type de retour décrit dans l'interface SearchFunc.


Types indexables

De la même manière que nous pouvons utiliser des interfaces pour décrire des types de fonction, nous pouvons également décrire des types dans lesquels nous pouvons "indexer" comme a[10] ou ageMap["daniel"]. Les types indexables ont une signature d'index qui décrit les types que nous pouvons utiliser pour indexer dans l'objet, ainsi que les types de retour correspondants lors de l'indexation. Prenons un exemple :

Ci-dessus, nous avons une interface StringArray qui a une signature d'index. Cette signature d'index indique que lorsqu'un StringArray est indexé avec un number, il renvoie un string.

Il existe deux types de signatures d'index pris en charge : string et number. Il est possible de prendre en charge les deux types d'indexeur, mais le type renvoyé par un indexeur numérique doit être un sous-type du type renvoyé par l'indexeur de chaîne. En effet, lors de l'indexation avec un nombre, JavaScript le convertira en chaîne avant l'indexation en un objet. Cela signifie que l'indexation avec 100 (est un number) est identique à l'indexation avec "100" (est un string), de sorte que les deux doivent être cohérents.

Bien que les signatures d'index de chaîne soient un moyen puissant de décrire le modèle "dictionnaire", elles imposent également que toutes les propriétés correspondent à leur type de retour. En effet, un index de chaîne déclare qu'un obj.property est également disponible en tant que obj["property"]. Dans l'exemple suivant, le type name ne correspond pas au type de l'index de chaîne et le vérificateur de type génère une erreur :

Enfin, vous pouvez créer des signatures d'index en lecture seule afin d'empêcher l'affectation à leurs index :

Vous ne pouvez pas définir myArray[2] car la signature d'index est en lecture seule.

Types de classe


Mettre en place une interface

Une des utilisations les plus courantes des interfaces dans des langages tels que C# et Java, celui de forcer explicitement une classe à respecter un contrat particulier, est également possible dans TypeScript.

Vous pouvez également décrire les méthodes d'une interface implémentées dans la classe, comme nous le faisons avec setTime dans l'exemple ci-dessous:

Les interfaces décrivent le côté public de la classe, plutôt que le côté public et le côté privé. Cela vous interdit de les utiliser pour vérifier qu'une classe a également des types particuliers pour le côté privé de l'instance de classe.

Différence entre les côtés statique et instance des classes

Lorsque vous travaillez avec des classes et des interfaces, gardez à l'esprit qu'une classe a deux types : le type du côté statique et le type du côté d'instance. Vous remarquerez peut-être que si vous créez une interface avec une signature de construction et essayez de créer une classe qui implémente cette interface, vous obtenez une erreur :

En effet, lorsqu'une classe implémente une interface, seul le côté instance de la classe est vérifié. étant donné que le constructeur est situé du côté statique, il n'est pas inclus dans cette vérification.

Au lieu de cela, vous devrez travailler directement avec le côté statique de la classe. Dans cet exemple, nous définissons deux interfaces, ClockConstructor pour le constructeur et ClockInterface pour les méthodes d'instance. Ensuite, pour plus de commodité, nous définissons une fonction constructeur createClock qui crée des instances du type qui lui est transmis.

Comme le premier paramètre de createClock est de type ClockConstructor, dans createClock(AnalogClock, 7, 32), il vérifie que AnalogClock dispose de la signature de constructeur correcte.

Étendre les interfaces

Comme les classes, les interfaces peuvent s'étendre mutuellement. Cela vous permet de copier les membres d'une interface dans une autre, ce qui vous donne plus de flexibilité pour séparer vos interfaces en composants réutilisables.

Une interface peut étendre plusieurs interfaces, créant une combinaison de toutes les interfaces.


Types hybrides

Comme nous l'avons mentionné précédemment, les interfaces peuvent décrire les types riches présents dans le monde réel JavaScript. En raison de la nature dynamique et flexible de JavaScript, vous pouvez parfois rencontrer un objet qui fonctionne comme une combinaison de certains des types décrits ci-dessus.

Un tel exemple est un objet qui agit à la fois comme une fonction et un objet, avec des propriétés supplémentaires :

Lors de l'interaction avec du code JavaScript tiers, vous devrez peut-être utiliser des modèles tels que ceux décrits ci-dessus pour décrire complètement la forme du type.

Interfaces étendant des classes

Lorsqu'un type d'interface étend un type de classe, il hérite des membres de la classe mais pas de leurs implémentations. C'est comme si l'interface avait déclaré tous les membres de la classe sans fournir d'implémentation. Les interfaces héritent même des membres privés et protégés d'une classe de base. Cela signifie que lorsque vous créez une interface qui étend une classe avec des membres privés ou protégés, ce type d'interface ne peut être implémenté que par cette classe ou l'une de ses sous-classes.

Cela est utile lorsque vous avez une grande hiérarchie d'héritage, mais que vous souhaitez spécifier que votre code fonctionne uniquement avec des sous-classes possédant certaines propriétés. Les sous-classes ne doivent pas obligatoirement être liées, à part hériter de la classe de base. Par exemple :

Dans l'exemple ci-dessus, SelectableControl contient tous les membres de Control, y compris la propriété private state. étant donné que state est un membre privé, seuls les descendants de Control peuvent implémenter SelectableControl. En effet, seuls les descendants de Control auront un membre privé d'état issu de la même déclaration, ce qui oblige les membres privés à être compatibles.

Dans la classe Control, il est possible d'accéder au membre privé d'état via une instance de SelectableControl. Effectivement, un SelectableControl agit comme un contrôle connu pour avoir une méthode de sélection. Les classes Button et TextBox sont des sous-types de SelectableControl (car ils héritent tous deux de Control et ont une méthode select), mais pas les classes Image et Location.