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:
- Tippfehler in der Laufzeit aufgrund der Tatsache, dass die verschiedenen Teile des Codes bei impliziten Typen nicht konsistent sind.
- 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).
- 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.
- 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:
- 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).
- 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.
- 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:
- Der TypeScript-Compiler kann theoretisch typbasierte Optimierungen durchführen, aber hier verlieren wir diese Möglichkeit.
- 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:
- Geringere Qualität der Editor / IDE-Integration (im Vergleich zu TypeScript). Nuclide, Facebooks eigene IDE mit der besten Integration, ist bereits veraltet.
- 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).
- 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.
- 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');
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);
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);
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> {
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 => {
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
- Wir haben von eslint zu tslint gewechselt (mit eslint für TypeScript schien es schwieriger zu sein, loszulegen).
- 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:
- 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.
- 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
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 .