Übertragen Sie 30.000 Codezeilen von Flow nach TypeScript

Wir haben kürzlich 30.000 Zeilen JavaScript-Code von unserem MemSQL Studio- System von Flow nach TypeScript verschoben . In diesem Artikel werde ich erklären, warum wir die Codebasis portiert haben, wie es passiert ist und was passiert ist.

Haftungsausschluss: Mein Ziel ist es, Flow überhaupt nicht zu kritisieren. Ich bewundere das Projekt und denke, dass in der JavaScript-Community genügend Platz für beide Optionen zur Typprüfung vorhanden ist. Am Ende wird jeder wählen, was am besten zu ihm passt. Ich hoffe aufrichtig, dass der Artikel bei dieser Wahl helfen wird.

Zunächst werde ich Sie auf den neuesten Stand bringen. Wir bei MemSQL sind große Fans von statischer und starker JavaScript-Typisierung, um häufige Probleme mit dynamischer und schwacher Typisierung zu vermeiden.

Rede über häufige Probleme:

  1. Tippfehler in der Laufzeit aufgrund der Tatsache, dass die verschiedenen Teile des Codes bei impliziten Typen nicht konsistent sind.
  2. Es wird zu viel Zeit damit verbracht, Tests für so triviale Dinge wie das Überprüfen von Typparametern zu schreiben (das Überprüfen der Laufzeit erhöht auch die Größe des Pakets).
  3. Es gibt einen Mangel an Editor / IDE-Integration, da es ohne statische Typisierung viel schwieriger ist, die Funktion "Zur Definition springen", das mechanische Refactoring und andere Funktionen zu implementieren.
  4. Es gibt keine Möglichkeit, Code um Datenmodelle herum zu schreiben, dh zuerst Datentypen zu entwerfen, und dann schreibt sich der Code im Grunde selbst.

Dies sind nur einige der Vorteile der statischen Typisierung, die in einem kürzlich erschienenen Artikel über Flow näher aufgeführt sind.

Anfang 2016 haben wir tcomb implementiert, um eine Art Sicherheit zur Laufzeit eines unserer internen JavaScript-Projekte zu implementieren (Haftungsausschluss: Ich habe mich nicht mit diesem Projekt befasst). Obwohl die Laufzeitprüfung manchmal nützlich ist, bietet sie nicht einmal alle Vorteile der statischen Typisierung (die Kombination aus statischer Typisierung und Typprüfung zur Laufzeit kann in bestimmten Fällen geeignet sein. Mit io-ts können Sie dies mit tcomb und TypeScript tun, obwohl ich es noch nie versucht habe ) Um dies zu verstehen, haben wir uns entschlossen, Flow für ein anderes Projekt zu implementieren, das wir 2016 gestartet haben. Zu dieser Zeit schien Flow eine gute Wahl zu sein:

  • Unterstützung von Facebook, das bei der Entwicklung von React und dem Wachstum der Community hervorragende Arbeit geleistet hat (sie haben auch React with Flow entwickelt).
  • Etwa das gleiche JavaScript-Entwicklungsökosystem. Es war beängstigend, Babel für tsc (den TypeScript-Compiler) aufzugeben, weil wir die Flexibilität verloren hatten, zu einer anderen Typprüfung zu wechseln (offensichtlich hat sich die Situation seitdem geändert).
  • Es ist nicht erforderlich, die gesamte Codebasis zu typisieren (wir wollten uns vor dem All-In eine Vorstellung von statisch typisiertem JavaScript machen), sondern nur einen Teil der Dateien. Bitte beachten Sie, dass sowohl Flow als auch TypeScript dies jetzt zulassen.
  • TypeScript (zu dieser Zeit) fehlten einige der Grundfunktionen, die jetzt verfügbar sind. Dies sind Nachschlagetypen , Standardparameter für generische Typen usw.

Als wir Ende 2017 mit der Arbeit an MemSQL Studio begannen, wollten wir die Typen der gesamten Anwendung behandeln (sie ist vollständig in JavaScript geschrieben: Sowohl das Frontend als auch das Backend werden im Browser ausgeführt). Wir haben Flow als Werkzeug genommen, das wir in der Vergangenheit erfolgreich eingesetzt haben.

Aber meine Aufmerksamkeit wurde auf Babel 7 mit TypeScript-Unterstützung gelenkt. Diese Version bedeutete, dass für die Umstellung auf TypeScript kein Übergang mehr zum gesamten TypeScript-Ökosystem erforderlich war und Sie Babel weiterhin für JavaScript verwenden konnten. Noch wichtiger ist, dass wir TypeScript nur zur Typprüfung verwenden können und nicht als vollwertige „Sprache“.

Persönlich glaube ich, dass die Trennung der Typprüfung vom Codegenerator eine elegantere Art der statischen (und starken) Eingabe in JavaScript ist, weil:

  1. Wir teilen die Probleme des Codes und der Eingabe. Dies reduziert die Stopps der Typprüfung und beschleunigt die Entwicklung: Wenn die Typprüfung aus irgendeinem Grund langsam ist, wird der Code weiterhin korrekt generiert (wenn Sie tsc mit Babel verwenden, können Sie ihn so konfigurieren, dass er dasselbe tut).
  2. Babel hat großartige Plugins und Funktionen, die der TypeScript-Generator nicht hat. Mit Babel können Sie beispielsweise unterstützte Browser angeben und geben automatisch Code für diese aus. Dies ist eine sehr komplexe Funktion und es macht keinen Sinn, sie in zwei verschiedenen Projekten gleichzeitig zu unterstützen.
  3. Ich mag JavaScript als Programmiersprache (mit Ausnahme des Fehlens statischer Typisierung), und ich habe keine Ahnung, wie viel TypeScript existieren wird, während ich seit vielen Jahren an ECMAScript glaube. Daher schreibe und "denke" ich lieber in JavaScript (beachten Sie, dass ich "Flow verwenden" oder "TypeScript verwenden" anstelle von "In Flow schreiben" oder "TypeScript" sage, da ich sie immer mit Werkzeugen und nicht mit Programmiersprachen darstelle).

Dieser Ansatz hat natürlich einige Nachteile:

  1. Der TypeScript-Compiler kann theoretisch typbasierte Optimierungen durchführen, aber hier verlieren wir diese Möglichkeit.
  2. Die Konfiguration des Projekts ist etwas komplizierter, da die Anzahl der Tools und Abhängigkeiten zunimmt. Ich denke, das ist ein relativ schwaches Argument: Ein Haufen Babel und Flow hat uns nie im Stich gelassen.

TypeScript als Alternative zu Flow


Ich bemerkte ein wachsendes Interesse an TypeScript in der JavaScript-Community: sowohl online als auch bei Entwicklern. Sobald ich herausfand, dass Babel 7 TypeScript unterstützt, begann ich sofort, mögliche Übergangsoptionen zu untersuchen. Darüber hinaus sind wir auf einige der Nachteile von Flow gestoßen:

  1. Geringere Qualität der Editor / IDE-Integration (im Vergleich zu TypeScript). Nuclide, Facebooks eigene IDE mit der besten Integration, ist bereits veraltet.
  2. Eine kleinere Community, dh weniger Typdefinitionen für verschiedene Bibliotheken, und sie sind von geringerer Qualität (derzeit hat das DefinitelyTyped-Repository 19 682 GitHub-Sterne und das Flow- Typ - Repository nur 3070).
  3. Fehlen eines öffentlichen Entwicklungsplans und schlechte Interaktion zwischen dem Flow-Team auf Facebook und der Community. Sie können diesen Kommentar eines Facebook-Mitarbeiters lesen, um die Situation zu verstehen.
  4. Hoher Speicherverbrauch und häufige Lecks - für einige unserer Entwickler hat Flow manchmal fast 10 GB RAM beansprucht.

Natürlich sollten Sie untersuchen, wie TypeScript zu uns passt. Dies ist eine sehr komplexe Frage: Das Studium des Themas beinhaltete ein gründliches Lesen der Dokumentation, was zu dem Verständnis beitrug, dass es für jede Flow-Funktion ein gleichwertiges TypeScript gibt. Dann habe ich den öffentlichen TypeScript-Entwicklungsplan untersucht und die für die Zukunft geplanten Funktionen (z. B. teilweise Ableitung von Typargumenten, die wir in Flow verwendet haben) sehr gemocht.

Übertragen Sie mehr als 30.000 Codezeilen von Flow nach TypeScript


Für den Anfang sollten Sie Babel von 6 auf 7 aktualisieren. Diese einfache Aufgabe dauerte 16 Mannstunden, da wir beschlossen, Webpack 3 auf 4 gleichzeitig zu aktualisieren. Einige veraltete Abhängigkeiten in unserem Code erschwerten die Aufgabe. Die überwiegende Mehrheit der JavaScript-Projekte wird solche Probleme nicht haben.

Danach haben wir die Babel Flow-Voreinstellung durch die neue TypeScript-Voreinstellung ersetzt und dann zum ersten Mal den TypeScript-Compiler für alle unsere mit Flow geschriebenen Quellen gestartet. Das Ergebnis sind 8245-Syntaxfehler (tsc CLI zeigt keine echten Fehler für das Projekt an, bis alle Syntaxfehler behoben wurden).

Anfangs erschreckte uns diese Zahl (sehr), aber wir stellten schnell fest, dass die meisten Fehler darauf zurückzuführen waren, dass TypeScript keine .js-Dateien unterstützte. Nachdem ich das Thema studiert hatte, erfuhr ich, dass TypeScript-Dateien entweder mit .ts oder .tsx enden sollten (wenn sie JSX haben). Dies scheint mir eine klare Unannehmlichkeit zu sein. Um nicht über das Vorhandensein / Fehlen von JSX nachzudenken, habe ich einfach alle Dateien in .tsx umbenannt.

Es verbleiben ca. 4.000 Syntaxfehler. Die meisten davon beziehen sich auf den Typimport , der mit TypeScript einfach durch Import ersetzt werden kann, sowie auf den Unterschied in der Bezeichnung von Objekten ( {||} anstelle von {} ). Wir haben schnell ein paar reguläre Ausdrücke angewendet und 414 Syntaxfehler hinterlassen. Alles andere musste manuell repariert werden:

  • Der existenzielle Typ , mit dem wir teilweise Argumente eines generischen Typs ableiten, sollte durch explizite oder unbekannte Argumente ersetzt werden, um TypeScript mitzuteilen, dass einige Argumente unwichtig sind.
  • Type $ Keys und andere erweiterte Flow-Typen haben in TypeScript eine andere Syntax (z. B. entspricht $Shape“” Partial“” in TypeScript).

Nachdem wir alle Syntaxfehler korrigiert hatten, sagte tsc schließlich, wie viele echte Typfehler in unserer Codebasis nur etwa 1300 sind. Jetzt mussten wir uns hinsetzen und entscheiden, ob wir fortfahren wollten oder nicht. Wenn die Migration Wochen dauert, ist es schließlich am besten, auf Flow zu bleiben. Wir haben jedoch beschlossen, dass die Code-Portierung weniger als eine Woche Arbeit eines Ingenieurs erfordert, was durchaus akzeptabel ist.

Bitte beachten Sie, dass ich während der Migration alle Arbeiten an dieser Codebasis einstellen musste. Parallel dazu können Sie neue Projekte starten - aber Sie müssen möglicherweise Hunderte von Typfehlern in vorhandenem Code berücksichtigen, was nicht einfach ist.

Was für Fehler?


TypeScript und Flow verarbeiten JavaScript-Code auf viele Arten. Daher ist Flow in Bezug auf einige Dinge strenger und TypeScript in Bezug auf andere. Ein tiefer Vergleich der beiden Systeme wird sehr lang sein. Schauen Sie sich also nur einige Beispiele an.

Hinweis: Alle Links zur TypeScript-Sandbox setzen "strenge" Parameter voraus. Wenn Sie einen Link freigeben, werden diese Optionen leider nicht in der URL gespeichert. Daher müssen sie manuell festgelegt werden, nachdem ein Link zur Sandbox aus diesem Artikel geöffnet wurde.

invariant.js


Die invariant Funktion war in unserem Quellcode sehr verbreitet. Nur um die Dokumentation zu zitieren:

 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 

Die Idee ist klar: Eine einfache Funktion, die unter bestimmten Bedingungen einen Fehler auslöst. Mal sehen, wie man es in Flow implementiert und verwendet :

 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" } } 

Laden Sie nun dasselbe Snippet in TypeScript . Wie Sie dem Link entnehmen können, gibt TypeScript einen Fehler aus, da nicht verstanden werden kann, dass x nach der letzten Zeile garantiert nicht undefined bleibt. Dies ist eigentlich ein bekanntes Problem - TypeScript weiß (vorerst) nicht, wie diese Folgerung über eine Funktion durchgeführt werden soll. Dies ist jedoch eine sehr häufige Vorlage in unserer Codebasis, daher musste ich jede invariante Instanz (mehr als 150 Teile) manuell durch einen anderen Code ersetzen, der sofort einen Fehler ausgibt:

 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" } } 

Nicht wirklich im Vergleich zu invariant , aber kein so wichtiges Thema.

$ ExpectError vs @ ts-ignore


Flow hat eine sehr interessante Funktion, ähnlich wie @ts-ignore , außer dass es einen Fehler auslöst, wenn die nächste Zeile kein Fehler ist. Dies ist sehr nützlich, um „Typentests“ zu schreiben, die sicherstellen, dass bei der Typprüfung (ob TypeScript oder Flow) bestimmte Typfehler festgestellt werden.

Leider hat TypeScript keine solche Funktion, so dass unsere Tests an Wert verloren haben. Ich freue mich darauf , diese Funktion in TypeScript zu implementieren .

Generische Typfehler und Typinferenz


Oft erlaubt TypeScript expliziteren Code als Flow, wie in diesem Beispiel:

 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); } 

Flow leitet den Typ leaf.concat (Aggregatoren) als Array <Leaf | ab Aggregator> , der dann in Array<MemsqlNode> . Ich denke, dies ist ein gutes Beispiel, bei dem Flow etwas intelligenter sein kann und TypeScript ein wenig Hilfe benötigt: In diesem Fall können wir eine Typzusicherung anwenden, dies ist jedoch gefährlich und sollte sehr sorgfältig durchgeführt werden.

Obwohl ich keine formalen Beweise habe, glaube ich, dass Flow TypeScript in der Typinferenz weit überlegen ist. Ich hoffe wirklich, dass TypeScript das Flow-Level erreicht, da sich die Sprache sehr aktiv entwickelt und in diesem Bereich in letzter Zeit viele Verbesserungen vorgenommen wurden. An vielen Stellen in unserem Code musste TypeScript ein wenig durch Anmerkungen oder Typzusicherungen helfen, obwohl wir letztere so weit wie möglich vermieden haben . Betrachten wir noch ein Beispiel (wir hatten mehr als 200 solcher Fehler):

 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", } ]); }; 

Mit TypeScript können Sie dies nicht schreiben, da Sie { name: "David Gomes", age: 23, type: "GOALKEEPER" } als Objekt vom Typ Player { name: "David Gomes", age: 23, type: "GOALKEEPER" } (den genauen Fehler finden Sie in der Sandbox). Dies ist ein weiterer Fall, in dem TypeScript nicht intelligent genug ist (zumindest im Vergleich zu Flow, der diesen Code versteht).

Es gibt verschiedene Möglichkeiten, dies zu beheben:

  • Deklarieren Sie "STRIKER" als "STRIKER" damit TypeScript versteht, dass die Zeichenfolge eine gültige Aufzählung vom Typ "STRIKER" | "GOALKEEPER" "STRIKER" | "GOALKEEPER" .
  • Deklarieren Sie alle Objekte als Player .
  • Oder was ich für die beste Lösung halte: Helfen Sie TypeScript einfach, ohne Promise.all<Player>(...) indem Sie Promise.all<Player>(...) schreiben.

Hier ist ein weiteres Beispiel (TypeScript), in dem Flow bei der Typinferenz wieder besser ist :

 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); }); 

Ein sehr kleines, aber interessantes Beispiel: Flow betrachtet Array<T>.pop() Typ T und TypeScript als T | void T | void Ein Punkt zugunsten von TypeScript, da Sie Array.pop , die Existenz eines Elements zu überprüfen (wenn das Array leer ist, gibt Array.pop undefined ). Es gibt einige andere kleine Beispiele wie dieses, bei denen TypeScript Flow überlegen ist.

TypeScript-Definitionen für Abhängigkeiten von Drittanbietern


Wenn Sie eine JavaScript-Anwendung schreiben, haben Sie natürlich mindestens einige Abhängigkeiten. Sie sollten eingegeben werden, da Sie sonst die meisten Möglichkeiten der statischen Typanalyse verlieren (wie am Anfang des Artikels beschrieben).

Bibliotheken von npm können mit Flow- oder TypeScript-Typdefinitionen mit oder ohne beides geliefert werden. Sehr oft werden (kleine) Bibliotheken weder mit der einen noch mit der anderen geliefert, daher müssen Sie Ihre eigenen Typdefinitionen schreiben oder sie von der Community ausleihen. Sowohl Flow als auch TypeScript unterstützen Standarddefinitions-Repositorys für JavaScript-Pakete von Drittanbietern: Diese sind Flow-typisiert und DefinitelyTyped .

Ich muss sagen, dass DefinitelyTyped uns viel besser gefallen hat. Bei Flow-Typing musste ich das CLI-Tool verwenden, um Typdefinitionen für verschiedene Abhängigkeiten in das Projekt einzuführen. DefinitelyTyped kombiniert diese Funktion mit dem npm CLI-Tool, indem @types/package-name Pakete an das npm-Paketrepository gesendet werden. Dies ist sehr cool und hat die Eingabe von Typdefinitionen für unsere Abhängigkeiten stark vereinfacht (Scherz, Reagieren, Lodash, Reagieren-Redux, dies sind nur einige davon).

Außerdem hatte ich viel Spaß beim Auffüllen der DefinitelyTyped-Datenbank (denken Sie nicht, dass Typdefinitionen beim Portieren von Code von Flow nach TypeScript gleichwertig sind). Ich habe bereits einige Pull-Anfragen gesendet , und es gab nirgendwo Probleme. Klonen Sie einfach das Repository, bearbeiten Sie Typdefinitionen, fügen Sie Tests hinzu - und senden Sie eine Pull-Anfrage. Der DefinitelyTyped GitHub-Bot markiert die Autoren der von Ihnen bearbeiteten Definitionen. Wenn keiner von ihnen innerhalb von 7 Tagen eine Rückmeldung gibt, wird die Pull-Anfrage zur Prüfung an den Betreuer gesendet. Nach dem Zusammenführen mit dem Hauptzweig wird eine neue Version des Abhängigkeitspakets an npm gesendet. Als ich beispielsweise das Paket @ types / redux-form zum ersten Mal aktualisierte, wurde Version 7.4.14 automatisch an npm gesendet. Aktualisieren Sie einfach die Datei package.json, um neue Typdefinitionen zu erhalten. Wenn Sie nicht auf die Annahme der Pull-Anforderung warten können, können Sie jederzeit die Definitionen der in Ihrem Projekt verwendeten Typen ändern, wie in einem der vorherigen Artikel beschrieben .

Im Allgemeinen ist die Qualität der Typdefinitionen in DefinitelyTyped aufgrund der größeren und erfolgreicheren TypeScript-Community viel besser. Nachdem das Projekt auf TypeScript übertragen wurde , stieg unsere Typabdeckung von 88% auf 96% , hauptsächlich aufgrund besserer Definitionen von Abhängigkeitstypen von Drittanbietern mit weniger Typen.

Flusen und Tests


  1. Wir haben von eslint zu tslint gewechselt (mit eslint für TypeScript schien es schwieriger zu sein, loszulegen).
  2. TypeScript-Tests verwenden ts-jest . Einige der Tests werden getippt, andere nicht (wenn sie zu lange getippt werden, speichern wir sie als .js-Dateien).

Was ist passiert, nachdem alle Tippfehler behoben wurden?


Nach 40 Arbeitsstunden haben wir den letzten Tippfehler erreicht und ihn mit @ts-ignore für eine Weile verschoben.

Nachdem ich die Kommentare zur Codeüberprüfung überprüft und einige Fehler behoben habe (leider musste ich den Laufzeitcode ein wenig ändern, um die Logik zu beheben, die TypeScript nicht verstehen konnte), ist die Pull-Anforderung weg, und seitdem verwenden wir TypeScript. (Und ja, wir haben das letzte @ts-ignore in der nächsten Pull-Anfrage behoben).

Neben der Integration in den Editor ist die Arbeit mit TypeScript der Arbeit mit Flow sehr ähnlich. Die Leistung des Flow-Servers ist etwas höher, dies ist jedoch kein großes Problem, da sie gleich schnell Fehler für die aktuelle Datei generieren. Der einzige Leistungsunterschied besteht darin, dass TypeScript einen neuen Fehler meldet, nachdem die Datei etwas später (um 0,5 - 1 s) gespeichert wurde. Die Startzeit des Servers ist ungefähr gleich (ca. 2 Minuten), aber nicht so wichtig. Bisher hatten wir keine Probleme mit dem Speicherverbrauch. Es scheint, dass tsc ständig rund 600 MB verwendet.

Es mag den Anschein haben, dass die Typinferenzfunktion Flow einen großen Vorteil verschafft, aber es gibt zwei Gründe, warum dies nicht wirklich wichtig ist:

  1. Wir haben die Flow-Codebasis in TypeScript konvertiert. Offensichtlich sind wir nur auf solchen Code gestoßen, den Flow ausdrücken kann, TypeScript jedoch nicht. Wenn die Migration in die entgegengesetzte Richtung stattgefunden hätte, würde es sicher Dinge geben, die TypeScript besser anzeigt / ausdrückt.
  2. Typinferenz ist wichtig, um präziseren Code zu schreiben. Trotzdem sind andere Dinge wichtiger, wie eine starke Community und die Verfügbarkeit von Typdefinitionen, da schwache Typinferenzen behoben werden können, indem etwas mehr Zeit für die Eingabe aufgewendet wird.

Codestatistik


 $ 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 

Was weiter?


Wir sind nicht damit fertig, die statische Typanalyse zu verbessern. MemSQL hat andere Projekte, die irgendwann von Flow zu TypeScript wechseln werden (und einige JavaScript-Projekte, die TypeScript verwenden), und wir möchten unsere TypeScript-Konfiguration strenger gestalten. Wir haben derzeit die Option strictNullChecks aktiviert , aber noImplicitAny ist immer noch deaktiviert. Wir werden auch einige gefährliche Typanweisungen aus dem Code entfernen.

Ich freue mich, Ihnen alles mitteilen zu können, was ich während meiner Abenteuer mit der Eingabe von JavaScript gelernt habe. Wenn Sie an einem bestimmten Thema interessiert sind, lassen Sie es mich bitte wissen .

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


All Articles