Mr Josselin A - TypeScript









Compatibilité des types

Références

L'actualité

Librairie

L'information

Introduction

La compatibilité des types dans TypeScript est basée sur le sous-typage structurel. Le typage structurel est une façon de relier des types uniquement en fonction de leurs membres. Ceci est en contraste avec la frappe nominale.

Considérons le code suivant :

Dans les langages nominalement typés tels que C# ou Java, le code équivalent serait une erreur car la classe Person ne se décrit pas explicitement comme un implémenteur de l'interface Named.

Le système de types structurels de TypeScript a été conçu en fonction de la manière dont le code JavaScript est généralement écrit. étant donné que JavaScript utilise largement des objets anonymes, tels que les expressions de fonction et les littéraux d'objet, il est beaucoup plus naturel de représenter les types de relations trouvées dans les bibliothèques JavaScript avec un système de types structurel au lieu d'un système nominal.


Une note sur la solidité

Le système de types de TypeScript permet de sécuriser certaines opérations impossibles à connaître au moment de la compilation. Lorsqu'un système de types a cette propriété, on dit qu'elle n'est pas "saine". Les endroits où TypeScript autorise les comportements non sains ont été soigneusement pris en compte, et tout au long de ce document, nous expliquerons leur origine et les scénarios de motivation qui s'y cachent.

Débuter

La règle de base pour le système de types structurel de TypeScript est que x est compatibilité avec y si y a au moins les mêmes membres que x.

Par exemple :

Pour vérifier si y peut être assigné à x, le compilateur vérifie chaque propriété de x pour trouver une propriété compatible correspondante dans y. Dans ce cas, y doit avoir un membre appelé name qui est une chaîne. C'est le cas, donc la cession est autorisée.

La même règle d'affectation est utilisée lors de la vérification des arguments d'appel de fonction :
Notez que y a une propriété location supplémentaire, mais cela ne crée pas d'erreur. Seuls les membres du type de cible (nommés dans ce cas) sont pris en compte lors de la vérification de la compatibilité.

Ce processus de comparaison se poursuit de manière récursive, en explorant le type de chaque membre et sous-membre.

Comparer deux fonctions

Bien que la comparaison des types primitifs et des types d'objet soit relativement simple, la question de savoir quels types de fonctions doivent être considérés comme compatibles est un peu plus complexe. Commençons par un exemple de base de deux fonctions qui ne diffèrent que par leurs listes de paramètres :

Pour vérifier si x est assignable à y, regardons d'abord la liste des paramètres. Chaque paramètre x doit avoir un paramètre correspondant y avec un type compatible. Notez que les noms des paramètres ne sont pas pris en compte, mais uniquement leurs types. Dans ce cas, chaque paramètre de x a un paramètre compatible correspondant dans y, donc l'affectation est autorisée.

La deuxième affectation est une erreur, car y un deuxième paramètre requis, x n'a pas été défini, de sorte que l'affectation est interdite.

Vous vous demandez peut-être pourquoi nous autorisons le "rejet" de paramètres, comme dans l'exemple y = x. La raison pour laquelle cette affectation est autorisée est qu'ignorer des paramètres de fonction supplémentaires est assez courant en JavaScript. Par exemple, Array#forEach fournit trois paramètres à la fonction de rappel : l'élément de tableau, son index et le tableau qui le contient. Néanmoins, il est très utile de fournir un rappel utilisant uniquement le premier paramètre :

Voyons maintenant comment les types de retour sont traités, en utilisant deux fonctions qui ne diffèrent que par leur type de retour :

Le système de types impose que le type de retour de la fonction source soit un sous-type du type de retour du type cible.


Bivariance

Lors de la comparaison des types de paramètres de fonction, l'affectation réussit si le paramètre source est assignable au paramètre cible ou inversement. Cela n'est pas judicieux car un appelant risque de se voir attribuer une fonction qui prend un type plus spécialisé, mais l'appelle avec un type moins spécialisé. En pratique, ce type d'erreur est rare et permet de nombreux modèles JavaScript courants.

Un bref exemple :



Paramètres facultatifs et paramètres de repos

Lors de la comparaison de fonctions pour la compatibilité, les paramètres facultatifs et obligatoires sont interchangeables. Les paramètres facultatifs supplémentaires du type source ne constituent pas une erreur et les paramètres facultatifs du type cible sans paramètres correspondants dans le type source ne constituent pas une erreur.

Lorsqu'une fonction a un paramètre de repos, elle est traitée comme s'il s'agissait d'une série infinie de paramètres facultatifs.

Cela n'est pas judicieux du point de vue du système de types, mais du point de vue de l'exécution, l'idée d'un paramètre facultatif n'est généralement pas bien appliquée, car le passage undefined à cette position est équivalent pour la plupart des fonctions.

L'exemple de motivation est le modèle commun d'une fonction qui prend un rappel et l'invoque avec un nombre d'arguments prévisible (pour le programmeur) mais inconnu (pour le système de types) :

Fonctions surchargées

Lorsqu'une fonction présente des surcharges, chaque surcharge du type source doit être associée à une signature compatible du type cible. Cela garantit que la fonction cible peut être appelée dans les mêmes situations que la fonction source.

Enum

Les énumérations sont compatibles avec les nombres et les nombres sont compatibles avec les énumérations. Les valeurs d'énumération de différents types d'énumération sont considérées comme incompatibles.

Par exemple :


Des classes

Les classes fonctionnent de la même manière que les types littéraux d'objet et les interfaces, à une exception près : elles ont à la fois un type statique et un type d'instance. Lors de la comparaison de deux objets d'un type de classe, seuls les membres de l'instance sont comparés. Les membres statiques et les constructeurs n'affectent pas la compatibilité.

Membres privés et protégés

Les membres privés et protégés d'une classe affectent leur compatibilité. Lorsque la compatibilité d'une instance de classe est vérifiée, si le type cible contient un membre privé, le type source doit également contenir un membre privé provenant de la même classe. De même, il en va de même pour une instance avec un membre protégé. Cela permet à une classe d'être compatible avec sa super classe, mais pas avec des classes d'une hiérarchie d'héritage différente, qui ont sinon la même forme.

Génériques

Comme TypeScript est un système de types structurel, les paramètres de type n'affectent que le type résultant lorsqu'ils sont utilisés avec le type d'un membre.
Par exemple :

Dans ce qui précède, x et y sont compatibles car leurs structures n'utilisent pas l'argument de type de manière différenciante. Changer cet exemple en ajoutant un membre Empty‹T› montre comment cela fonctionne :

De cette manière, un type générique dont les arguments de type sont spécifiés agit comme un type non générique.

Pour les types génériques pour lesquels les arguments de type ne sont pas spécifiés, la compatibilité est vérifiée en spécifiant any à la place de tous les arguments de type non spécifiés. La compatibilité des types résultants est ensuite vérifiée, comme dans le cas non générique.

Par exemple :


Sujets avancés


Sous-type vs affectation

Jusqu'à présent, nous avons utilisé le terme "compatible", qui n'est pas défini dans les spécifications du langage. Dans TypeScript, il existe deux types de compatibilité: le sous-type et l'affectation. Celles-ci ne diffèrent que par le fait que l'affectation étend la compatibilité des sous-types avec les règles afin de permettre l'affectation de any et vers et de enum avec les valeurs numériques correspondantes.

Différents endroits dans la langue utilisent l'un des deux mécanismes de compatibilité, selon la situation. Pour des raisons pratiques, la compatibilité des types est dictée par la compatibilité des affectations, même dans les cas des clauses implements et extends.