Transférez 30 000 lignes de code de Flow vers TypeScript

Nous avons récemment déplacé 30 000 lignes de code JavaScript de notre système MemSQL Studio de Flow vers TypeScript. Dans cet article, je vais expliquer pourquoi nous avons porté la base de code, comment cela s'est produit et ce qui s'est passé.

Avertissement: Mon objectif n'est pas de critiquer Flow du tout. J'admire le projet et je pense qu'il y a assez de place dans la communauté JavaScript pour les deux options de vérification de type. Au final, chacun choisira ce qui lui convient le mieux. J'espère sincèrement que l'article vous aidera dans ce choix.

Je vais d'abord vous mettre à jour. Chez MemSQL, nous sommes de grands fans de la frappe JavaScript statique et forte pour éviter les problèmes courants de frappe dynamique et faible.

Discours sur les problèmes courants:

  1. Erreurs de type lors de l'exécution du fait que les différentes parties du code ne correspondent pas aux types implicites.
  2. Trop de temps est consacré à l'écriture de tests pour des choses aussi triviales que la vérification des paramètres de type (la vérification à l'exécution augmente également la taille du package).
  3. Il y a un manque d'intégration éditeur / IDE, car sans typage statique, il est beaucoup plus difficile d'implémenter la fonction Jump to Definition, le refactoring mécanique et d'autres fonctions.
  4. Il n'y a aucun moyen d'écrire du code autour des modèles de données, c'est-à-dire d'abord de concevoir des types de données, puis le code «s'auto-écrit».

Ce ne sont que quelques-uns des avantages du typage statique, énumérés plus loin dans un récent article sur Flow .

Début 2016, nous avons implémenté tcomb pour implémenter un certain type de sécurité lors de l'exécution de l'un de nos projets JavaScript internes (avertissement: je n'étais pas impliqué dans ce projet). Bien que la vérification de l'exécution soit parfois utile, elle n'offre même pas tous les avantages de la saisie statique (la combinaison de la saisie statique et de la vérification de type dans l'exécution peut convenir dans certains cas, io-ts vous permet de le faire avec tcomb et TypeScript, bien que je n'aie jamais essayé ) Comprenant cela, nous avons décidé de mettre en œuvre Flow pour un autre projet que nous avons commencé en 2016. À cette époque, Flow semblait être un excellent choix:

  • Le soutien de Facebook, qui a fait un travail incroyable en développant React et en développant la communauté (ils ont également développé React with Flow).
  • À peu près le même écosystème de développement JavaScript. C'était effrayant d'abandonner Babel pour tsc (le compilateur TypeScript) car nous avions perdu la flexibilité de passer à une autre vérification de type (évidemment, la situation a changé depuis lors).
  • Pas besoin de taper l'intégralité de la base de code (nous voulions avoir une idée de JavaScript typé statiquement avant de faire tapis), mais seulement une partie des fichiers. Veuillez noter que Flow et TypeScript le permettent désormais.
  • TypeScript (à l'époque) manquait certaines des fonctions de base qui sont maintenant disponibles, ce sont les types de recherche , les paramètres par défaut pour les types génériques , etc.

Lorsque nous avons commencé à travailler sur MemSQL Studio fin 2017, nous allions couvrir les types de l'application entière (elle est entièrement écrite en JavaScript: le frontend et le backend sont exécutés dans le navigateur). Nous avons pris Flow comme un outil que nous avons utilisé avec succès dans le passé.

Mais mon attention a été attirée sur Babel 7 avec prise en charge de TypeScript . Cette version signifiait que le passage à TypeScript ne nécessitait plus de transition vers l'ensemble de l'écosystème TypeScript, et vous pouviez continuer à utiliser Babel pour JavaScript. Plus important encore, nous pourrions utiliser TypeScript uniquement pour la vérification de type , et non comme un «langage» à part entière.

Personnellement, je crois que séparer la vérification de type du générateur de code est une manière plus élégante de taper statique (et fort) en JavaScript, car:

  1. Nous partageons les problèmes de code et de frappe. Cela réduit les arrêts de vérification de type et accélère le développement: si pour une raison quelconque, la vérification de type est lente, le code sera toujours généré correctement (si vous utilisez tsc avec Babel, vous pouvez le configurer pour qu'il en fasse de même).
  2. Babel possède d'excellents plugins et fonctionnalités que le générateur TypeScript ne possède pas. Par exemple, Babel vous permet de spécifier les navigateurs pris en charge et leur émettra automatiquement du code. C'est une fonction très complexe et cela n'a aucun sens de la soutenir dans deux projets différents en même temps.
  3. J'aime JavaScript comme langage de programmation (sauf pour le manque de typage statique), et je n'ai aucune idée de la quantité de TypeScript qui existera, alors que je crois en ECMAScript depuis de nombreuses années. Par conséquent, je préfère écrire et "penser" en JavaScript (notez que je dis "utiliser Flow" ou "utiliser TypeScript" au lieu de "écrire dans Flow" ou "TypeScript", car je les représente toujours avec des outils, pas des langages de programmation).

Bien sûr, cette approche présente certains inconvénients:

  1. Le compilateur TypeScript peut théoriquement effectuer des optimisations basées sur le type, mais ici nous perdons cette opportunité.
  2. La configuration du projet est un peu plus compliquée avec une augmentation du nombre d'outils et de dépendances. Je pense que c'est un argument relativement faible: un groupe de Babel et Flow ne nous ont jamais laissé tomber.

TypeScript comme alternative à Flow


J'ai remarqué un intérêt croissant pour TypeScript dans la communauté JavaScript: à la fois en ligne et parmi les développeurs qui l'entourent. Par conséquent, dès que j'ai découvert que Babel 7 prend en charge TypeScript, j'ai immédiatement commencé à étudier les options de transition potentielles. De plus, nous avons rencontré certains des inconvénients de Flow:

  1. Qualité inférieure de l'intégration éditeur / IDE (par rapport à TypeScript). Nuclide, l'IDE de Facebook avec la meilleure intégration, est déjà obsolète.
  2. Une communauté plus petite, ce qui signifie moins de définitions de types pour différentes bibliothèques, et elles sont de qualité inférieure (actuellement le référentiel DefinitelyTyped a 19 682 étoiles GitHub, et le référentiel de type flux n'en a que 3070).
  3. Absence de plan de développement public et mauvaise interaction entre l'équipe Flow sur Facebook et la communauté. Vous pouvez lire ce commentaire d'un employé de Facebook pour comprendre la situation.
  4. Consommation de mémoire élevée et fuites fréquentes - pour certains de nos développeurs, Flow prenait parfois près de 10 Go de RAM.

Bien sûr, vous devez étudier comment TypeScript nous convient. C'est une question très complexe: étudier le sujet comprenait une lecture approfondie de la documentation, ce qui a permis de comprendre que pour chaque fonction Flow, il existe un équivalent à TypeScript. Ensuite, j'ai exploré le plan de développement public TypeScript, et j'ai vraiment aimé les fonctionnalités qui sont prévues pour l'avenir (par exemple, la dérivation partielle des arguments de type que nous avons utilisés dans Flow).

Transférez plus de 30 000 lignes de code de Flow vers TypeScript


Pour commencer, vous devriez mettre à niveau Babel de 6 à 7. Cette tâche simple a pris 16 heures-homme, car nous avons décidé de mettre à niveau Webpack 3 à 4. En même temps, certaines dépendances obsolètes dans notre code ont compliqué la tâche. La grande majorité des projets JavaScript n'auront pas de tels problèmes.

Après cela, nous avons remplacé le préréglage Babel Flow par le nouveau préréglage TypeScript, puis pour la première fois lancé le compilateur TypeScript sur toutes nos sources écrites à l'aide de Flow. Le résultat est 8245 erreurs de syntaxe (tsc CLI n'affiche pas de vraies erreurs pour le projet tant que toutes les erreurs de syntaxe n'ont pas été corrigées).

Au début, ce nombre nous effrayait (très), mais nous avons rapidement réalisé que la plupart des erreurs étaient dues au fait que TypeScript ne supportait pas les fichiers .js. Après avoir étudié le sujet, j'ai appris que les fichiers TypeScript doivent se terminer par .ts ou .tsx (s'ils ont JSX). Cela me semble un inconvénient évident. Afin de ne pas penser à la présence / absence de JSX, j'ai simplement renommé tous les fichiers en .tsx.

Il reste environ 4 000 erreurs de syntaxe. La plupart d'entre eux sont liés à l' importation de type , qui avec TypeScript peut être remplacé simplement par l'importation, ainsi qu'à la différence de désignation des objets ( {||} au lieu de {} ). En appliquant rapidement quelques expressions régulières, nous avons laissé 414 erreurs de syntaxe. Tout le reste devait être corrigé manuellement:

  • Le type existentiel , que nous utilisons pour dériver partiellement des arguments d'un type générique, doit être remplacé par des arguments explicites ou inconnu pour indiquer à TypeScript que certains arguments sont sans importance.
  • Type $ Keys et d'autres types de flux avancés ont une syntaxe différente dans TypeScript (par exemple, $Shape“” correspond à Partial“” dans TypeScript).

Après avoir corrigé toutes les erreurs de syntaxe, tsc a finalement indiqué combien d'erreurs de type réel dans notre base de code ne sont que d'environ 1300. Maintenant, nous devions nous asseoir et décider de continuer ou non. Après tout, si la migration prend des semaines, il est préférable de rester sur Flow. Cependant, nous avons décidé que le portage de code nécessiterait moins d'une semaine de travail par un ingénieur, ce qui est tout à fait acceptable.

Veuillez noter que pendant la migration, j'ai dû arrêter tout travail sur cette base de code. Néanmoins, en parallèle, vous pouvez démarrer de nouveaux projets - mais vous devez garder à l'esprit potentiellement des centaines d'erreurs de type dans le code existant, ce qui n'est pas facile.

Quel genre d'erreurs?


TypeScript et Flow traitent le code JavaScript de plusieurs manières. Ainsi, Flow est plus strict par rapport à certaines choses, et TypeScript - par rapport à d'autres. Une comparaison approfondie des deux systèmes sera très longue, alors regardez quelques exemples.

Remarque: tous les liens vers le sandbox TypeScript supposent des paramètres "stricts". Malheureusement, lorsque vous partagez un lien, ces options ne sont pas stockées dans l'URL. Par conséquent, ils doivent être définis manuellement après avoir ouvert un lien vers le bac à sable à partir de cet article.

invariant.js


La fonction invariant s'est avérée très courante dans notre code source. Juste pour citer la documentation:

 var invariant = require('invariant'); invariant(someTruthyVal, 'This will not throw'); // No errors invariant(someFalseyVal, 'This will throw an error with this message'); // Error raised: Invariant Violation: This will throw an error with this message 

L'idée est claire: une fonction simple qui lance une erreur à certaines conditions. Voyons comment l' implémenter et l'utiliser sur Flow:

 type Maybe<T> = T | void; function invariant(condition: boolean, message: string) { if (!condition) { throw new Error(message); } } function f(x: Maybe<number>, c: number) { if (c > 0) { invariant(x !== undefined, "When c is positive, x should never be undefined"); (x + 1); // works because x has been refined to "number" } } 

Maintenant, chargez le même extrait dans TypeScript . Comme vous pouvez le voir sur le lien, TypeScript donne une erreur, car il ne peut pas comprendre que x garanti de ne pas rester undefined après la dernière ligne. Il s'agit en fait d'un problème bien connu - TypeScript (pour l'instant) ne sait pas comment faire cette inférence via une fonction. Cependant, il s'agit d'un modèle très courant dans notre base de code, j'ai donc dû remplacer manuellement chaque instance d'invariant (plus de 150 pièces) par un autre code qui donne immédiatement une erreur:

 type Maybe<T> = T | void; function f(x: Maybe<number>, c: number) { if (c > 0) { if (x === undefined) { throw new Error("When c is positive, x should never be undefined"); } (x + 1); // works because x has been refined to "number" } } 

Pas vraiment comparé à l' invariant , mais ce n'est pas un problème si important.

$ ExpectError vs @ ts-ignore


Flow a une fonction très intéressante, similaire à @ts-ignore , sauf qu'il renvoie une erreur si la ligne suivante n'est pas une erreur. Ceci est très utile pour écrire des «tests de type» qui garantissent que la vérification de type (que ce soit TypeScript ou Flow) trouve certaines erreurs de type.

Malheureusement, TypeScript n'a pas une telle fonction, donc nos tests ont perdu de la valeur. J'ai hâte d' implémenter cette fonction sur TypeScript .

Erreurs de type générique et inférence de type


Souvent, TypeScript permet un code plus explicite que Flow, comme dans cet exemple:

 type Leaf = { host: string; port: number; type: "LEAF"; }; type Aggregator = { host: string; port: number; type: "AGGREGATOR"; } type MemsqlNode = Leaf | Aggregator; function f(leaves: Array<Leaf>, aggregators: Array<Aggregator>): Array<MemsqlNode> { // The next line errors because you cannot concat aggregators to leaves. return leaves.concat(aggregators); } 

Le flux déduit le type leaves.concat (agrégateurs) comme Array <Leaf | Aggregator> , qui peut ensuite être Array<MemsqlNode> en Array<MemsqlNode> . Je pense que c'est un bon exemple où Flow est un peu plus intelligent, et TypeScript a besoin d'un peu d'aide: dans ce cas, nous pouvons appliquer une assertion de type, mais cela est dangereux et doit être fait très soigneusement.

Bien que je n'ai aucune preuve formelle, je pense que Flow est de loin supérieur à TypeScript en termes d'inférence de type. J'espère vraiment que TypeScript atteindra le niveau Flow, car le langage se développe très activement, et de nombreuses améliorations récentes ont été apportées dans ce domaine. Dans de nombreux endroits de notre code, TypeScript a dû aider un peu les annotations ou les assertions de type, bien que nous ayons évité ces dernières autant que possible). Prenons un autre exemple (nous avons eu plus de 200 erreurs de ce type):

 type Player = { name: string; age: number; position: "STRIKER" | "GOALKEEPER", }; type F = () => Promise<Array<Player>>; const f1: F = () => { return Promise.all([ { name: "David Gomes", age: 23, position: "GOALKEEPER", }, { name: "Cristiano Ronaldo", age: 33, position: "STRIKER", } ]); }; 

TypeScript ne vous autorisera pas à écrire ceci car il ne vous permettra pas de déclarer { name: "David Gomes", age: 23, type: "GOALKEEPER" } en tant qu'objet de type Player (voir le bac à sable pour l'erreur exacte). C'est un autre cas où je trouve que TypeScript n'est pas assez intelligent (au moins par rapport à Flow, qui comprend ce code).

Il existe plusieurs options pour résoudre ce problème:

  • Déclarez "STRIKER" comme "STRIKER" pour que TypeScript comprenne que la chaîne est une énumération valide de type "STRIKER" | "GOALKEEPER" "STRIKER" | "GOALKEEPER" .
  • Déclarez tous les objets en tant que Player .
  • Ou ce que je considère comme la meilleure solution: il suffit d'aider TypeScript sans utiliser d'instructions de type en écrivant Promise.all<Player>(...) .

Voici un autre exemple (TypeScript) où Flow est à nouveau meilleur pour l'inférence de type :

 type Connection = { id: number }; declare function getConnection(): Connection; function resolveConnection() { return new Promise(resolve => { return resolve(getConnection()); }) } resolveConnection().then(conn => { // TypeScript errors in the next line because it does not understand // that conn is of type Connection. We have to manually annotate // resolveConnection as Promise<Connection>. (conn.id); }); 

Un exemple très petit mais intéressant: Flow considère Array<T>.pop() type T , et TypeScript le considère comme T | void T | void Un point en faveur de TypeScript, car il vous oblige à revérifier l'existence d'un élément (si le tableau est vide, alors Array.pop renvoie undefined ). Il existe plusieurs autres petits exemples comme celui-ci où TypeScript est supérieur à Flow.

Définitions TypeScript pour les dépendances tierces


Bien sûr, lors de l'écriture d'une application JavaScript, vous aurez au moins quelques dépendances. Ils doivent être saisis, sinon vous perdrez la plupart des possibilités d'analyse de type statique (comme décrit au début de l'article).

Les bibliothèques de npm peuvent être accompagnées de définitions de type Flow ou TypeScript, avec ou sans les deux. Très souvent, les (petites) bibliothèques ne sont fournies ni avec l'une ni avec l'autre, vous devez donc écrire vos propres définitions de type ou les emprunter à la communauté. Flow et TypeScript prennent en charge les référentiels de définition standard pour les packages JavaScript tiers: ils sont de type flux et DefinitelyTyped .

Je dois dire que DefinitelyTyped nous a beaucoup plu. Avec le type de flux, j'ai dû utiliser l'outil CLI pour introduire des définitions de types pour diverses dépendances dans le projet. DefinitelyTyped combine cette fonction avec l'outil CLI npm en envoyant des @types/package-name au référentiel de packages npm. C'est très cool et simplifie grandement la saisie des définitions de types pour nos dépendances (plaisanterie, react, lodash, react-redux, ce ne sont que quelques-uns).

De plus, j'ai eu beaucoup de plaisir à remplir la base de données DefinitelyTyped (ne pensez pas que les définitions de type sont équivalentes lors du portage de code de Flow vers TypeScript). J'ai déjà envoyé quelques demandes de tirage , et il n'y a eu aucun problème nulle part. Il vous suffit de cloner le référentiel, de modifier les définitions de type, d'ajouter des tests et d'envoyer une demande d'extraction. Le bot DefinitelyTyped GitHub marque les auteurs des définitions que vous avez modifiées. Si aucun d'entre eux ne fournit de commentaires dans les 7 jours, la demande d'extraction est soumise pour examen au responsable. Après avoir fusionné avec la branche principale, une nouvelle version du package de dépendances est envoyée à npm. Par exemple, lorsque j'ai mis à jour le package @ types / redux-form pour la première fois, la version 7.4.14 a été automatiquement envoyée à npm. il suffit donc de mettre à jour le fichier package.json pour obtenir de nouvelles définitions de type. Si vous ne pouvez pas attendre l'adoption de la demande d'extraction, vous pouvez toujours modifier les définitions des types utilisés dans votre projet, comme décrit dans l'un des articles précédents .

En général, la qualité des définitions de type dans DefinitelyTyped est bien meilleure en raison de la communauté TypeScript plus grande et plus prospère. En fait, après le transfert du projet vers TypeScript , notre couverture de type est passée de 88% à 96% , principalement en raison de meilleures définitions des types de dépendance tiers, avec moins any types.

Peluches et tests


  1. Nous sommes passés d'eslint à tslint (avec eslint pour TypeScript, il semblait plus difficile de commencer).
  2. Les tests TypeScript utilisent ts-jest . Certains tests sont typés, tandis que d'autres ne le sont pas (s'ils sont tapés trop longtemps, nous les enregistrons en tant que fichiers .js).

Que s'est-il passé après avoir corrigé toutes les erreurs de frappe?


Après 40 heures de travail, nous avons atteint la dernière erreur de frappe, la reportant pendant un certain temps en utilisant @ts-ignore .

Après avoir examiné les commentaires de révision du code et corrigé quelques bugs (malheureusement, j'ai dû changer un peu le code d'exécution pour corriger la logique que TypeScript ne pouvait pas comprendre), la demande d'extraction avait disparu, et depuis lors, nous utilisons TypeScript. (Et oui, nous avons corrigé ce dernier @ts-ignore dans la prochaine demande de tirage).

Outre l'intégration avec l'éditeur, travailler avec TypeScript est très similaire à travailler avec Flow. Les performances du serveur de flux sont légèrement supérieures, mais ce n'est pas un gros problème, car elles génèrent aussi rapidement des erreurs pour le fichier actuel. La seule différence de performances est que TypeScript signale une nouvelle erreur après avoir enregistré le fichier un peu plus tard (de 0,5 à 1 s). Le temps de démarrage du serveur est approximativement le même (environ 2 minutes), mais ce n'est pas si important. Jusqu'à présent, nous n'avons eu aucun problème de consommation de mémoire. Il semble que tsc utilise constamment environ 600 Mo.

Il peut sembler que la fonction d'inférence de type donne à Flow un gros avantage, mais il y a deux raisons pour lesquelles cela n'a pas vraiment d'importance:

  1. Nous avons converti la base de code Flow en TypeScript. De toute évidence, nous n'avons rencontré que du code que Flow peut exprimer, mais pas TypeScript. Si la migration s'était produite dans la direction opposée, je suis sûr qu'il y aurait des choses que TypeScript afficherait / exprimerait mieux.
  2. L'inférence de type est importante pour aider à écrire du code plus concis. Mais tout de même, d'autres choses sont plus importantes, comme une communauté forte et la disponibilité de définitions de type, car une inférence de type faible peut être corrigée en passant un peu plus de temps à taper.

Statistiques de code


 $ npm run type-coverage # https://github.com/plantain-00/type-coverage 43330 / 45047 96.19% $ cloc # ignoring tests and dependencies -------------------------------------------------------------------------------- Language files blank comment code -------------------------------------------------------------------------------- TypeScript 330 5179 1405 31463 

Et ensuite?


Nous n'avons pas fini d'améliorer l'analyse de type statique. MemSQL a d'autres projets qui finiront par passer de Flow à TypeScript (et certains projets JavaScript qui commenceront à utiliser TypeScript), et nous voulons rendre notre configuration TypeScript plus rigoureuse. L'option strictNullChecks est actuellement activée , mais noImplicitAny est toujours désactivé. Nous supprimerons également quelques instructions de type dangereux du code.

Je suis heureux de partager avec vous tout ce que j'ai appris au cours de mes aventures avec la saisie de JavaScript. Si vous êtes intéressé par un sujet spécifique, faites-le moi savoir .

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


All Articles