Saisie nominale dans TypeScript ou comment protéger votre interface contre les identifiants étrangers


Récemment, en étudiant les causes du mauvais fonctionnement de mon projet de maison, j'ai encore une fois remarqué une erreur qui se répète souvent à cause de la fatigue. L'essence de l'erreur est que, ayant plusieurs identifiants dans un bloc de code, lorsque j'appelle une fonction, je passe l'identifiant d'un objet d'un autre type. Dans cet article, je vais vous expliquer comment résoudre ce problème à l'aide de TypeScript.


Un peu de théorie


TypeScript est basé sur un typage structurel, qui correspond bien à l'idéologie canard de JavaScript. Un nombre suffisant d'articles ont été écrits à ce sujet. Je ne les répéterai pas, je ne ferai que souligner la principale différence avec la dactylographie nominative, qui est plus courante dans d'autres langues. Jetons un coup d'œil à un petit exemple.


class Car { id: number; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: number; move (x: number, y: number) { //   } } let car: Car = new Boat(); //  TypeScript   let boat: Boat = new Car(); //        

Pourquoi TypeScript se comporte-t-il de cette façon? Ceci n'est qu'une manifestation du typage structurel. Contrairement au nominatif, qui surveille les noms de type, le typage structurel décide de la compatibilité des types en fonction de leur contenu. La classe Car contient toutes les propriétés et méthodes de la classe Boat, donc Car peut être utilisé comme bateau. L'inverse n'est pas vrai car Boat n'a pas la propriété numberOfWheels.


Taper des identifiants


Tout d'abord, nous allons définir des types pour les identifiants


 type CarId: number; type BoatId: number; 

et réécrivez les classes en utilisant ces types.


 class Car { id: CarId; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: BoatId; move (x: number, y: number) { //   } } 

Vous remarquerez que la situation n'a pas beaucoup changé, car nous n'avons toujours pas de contrôle sur la provenance de l'identifiant, et vous aurez raison. Mais cet exemple donne déjà quelques avantages.


  1. Pendant le développement du programme, le type d'identifiant peut soudainement changer. Ainsi, par exemple, un certain numéro de voiture, unique au projet, peut être remplacé par un numéro de chaîne VIN. Sans spécifier le type d'identifiant, vous devrez remplacer le numéro par une chaîne à tous les endroits où il se produit. Avec la tâche de type, le changement devra être effectué uniquement à un endroit où le type lui-même est déterminé.


  2. Lors de l'appel de fonctions, nous obtenons des conseils de notre éditeur de code, quels devraient être les identificateurs de type. Supposons que nous ayons déclaré les fonctions suivantes:


     function getCarById(id: CarId): Car { // ... } function getBoatById(id: BoatId): Boat { // ... } 

    Ensuite, nous obtiendrons un indice de l'éditeur que nous devons transmettre non seulement un nombre, mais CarId ou BoatId.



Émuler la frappe la plus stricte


Il n'y a pas de typage nominal dans TypeScript, mais nous pouvons émuler son comportement, ce qui rend tout type unique. Pour ce faire, ajoutez une propriété unique au type. Cette astuce est mentionnée dans les articles en anglais sous le terme Branding, et voici à quoi elle ressemble:


 type BoatId = number & { _type: 'BoatId'}; type CarId = number & { _type: 'CarId'}; 

Après avoir souligné que nos types doivent être à la fois un nombre et un objet avec une propriété avec une valeur unique, nous avons rendu nos types incompatibles dans la compréhension du typage structurel. Voyons comment cela fonctionne.


 let carId: CarId; let boatId: BoatId; let car: Car; let boat: Boat; car = getCarById(carId); // OK car = getCarById(boatId); // ERROR boat = getBoatById(boatId); // OK boat = getBoatById(carId); // ERROR carId = 1; // ERROR boatId = 2; // ERROR car = getCarById(3); // ERROR boat = getBoatById(4); // ERROR 

Tout semble bon sauf pour les quatre dernières lignes. Pour créer des identifiants, vous avez besoin d'une fonction d'assistance:


 function makeCarIdFromVin(id: number): CarId { return vin as any; } 

L'inconvénient de cette méthode est que cette fonction restera en exécution.


Rendre la frappe forte un peu moins stricte


Dans le dernier exemple, j'ai dû utiliser une fonction supplémentaire pour créer l'identifiant. Vous pouvez vous en débarrasser en utilisant la définition de l'interface Flavour:


 interface Flavoring<FlavorT> { _type?: FlavorT; } export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>; 

Vous pouvez maintenant définir les types d'identifiants comme suit:


 type CarId = Flavor<number, “CarId”> type BoatId = Flavor<number, “BoatId”> 

Étant donné que la propriété _type est facultative, vous pouvez utiliser une conversion implicite:


 let boatId: BoatId = 5; // OK let carId: CarId = 3; // OK 

Et nous ne pouvons toujours pas mélanger les identifiants:


 let carId: CarId = boatId; // ERROR 

Quelle option choisir


Les deux options ont le droit d'exister. Le branding a l'avantage de protéger une variable d'une affectation directe. Ceci est utile si la variable stocke la chaîne dans un certain format, tel qu'un chemin de fichier absolu, une date ou une adresse IP. La fonction d'assistance qui traite de la conversion de type dans ce cas peut également vérifier et traiter les données d'entrée. Dans d'autres cas, il est plus pratique d'utiliser Flavour.


Les sources


  1. Point de départ stackoverflow.com
  2. Interprétation gratuite de l'article

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


All Articles