Génériques dans TypeScript: se réunir

Bonjour à tous! L' équipe TestMace publie une autre traduction de l'article du monde du développement Web. Cette fois pour les débutants! Bonne lecture.


Brisez le voile du mystère et de l'incompréhension sur la syntaxe <T> et enfin faites-vous des amis



Probablement, seuls les développeurs chevronnés de Java ou d'autres langages fortement typés ne se cognent pas les yeux lorsqu'ils voient le générique dans TypeScript. Sa syntaxe est fondamentalement différente de tout ce que nous avons l'habitude de voir en JavaScript, il n'est donc pas si facile de deviner immédiatement ce qu'il fait.


Je voudrais vous montrer qu'en fait, tout est beaucoup plus simple qu'il n'y paraît. Je prouverai que si vous êtes capable d'implémenter une fonction avec des arguments en JavaScript, alors vous pouvez utiliser des génériques sans effort supplémentaire. C'est parti!


Génériques dans TypeScript


La documentation TypeScript fournit la définition suivante: "Les génériques sont la possibilité de créer des composants qui fonctionnent non seulement avec un, mais avec plusieurs types de données."


Ouah! Donc, l'idée principale est que les génériques nous permettent de créer des composants réutilisables qui fonctionnent avec différents types de données qui leur sont transmis. Mais comment est-ce possible? Voici ce que je pense.


Les génériques et les types sont liés les uns aux autres, comme les valeurs de fonction et les arguments. C'est une telle façon de dire aux composants (fonctions, classes ou interfaces) quel type utiliser lors de leur appel, tout comme lors d'un appel, nous disons à la fonction quelles valeurs utiliser comme arguments.


Il est préférable de comprendre cela en utilisant le générique d'une fonction identique comme exemple. Une fonction identique est une fonction qui renvoie la valeur de l'argument qui lui est passé. En JavaScript, cela ressemblera à ceci:


identity.js
 function identity (value) { return value; } console.log(identity(1)) // 1 

Faisons-le fonctionner avec des chiffres:


identity.ts
 function identity (value: Number) : Number { return value; } console.log(identity(1)) // 1 

Eh bien, nous avons ajouté un type à la définition d'une fonction identique, mais nous aimerions qu'elle soit plus flexible et fonctionne pour les valeurs de tout type, et pas seulement pour les nombres. C'est à cela que servent les génériques. Ils permettent à la fonction de prendre des valeurs de tout type de données à l'entrée et, selon eux, de transformer la fonction elle-même.


genericIdentity.ts
 function identity <T>(value: T) : T { return value; } console.log(identity<Number>(1)) // 1 

Oh, cette étrange syntaxe <T> ! Arrêtez la panique. Nous passons simplement le type que nous voulons utiliser pour un appel de fonction spécifique.



Regardez l'image ci-dessus. Lorsque vous appelez identity<Number>(1) , le type de Number est le même argument que 1. Il est substitué partout pour T Une fonction peut prendre plusieurs types de la même manière qu'elle prend plusieurs arguments.



Regardez l'appel de fonction. Maintenant, la syntaxe générique ne devrait pas vous faire peur. T et U sont que les noms des variables que vous vous attribuez. Lorsqu'une fonction est appelée, les types avec lesquels cette fonction fonctionnera sont indiqués à la place .


Une autre version de la compréhension du concept des génériques est qu'ils transforment la fonction en fonction du type de données spécifié. L'animation ci-dessous montre comment l'enregistrement de fonction et le résultat renvoyé changent lorsque le type est modifié.



Comme vous pouvez le voir, la fonction accepte tout type, ce qui vous permet de créer des composants réutilisables de différents types, comme promis dans la documentation.


Faites particulièrement attention au deuxième appel à console.log dans l'animation ci-dessus - le type ne lui est pas transmis. Dans ce cas, TypeScript essaiera de calculer le type à partir des données transmises.


Classes génériques et interfaces


Vous savez déjà que les génériques ne sont qu'un moyen de passer des types à un composant. Vous venez de voir comment ils fonctionnent avec les fonctions, et j'ai une bonne nouvelle: ils fonctionnent de la même manière avec les classes et les interfaces. Dans ce cas, l'indication de type suit le nom de l'interface ou de la classe.


Regardez un exemple et essayez de le découvrir par vous-même. J'espère que vous avez réussi.


genericClass.ts
 interface GenericInterface<U> { value: U getIdentity: () => U } class IdentityClass<T> implements GenericInterface<T> { value: T constructor(value: T) { this.value = value } getIdentity () : T { return this.value } } const myNumberClass = new IdentityClass<Number>(1) console.log(myNumberClass.getIdentity()) // 1 const myStringClass = new IdentityClass<string>("Hello!") console.log(myStringClass.getIdentity()) // Hello! 

Si le code n'est pas immédiatement compris, essayez de tracer les valeurs de type de haut en bas jusqu'aux appels de fonction. La procédure est la suivante:


  1. Une nouvelle instance de la classe IdentityClass est créée et le type Number et la valeur 1 sont transmis.
  2. Dans la classe, la valeur de T affectée au type Number .
  3. IdentityClass implémente GenericInterface<T> , et nous savons que T est Number , et un tel enregistrement est équivalent à l' GenericInterface<Number> .
  4. Dans GenericInterface U générique devient Number . Dans cet exemple, j'ai intentionnellement utilisé différents noms de variable pour montrer que la valeur de type remonte la chaîne et que le nom de variable n'a aucune signification.

Cas d'utilisation réels: aller au-delà des types primitifs


Dans toutes les insertions de code ci-dessus, des types primitifs tels que Number et string ont été utilisés. Par exemple, c'est le plus, mais en pratique, il est peu probable que vous utilisiez des génériques pour les types primitifs. Les génériques seront vraiment utiles lorsque vous travaillez avec des types ou des classes arbitraires qui forment un arbre d'héritage.


Prenons un exemple classique d'héritage. Disons que nous avons une classe Car , qui est la base des classes Truck et Vespa . Nous écrivons la fonction utilitaire washCar, qui prend une instance générique de Car et la renvoie.


car.ts
 class Car { label: string = 'Generic Car' numWheels: Number = 4 horn() { return "beep beep!" } } class Truck extends Car { label = 'Truck' numWheels = 18 } class Vespa extends Car { label = 'Vespa' numWheels = 2 } function washCar <T extends Car> (car: T) : T { console.log(`Received a ${car.label} in the car wash.`) console.log(`Cleaning all ${car.numWheels} tires.`) console.log('Beeping horn -', car.horn()) console.log('Returning your car now') return car } const myVespa = new Vespa() washCar<Vespa>(myVespa) const myTruck = new Truck() washCar<Truck>(myTruck) 

En washCar fonction washCar que T extends Car , nous indiquons quelles fonctions et propriétés nous pouvons utiliser à l'intérieur de cette fonction. Generic vous permet également de renvoyer des données du type spécifié au lieu de la Car habituelle.


Le résultat de ce code sera:


 Received a Vespa in the car wash. Cleaning all 2 tires. Beeping horn - beep beep! Returning your car now Received a Truck in the car wash. Cleaning all 18 tires. Beeping horn - beep beep! Returning your car now 

Pour résumer


J'espère que je vous ai aidé à gérer les génériques. Rappelez-vous, tout ce que vous avez à faire est de simplement passer la valeur de type à la fonction :)


Si vous souhaitez en savoir plus sur les génériques, j'ai joint quelques liens ci-dessous.


Que lire :


Source: https://habr.com/ru/post/fr455473/


All Articles