Mr Josselin A - TypeScript









Génériques

Références

L'actualité

Librairie

L'information

Introduction

Une grande partie de l'ingénierie logicielle consiste à créer des composants dotés non seulement d'API cohérentes et bien définies, mais également réutilisables. Des composants capables de travailler sur les données d'aujourd'hui et de demain vous donneront les capacités les plus flexibles pour la création de systèmes logiciels volumineux.

Dans des langages tels que C# et Java, l'un des principaux outils de la boîte à outils permettant de créer des composants réutilisables est le générique, c'est-à-dire la possibilité de créer un composant qui peut fonctionner sur plusieurs types plutôt que sur un seul. Cela permet aux utilisateurs de consommer ces composants et d'utiliser leurs propres types.

Le monde des génériques

Commençons par le monde des génériques: la fonction d'identité. La fonction d'identité est une fonction qui renvoie tout ce qui est transmis. Vous pouvez le penser de la même manière que la commande echo.

Sans génériques, nous devrions soit donner à la fonction identité un type spécifique:

Ou, nous pourrions décrire la fonction d'identité en utilisant le type any :

Bien que any soit certainement générique en ce sens que la fonction acceptera n'importe quel type pour le type arg, nous perdons en fait les informations relatives à ce type lorsque le retour de la fonction a eu lieu. Si nous avons passé un nombre, la seule information dont nous disposons est que tout type pourrait être retourné.

Au lieu de cela, nous avons besoin d'un moyen de capturer le type de l'argument de manière à pouvoir également l'utiliser pour indiquer ce qui est renvoyé. Ici, nous allons utiliser une variable de type, un type spécial de variable qui fonctionne sur des types plutôt que sur des valeurs.

Nous avons maintenant ajouté une variable de type T à la fonction identity. Ce T nous permet de capturer le type fourni par l'utilisateur (nombre, par exemple), afin que nous puissions utiliser ces informations ultérieurement. Ici, nous utilisons à nouveau T comme type de retour. Lors de l'inspection, nous pouvons maintenant voir que le même type est utilisé pour l'argument et le type de retour. Cela nous permet de véhiculer ces informations de type d'un côté et de l'autre de la fonction.

Nous disons que cette version de la fonction identity est générique, car elle fonctionne sur plusieurs types. Contrairement à l'utilisation de any, elle est tout aussi précise (c'est-à-dire qu'elle ne perd aucune information) comme la première fonction identity qui utilisait des nombres pour l'argument et le type renvoyés.

Une fois que nous avons écrit la fonction identity générique, nous pouvons l'appeler de deux manières. La première consiste à transmettre tous les arguments, y compris l'argument type, à la fonction :

Ici, nous avons explicitement défini T comme chaîne de l'un des arguments de l'appel de la fonction, noté en utilisant <> autour des arguments plutôt que ().

La deuxième façon est peut-être aussi la plus courante. Ici, nous utilisons l'inférence d'argument de type; autrement dit, nous voulons que le compilateur définisse automatiquement la valeur de T pour nous en fonction du type de l'argument que nous transmettons :

Notez que nous n'avions pas à indiquer explicitement le type dans les crochets angulaires (<>); le compilateur a juste regardé la valeur "myString", et a défini T sur son type. Bien que l'inférence d'argument de type puisse être un outil utile pour garder le code plus court et plus lisible, vous devrez peut-être explicitement passer les arguments de type comme nous l'avons fait dans l'exemple précédent lorsque le compilateur ne parvient pas à inférer le type, comme cela peut arriver dans des exemples plus complexes.

Utilisation de variables de type génériques

Lorsque vous commencez à utiliser des génériques, vous remarquerez que lorsque vous créez des fonctions génériques telles que identity, le compilateur vous oblige à utiliser correctement tous les paramètres de type générique dans le corps de la fonction. C'est-à-dire que vous traitez réellement ces paramètres comme s'ils pouvaient être de tous types.

Prenons notre fonction identity de plus tôt :

Que faire si nous voulons également enregistrer la longueur de l'argument arg sur la console à chaque appel ? Nous pourrions être tentés d'écrire ceci :

Lorsque nous le ferons, le compilateur nous dira que nous utilisons le .length membre de arg, mais nous n’avons jamais dit que cet arg avait ce membre. Rappelez-vous, nous avons dit précédemment que ces variables de type remplacent tous les types, de sorte que quelqu'un utilisant cette fonction aurait pu passer un nombre à la place, qui ne possède pas de membre .length.

Disons que nous avons réellement prévu que cette fonction fonctionne sur des tableaux de T plutôt que directement. Puisque nous travaillons avec des tableaux, le membre .length devrait être disponible. Nous pouvons décrire cela de la même manière que nous créerions des tableaux d'autres types :

Vous pouvez lire le type de loggingIdentity comme suit : "la fonction générique loggingIdentity prend un paramètre de type T et un argument arg qui est un tableau de T et renvoie un tableau de T". Si nous passions dans un tableau de nombres, récupérer un tableau de nombres, car T serait lié au nombre. Cela nous permet d'utiliser notre variable de type générique T dans les types avec lesquels nous travaillons, plutôt que le type entier, ce qui nous donne une plus grande flexibilité.

Nous pouvons également écrire l'exemple de cette manière:

Vous êtes peut-être déjà familiarisé avec ce style de type dans d'autres languages. Dans la section suivante, nous verrons comment vous pouvez créer vos propres types génériques tels que Array ‹T›

Types génériques

Dans les sections précédentes, nous avons créé des fonctions d'identité génériques qui fonctionnaient sur plusieurs types. Dans cette section, nous allons explorer le type des fonctions elles-mêmes et comment créer des interfaces génériques.

Le type des fonctions génériques est identique à celui des fonctions non génériques, les paramètres de type étant listés en premier, de la même manière que les déclarations de fonction :

Nous aurions également pu utiliser un nom différent pour le paramètre de type générique dans le type, dans la mesure où le nombre de variables de type et la manière dont les variables de type sont utilisées sont alignés.

Nous pouvons également écrire le type générique en tant que signature d'appel d'un type d'objet littéral :

Ce qui nous amène à écrire notre première interface générique. Prenons le littéral d'objet de l'exemple précédent et déplaçons-le dans une interface :

Dans un exemple similaire, nous pouvons vouloir déplacer le paramètre générique pour qu'il soit un paramètre de toute l'interface. Cela nous permet de voir le ou les types sur lesquels nous sommes génériques (par exemple, Dictionary‹string› plutôt que simplement Dictionary). Cela rend le paramètre de type visible pour tous les autres membres de l'interface.

Notez que notre exemple a changé pour être quelque chose de légèrement différent. Au lieu de décrire une fonction générique, nous avons maintenant une signature de fonction non générique qui fait partie d'un type générique. Lorsque nous utilisons GenericIdentityFn, nous devrons également spécifier l'argument de type correspondant (ici: number), en verrouillant de manière efficace le contenu de la signature d'appel sous-jacente. Comprendre quand placer le paramètre de type directement sur la signature de l'appel et le mettre sur l'interface elle-même sera utile pour décrire les aspects d'un type qui sont génériques.

En plus des interfaces génériques, nous pouvons également créer des classes génériques. Notez qu'il n'est pas possible de créer des énumérations et des espaces de noms génériques.

Classes génériques

Une classe générique a une forme similaire à une interface générique. Les classes génériques ont une liste de paramètres de type générique entre crochets (<>) à la suite du nom de la classe.

Il s'agit d'une utilisation assez littérale de la classe GenericNumber, mais vous avez peut-être remarqué que rien ne l'empêche de n'utiliser que le type de number. Nous aurions pu utiliser un string à la place ou des objets encore plus complexes.

Comme avec interface, placer le paramètre type sur la classe elle-même nous permet de nous assurer que toutes les propriétés de la classe fonctionnent avec le même type.

Comme nous l'avons vu dans notre section sur les classes, une classe a deux côtés à son type: le côté statique et le côté instance. Les classes génériques ne sont génériques que du côté de leur instance plutôt que de leur côté statique. Par conséquent, lors de l'utilisation de classes, les membres statiques ne peuvent pas utiliser le paramètre type de la classe.

Contraintes génériques

Si vous vous rappelez d'un exemple précédent, vous souhaiterez peut-être parfois écrire une fonction générique qui fonctionne sur un ensemble de types et permettant de connaître les fonctionnalités de cet ensemble de types. Dans notre exemple loggingIdentity, nous voulions pouvoir accéder à la propriété .length de arg, mais le compilateur ne pouvait pas prouver que chaque type possédait une propriété .length. Il nous avertit donc que nous ne pouvons pas supposer cette hypothèse.

Au lieu de travailler avec tous les types, nous aimerions contraindre cette fonction à fonctionner avec tous les types ayant également la propriété .length. Tant que le type a ce membre, nous le permettrons, mais il est obligatoire d'avoir au moins ce membre. Pour ce faire, nous devons énumérer notre exigence en tant que contrainte sur ce que T peut être.

Pour ce faire, nous allons créer une interface qui décrit notre contrainte. Puis nous utiliserons cette interface et le mot-clé extends pour indiquer notre contrainte : Ici, nous allons créer une interface qui n'a qu'une propriété .length.
Comme la fonction générique est maintenant contrainte, elle ne fonctionnera plus avec aucun type :

Au lieu de cela, nous devons passer des valeurs dont le type possède toutes les propriétés requises :


Utilisation de paramètres de type dans les contraintes génériques

Vous pouvez déclarer un paramètre de type contraint par un autre paramètre de type. Par exemple, ici, nous aimerions obtenir une propriété d'un objet à partir de son nom. Nous aimerions nous assurer que nous ne récupérons pas accidentellement une propriété qui n'existe pas sur obj, nous allons donc placer une contrainte entre les deux types :


Utilisation de types de classe dans les génériques

Lors de la création d'usines dans TypeScript à l'aide de génériques, il est nécessaire de faire référence aux types de classe par leurs fonctions constructeur. Par exemple,

Un exemple plus avancé utilise la propriété prototype pour déduire et contraindre les relations entre la fonction constructeur et le côté instance des types de classe.