École de magie TypeScript: génériques et extension de type

L'auteur de l'article que nous traduisons aujourd'hui dit que TypeScript est génial. Lorsqu'il a commencé à utiliser TS, il aimait vraiment la liberté inhérente à ce langage. Plus un programmeur met d'effort dans son travail avec des mécanismes spécifiques à TS, plus les avantages qu'il reçoit sont importants. Il n'a ensuite utilisé les annotations de type que périodiquement. Parfois, il utilisait les opportunités de complétion de code et les conseils du compilateur, mais ne comptait principalement que sur sa propre vision des tâches qu'il avait résolues.

Au fil du temps, l'auteur de ce matériel s'est rendu compte que chaque fois qu'il contourne les erreurs détectées au stade de la compilation, il met une bombe à retardement dans son code qui peut exploser pendant l'exécution du programme. Chaque fois qu'il «se débattait» avec des erreurs en utilisant une construction as any simple as any , il devait payer pour cela avec de nombreuses heures de débogage difficile.



En conséquence, il a conclu qu'il valait mieux ne pas le faire. Il s'est lié d'amitié avec le compilateur, a commencé à prêter attention à ses indices. Le compilateur trouve des problèmes dans le code et les signale bien avant qu'ils ne causent de réels dommages. L'auteur de l'article, se considérant comme un développeur, s'est rendu compte que le compilateur est son meilleur ami, car il le protège de lui-même. Comment ne pas se souvenir des paroles d'Albus Dumbledore: "Il faut beaucoup de courage pour dénoncer vos ennemis, mais rien de moins que cela est nécessaire pour dénoncer vos amis."

Peu importe la qualité du compilateur, il n'est pas toujours facile de plaire. Il est parfois très difficile d'éviter l'utilisation de any type. Et parfois, il semble que any soit la seule solution raisonnable à un problème.

Ce matériel se concentre sur deux situations. En évitant l'utilisation de any type en eux, vous pouvez assurer la sécurité du type du code, ouvrir les possibilités de sa réutilisation et le rendre intuitif.

Génériques


Supposons que nous travaillons sur une base de données d'une école. Nous avons écrit une fonction d'aide très pratique getBy . Afin d'obtenir l'objet représentant l'élève par son nom, nous pouvons utiliser une commande de la forme getBy(model, "name", "Harry") . Jetons un œil à l'implémentation de ce mécanisme (ici, pour ne pas compliquer le code, la base de données est représentée par un tableau ordinaire).

 type Student = { name: string; age: number; hasScar: boolean; }; const students: Student[] = [ { name: "Harry", age: 17, hasScar: true }, { name: "Ron", age: 17, hasScar: false }, { name: "Hermione", age: 16, hasScar: false } ]; function getBy(model, prop, value) {   return model.filter(item => item[prop] === value)[0] } 

Comme vous pouvez le voir, nous avons une bonne fonction, mais elle n'utilise pas d'annotations de type, et leur absence signifie également qu'une telle fonction ne peut pas être appelée type-safe. Réparez-le.

 function getBy(model: Student[], prop: string, value): Student | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "name", "Hermione") // result: Student 

Notre fonction est donc déjà bien meilleure. Le compilateur connaît désormais le type de résultat attendu, cela vous sera utile plus tard. Cependant, afin de réaliser un travail sûr avec les types, nous avons sacrifié les possibilités de réutilisation de la fonction. Et si jamais nous devions l'utiliser pour obtenir d'autres entités? Il est impossible que cette fonction ne puisse être améliorée d'aucune façon. Et ça l'est vraiment.

Dans TypeScript, comme dans d'autres langages fortement typés, nous pouvons utiliser des génériques, également appelés "types génériques", "types universels", "généralisations".

Un générique est similaire à une variable régulière, mais au lieu d'une certaine valeur, il contient une définition de type. Nous réécrivons le code de notre fonction pour qu'au lieu du type Student il utilise le type universel T

 function getBy<T>(model: T[], prop: string, value): T | null {   return model.filter(item => item[prop] === value)[0] } const result = getBy<Student>(students, "name", "Hermione") // result: Student 

La beauté! Maintenant, la fonction est idéale pour la réutilisation, tandis que la sécurité de type est toujours de notre côté. Notez comment le type Student est explicitement défini dans la dernière ligne de l'extrait de code ci-dessus où le T générique T . Ceci est fait afin de rendre l'exemple aussi clair que possible, mais le compilateur, en fait, peut dériver indépendamment le type nécessaire, donc dans les exemples suivants nous ne ferons pas de tels raffinements de type.

Nous avons donc maintenant une fonction d'assistance fiable pouvant être réutilisée. Cependant, il peut encore être amélioré. Que se passe-t-il si une erreur est commise lors de la saisie du deuxième paramètre et qu'au lieu de "name" il semble y avoir "naem" ? La fonction se comportera comme si l'étudiant que vous recherchez ne se trouve tout simplement pas dans la base de données et, ce qui est très désagréable, il ne produira aucune erreur. Cela peut entraîner un débogage à long terme.

Afin de se protéger contre de telles erreurs, nous introduisons un autre type universel, P Dans ce cas, il est nécessaire que P soit une clé de type T , donc, si Student utilisé ici, alors il est nécessaire que P soit la chaîne "name" , "age" ou "hasScar" . Voici comment procéder.

 function getBy<T, P extends keyof T>(model: T[], prop: P, value): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "naem", "Hermione") // Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "age" | "hasScar"'. 

L'utilisation de génériques et du keyof est une astuce très puissante. Si vous écrivez des programmes dans un IDE qui prend en charge TypeScript, puis en entrant des arguments, vous pouvez profiter des capacités de saisie semi-automatique, ce qui est très pratique.

Cependant, nous n'avons pas encore fini de travailler sur la fonction getBy . Elle a un troisième argument, dont nous n'avons pas encore défini le type. Cela ne nous convient pas du tout. Jusqu'à présent, nous ne pouvions pas savoir à l'avance quel type il devait être, car cela dépend de ce que nous passons comme deuxième argument. Mais maintenant, puisque nous avons le type P , nous pouvons inférer dynamiquement le type du troisième argument. Le type du troisième argument sera finalement T[P] . Par conséquent, si T est Student et P est "age" , alors T[P] sera de type number .

 function getBy<T, P extends keyof T>(model: T[], prop: P, value: T[P]): T | null {   return model.filter(item => item[prop] === value)[0] || null } const result = getBy(students, "age", "17") // Error: Argument of type '"17"' is not assignable to parameter of type 'number'. const anotherResult = getBy(students, "hasScar", "true") // Error: Argument of type '"true"' is not assignable to parameter of type 'boolean'. const yetAnotherResult = getBy(students, "name", "Harry") //      

J'espère que vous comprenez maintenant parfaitement comment utiliser les génériques dans TypeScript, mais si vous voulez expérimenter très bien avec tout ce que vous voulez expérimenter avec le code discuté ici, vous pouvez jeter un œil ici .

Extension des types existants


Parfois, nous pouvons rencontrer le besoin d'ajouter des données ou des fonctionnalités aux interfaces dont nous ne pouvons pas changer le code. Vous devrez peut-être modifier l'objet standard, par exemple - ajouter une propriété à l'objet window ou étendre le comportement d'une bibliothèque externe comme Express . Et dans les deux cas, vous n'avez pas la possibilité d'affecter directement l'objet avec lequel vous souhaitez travailler.

Nous chercherons une solution à ce problème en ajoutant la fonction getBy vous connaissez déjà au prototype Array . Cela nous permettra, à l'aide de cette fonction, de construire des constructions syntaxiques plus précises. Pour le moment, nous ne parlons pas de savoir s'il est bon ou mauvais d'étendre des objets standard, car notre objectif principal est d'étudier l'approche considérée.

Si nous essayons d'ajouter une fonction au prototype Array , le compilateur n'aimera pas beaucoup cela:

 Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; // Error: Property 'getBy' does not exist on type 'any[]'. const bestie = students.getBy("name", "Ron"); // Error: Property 'getBy' does not exist on type 'Student[]'. const potionsTeacher = (teachers as any).getBy("subject", "Potions") //  ...   ? 

Si nous essayons de rassurer le compilateur en utilisant périodiquement le as any construction, nous annulerons tout ce que nous avons accompli. Le compilateur sera silencieux, mais vous pouvez oublier de travailler en toute sécurité avec les types.

Il serait préférable d'étendre le type Array , mais avant de le faire, parlons de la façon dont TypeScript gère les situations lorsque deux interfaces du même type sont présentes dans le code. Ici, un schéma d'action simple est appliqué. Les publicités seront, si possible, combinées. Si vous ne pouvez pas les combiner, le système donnera une erreur.

Donc, ce code fonctionne:

 interface Wand { length: number } interface Wand {   core: string } const myWand: Wand = { length: 11, core: "phoenix feather" } //  ! 

Et celui-ci n'est pas:

 interface Wand { length: number } interface Wand {   length: string } // Error: Subsequent property declarations must have the same type.  Property 'length' must be of type 'number', but here has type 'string'. 

Maintenant, après avoir réglé cela, nous voyons que nous sommes confrontés à une tâche assez simple. À savoir, tout ce que nous devons faire est de déclarer l'interface Array<T> et d'y ajouter la fonction getBy .

 interface Array<T> {  getBy<P extends keyof T>(prop: P, value: T[P]): T | null; } Array.prototype.getBy = function <T, P extends keyof T>(   this: T[],   prop: P,   value: T[P] ): T | null { return this.filter(item => item[prop] === value)[0] || null; }; const bestie = students.getBy("name", "Ron"); //   ! const potionsTeacher = (teachers as any).getBy("subject", "Potions") //     

Veuillez noter que la plupart du code que vous êtes susceptible d'écrire dans les fichiers de module, par conséquent, pour apporter des modifications à l'interface Array , vous aurez besoin d'accéder à la portée globale. Vous pouvez le faire en plaçant la définition de type dans declare global . Par exemple, comme ceci:

 declare global {   interface Array<T> {       getBy<P extends keyof T>(prop: P, value: T[P]): T | null;   } } 

Si vous souhaitez étendre l'interface d'une bibliothèque externe, vous aurez très probablement besoin d'accéder à l' namespace cette bibliothèque. Voici un exemple montrant comment ajouter le champ userId à Request depuis la bibliothèque Express :

 declare global { namespace Express {   interface Request {     userId: string;   } } } 

Vous pouvez expérimenter avec le code de cette section ici .

Résumé


Dans cet article, nous avons examiné les techniques d'utilisation des génériques et des extensions de type dans TypeScript. Nous espérons que ce que vous avez appris aujourd'hui vous aidera à écrire du code fiable, compréhensible et sûr.

Chers lecteurs! Que pensez-vous de tout type dans TypeScript?

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


All Articles