Bonjour à tous! Je m'appelle Dmitry Novikov, je suis un développeur javascript chez Alfa Bank, et aujourd'hui je vais vous parler de notre expérience dans la dérivation du type d'action à l'aide de Typescript, quels problèmes nous avons rencontrés et comment nous les avons résolus.
Ceci est une transcription de mon rapport sur Alfa JavaScript MeetUp. Vous pouvez voir le code des diapositives de présentation
ici et l'enregistrement du mitap diffusé
ici .
Nos applications frontales fonctionnent sur un tas de React + Redux. Le flux de données Redux ressemble simplement à ceci:

Il existe des créateurs d'actions - des fonctions qui renvoient une action. Les actions tombent dans le réducteur, le réducteur crée un nouveau côté basé sur l'ancien. Les composants sont signés au parti, qui à son tour peut envoyer de nouvelles actions - et tout se répète.
Voici à quoi ressemble le créateur d'action dans le code:

Il s'agit simplement d'une fonction qui renvoie une action - un objet qui doit avoir un champ de type chaîne et des données (facultatif).
Voici à quoi ressemble un réducteur typique:

Il s'agit d'un commutateur standard qui examine le champ type d'une action et génère un nouveau côté. Dans l'exemple ci-dessus, il ajoute simplement les valeurs de propriété de l'action.
Et si nous commettions par erreur une erreur en écrivant un réducteur? Par exemple, comme ceci, nous allons échanger les propriétés de différentes actions:

Javascript ne sait rien de nos actions et considère qu'un tel code est absolument valide. Cependant, cela ne fonctionnera pas comme prévu et nous aimerions voir cette erreur. Qu'est-ce qui nous aidera si ce n'est pas Typescript? Essayons de caractériser nos actions.

Pour commencer, nous écrirons des types de «front» pour nos actions - Action1Type et Action2Type. Et puis, combinez-les en un seul type d'union à utiliser dans le réducteur. L'approche est simple et directe, mais que se passe-t-il si les données des actions changent au cours du développement de l'application? Ne modifiez pas les types manuellement à chaque fois. Nous les réécrivons comme suit:

L'opérateur typeof nous renverra le type de créateur d'action, et ReturnType nous donnera le type de la valeur de retour de la fonction - c'est-à-dire type d'action. En conséquence, cela se révélera identique à la diapositive ci-dessus, mais plus manuellement - lors de la modification des actions, les ActionTypes de type union seront mis à jour automatiquement. Ouah! On l'écrit dans le réducteur et ...

Et immédiatement, nous obtenons des erreurs du script. De plus, les erreurs ne sont pas tout à fait claires - la propriété bar est absente dans l'action foo, et foo est absent dans la barre ... Cela semble être comme ça devrait être? Quelque chose semble être foiré. En général, l'approche frontale ne fonctionne pas comme prévu.
Mais ce n'est pas le seul problème. Imaginez qu'avec le temps, notre application grandisse, et nous aurons beaucoup d'actions. Beaucoup.

À quoi ressemblerait notre type commun dans ce cas? Probablement quelque chose comme ça:

Et si nous tenons compte du fait que les actions seront ajoutées et supprimées, nous devrons prendre en charge tout cela manuellement - ajouter et supprimer des types. Cela ne nous convient pas du tout non plus. Que faire Commençons par le premier problème.

Donc, nous avons quelques créateurs d'actions, et le type commun pour eux est l'union des types d'actions dérivés automatiquement. Chaque action a une propriété type et elle est définie comme une chaîne. C'est la racine du problème. Pour distinguer une action d'une autre, nous avons besoin que chaque type soit unique et n'acceptons qu'une seule valeur unique.

Ce type est appelé littéral. Le type littéral est de trois types: numérique, chaîne et booléen.

Par exemple, nous avons le type onlyNumberOne et nous spécifions qu'une variable de ce type ne peut être égale qu'au nombre 1. Assignez 2 - et obtenez une erreur de frappe. La chaîne fonctionne de manière similaire - une seule valeur de chaîne spécifique peut être affectée à une variable. Eh bien, booléen est vrai ou faux, sans ambiguïté.
Générique
Comment enregistrer ce type sans lui permettre de se transformer en chaîne? Nous utiliserons des génériques. Le générique est une telle abstraction sur les types. Supposons que nous ayons une fonction inutile qui prend une entrée comme argument et la renvoie sans modifications. Comment puis-je le taper? Écrivez-en, car il peut s'agir de n'importe quel type? Mais si une certaine logique est présente dans la fonction, alors la conversion de type peut se produire, et, par exemple, un nombre peut se transformer en chaîne, et n'importe quelle combinaison de n'importe quelle valeur l'ignorera. Ne convient pas.

Un générique nous aidera à sortir de cette situation. L'entrée ci-dessus signifie que nous passons un argument d'un certain type T, et la fonction renverra exactement le même type T. Nous ne savons pas lequel ce sera - un nombre, une chaîne, un booléen ou autre - mais nous pouvons garantir que ce sera exactement le même type. Cette option nous convient.
Développons un peu le concept des génériques. Nous devons traiter non pas tous les types en général, mais un littéral de chaîne concret. Il existe un mot-clé d'extension pour cela:

La notation «T étend la chaîne» signifie que T est un certain type, qui est un sous-ensemble du type chaîne. Il convient de noter que cela ne fonctionne qu'avec des types primitifs - si au lieu d'utiliser une chaîne, nous utiliserions un type d'objet avec un ensemble spécifique de propriétés, cela signifierait au contraire que T est un ensemble OVER de ce type.
Vous trouverez ci-dessous des exemples d'utilisation d'une fonction typée avec des extensions et des génériques:

- Argument de type chaîne - la fonction renverra une chaîne
- Un argument de type chaîne littérale - la fonction renverra une chaîne littérale
- Si l'argument ne ressemble pas à une chaîne, par exemple un nombre ou un tableau, le script donnera une erreur.
Eh bien, et dans l'ensemble, cela fonctionne.

Nous substituons notre fonction dans le type de l'action - elle retourne exactement le même type de chaîne, mais ce n'est plus une chaîne, mais une chaîne littérale, comme il se doit. Nous collectons le type d'union, nous représentons un réducteur - tout va bien. Et si nous faisons une erreur et écrivons les mauvaises propriétés, le script de temps nous donnera non pas deux, mais une erreur logique et compréhensible:

Allons un peu plus loin et abstraits du type de chaîne. Nous allons écrire la même typification, en utilisant seulement deux génériques - T et U. Maintenant, nous avons un certain type de T qui dépendra d'un autre type de U, au lieu duquel nous pouvons utiliser n'importe quoi - au moins une chaîne, au moins un nombre, au moins un booléen. Ceci est implémenté à l'aide de la fonction wrapper:

Et enfin: le problème décrit a été suspendu pendant longtemps comme problème sur le github, et enfin, dans la version 3.4 de Typescript, les développeurs nous ont présenté une assertion solution-const. Il a deux formes d'enregistrement:

Ainsi, si vous avez un nouveau script tapé, vous pouvez simplement utiliser l'un ou l'autre comme const dans les actions, et le type littéral ne se transformera pas en chaîne. Dans les anciennes versions, vous pouvez utiliser la méthode décrite ci-dessus. Il s'avère que nous avons maintenant jusqu'à deux solutions au premier problème. Mais le second reste.

Nous avons encore beaucoup d'actions différentes, et malgré le fait que nous savons maintenant comment gérer correctement leurs types, nous ne savons toujours pas comment les assembler automatiquement. Nous pouvons écrire l'union manuellement, mais si des actions sont supprimées et ajoutées, nous devons toujours les supprimer et les ajouter manuellement dans le type. C'est faux.

Par où commencer? Supposons que des créateurs d'actions soient importés ensemble à partir d'un seul fichier. Nous aimerions les contourner un par un, déduire les types de leurs actions et les regrouper en un seul type d'union. Et surtout, nous aimerions le faire automatiquement, sans modifier manuellement les types.

Commençons par faire le tour des créateurs d'actions. Pour ce faire, il existe un type mappé spécial qui décrit les collections de valeurs-clés. Voici un exemple:

Cela crée un type pour un objet dont les clés sont option1 et option2 (à partir de l'ensemble de clés), et les valeurs sont vraies ou fausses. Dans une version plus générale, cela peut être représenté comme un type de mapOfBool - un objet avec une sorte de clés de ligne et des valeurs booléennes.
Bon. Mais comment vérifier que c'est un objet qui nous est donné en entrée, et non un autre type? Le type conditionnel, simple ternaire dans le monde des types, nous y aidera.

Dans cet exemple, nous vérifions: le type T a quelque chose en commun avec la chaîne? Si oui, retournez une chaîne et sinon, ne retournez jamais. C'est un type si spécial qui nous renverra toujours une erreur. Le littéral de chaîne satisfait la condition ternaire. Voici quelques exemples de code:

Si nous spécifions quelque chose dans les génériques qui ne ressemble pas à de la chaîne, dactylographié nous donnera une erreur.
Nous avons compris la solution de contournement et la vérification, il ne reste plus qu'à obtenir les types et à les fusionner en union. Cela nous aidera à déduire l'inférence de type en tapuscrit. Infer vit généralement dans un type conditionnel et fait quelque chose comme ceci: il parcourt toutes les paires clé-valeur, essaie d'inférer le type de valeur et le compare avec les autres. Si les types de valeurs sont différents, il les combine en une union. Exactement ce dont nous avons besoin!

Eh bien, il reste maintenant à tout mettre ensemble.
Il s'avère que cette conception:

La logique est approximativement la suivante: si T ressemble à un objet qui a des clés de chaîne (noms des créateurs d'actions), et qu'ils ont des valeurs d'un certain type (une fonction qui nous renverra l'action), alors essayez de contourner ces paires, déduisez le type de ces valeurs et réduire leur type commun. Et si quelque chose ne va pas - jetez une erreur spéciale (tapez jamais).
Ce n'est difficile qu'à première vue. En fait, tout est assez simple. Il convient de prêter attention à une fonctionnalité intéressante - car chaque action a un champ de type unique, les types de ces actions ne collent pas ensemble et nous obtenons un type d'union complet à la sortie. Voici à quoi cela ressemble dans le code:

Nous importons les créateurs d'actions en tant qu'actions, prenons leur ReturnType (le type de la valeur de retour est des actions) et collectons en utilisant notre type spécial. Il s'avère juste ce qui était nécessaire.

Quel est le résultat? Nous avons obtenu l'union des types littéraux pour toutes les actions. Lorsqu'une nouvelle action est ajoutée, le type est mis à jour automatiquement. En conséquence, nous obtenons un typage strict des actions à part entière, maintenant nous ne pouvons pas faire d'erreur. Eh bien, en cours de route, nous avons appris sur les génériques, le type conditionnel, le type mappé, jamais et en déduire - vous pouvez obtenir encore plus d'informations sur ces outils
ici .