Typensicherheit in JavaScript: Flow und TypeScript

Jeder, der sich mit der Entwicklung der Benutzeroberfläche in einem blutigen Unternehmen befasst, hat wahrscheinlich von "typisiertem JavaScript" gehört, was "Microsoft TypeScript" bedeutet. Neben dieser Lösung gibt es jedoch mindestens ein weiteres gängiges JS-Typisierungssystem und auch einen wichtigen Akteur in der IT-Welt. Dies ist ein Flow von Facebook. Aufgrund meiner persönlichen Abneigung gegen Microsoft habe ich immer Flow verwendet. Objektiv wurde dies durch eine gute Integration in bestehende Versorgungsunternehmen und eine einfache Umstellung erklärt.



Leider müssen wir zugeben, dass Flow im Jahr 2021 TypeScript sowohl in Bezug auf die Popularität als auch in Bezug auf die Unterstützung durch eine Vielzahl von Dienstprogrammen (und Bibliotheken) bereits erheblich unterlegen ist, und es ist an der Zeit , ihn im Regal zu vergraben und mit dem Kauen von Kakteen aufzuhören.Gehen Sie zum De-facto-TypeScript-Standard. Aber darunter möchte ich zum Schluss diese Technologien vergleichen, sagen wir ein paar (oder nicht ein paar) Flow von Facebook.



Warum benötigen Sie Typensicherheit in JavaScript?



JavaScript ist eine wunderbare Sprache. Nein, nicht so. Das Ökosystem rund um JavaScript ist großartig. Für 2021 bewundert sie wirklich die Tatsache, dass Sie die modernsten Funktionen der Sprache verwenden und dann durch Ändern einer Einstellung des Build-Systems die ausführbare Datei transpilieren können, um ihre Ausführung in älteren Versionen von Browsern, einschließlich IE8, zu unterstützen , es wird nicht bei Nacht sein, erinnere dich. Sie können "in HTML schreiben" (dh JSX) und dann mit dem Dienstprogramm babel



(oder tsc



) alle Tags durch korrekte JavaScript-Konstrukte ersetzen, z. B. das Aufrufen der React-Bibliothek (oder eines anderen, aber mehr dazu in einem anderen Beitrag).



Warum eignet sich JavaScript als Skriptsprache, die in Ihrem Browser ausgeführt wird?



  • JavaScript muss nicht "kompiliert" werden. Sie fügen einfach JavaScript-Konstrukte hinzu und der Browser muss sie verstehen. Dies gibt sofort eine Reihe von praktischen und fast kostenlosen Dingen. Zum Beispiel das Debuggen direkt im Browser, was nicht in der Verantwortung des Programmierers liegt (der beispielsweise nicht vergessen darf, eine Reihe von Compiler-Debugging-Optionen und entsprechenden Bibliotheken einzuschließen), sondern des Browser-Entwicklers. Sie müssen nicht 10 bis 30 Minuten warten (Echtzeit für C / C ++), während Ihr 10k-Zeilenprojekt kompiliert wird, um zu versuchen, etwas anderes zu schreiben. Sie ändern einfach die Zeile, laden die Browserseite neu und beobachten das neue Verhalten des Codes. Und wenn Sie beispielsweise ein Webpack verwenden, wird die Seite auch für Sie neu geladen. In vielen Browsern können Sie den Code direkt auf der Seite mit ihren devtools ändern.
  • - . 2021 . Chrome/Firefox, , , 5% (enterprise-) 30% (UI/) , .
  • JavaScript , . — ( worker'). , 100% CPU ( UI ), , , Promise/async/await/etc.
  • Gleichzeitig denke ich nicht einmal an die Frage, warum JavaScript wichtig ist. Schließlich können Sie mit Hilfe von JS: Formulare validieren, den Seiteninhalt aktualisieren, ohne ihn vollständig neu zu laden, nicht standardmäßige Verhaltenseffekte hinzufügen, mit Audio und Video arbeiten und sogar den gesamten Client Ihrer Unternehmensanwendung in schreiben JavaScript.


Wie bei fast jeder Skriptsprache (interpretiert) können Sie in JavaScript ... fehlerhaften Code schreiben. Wenn der Browser diesen Code nicht erreicht, wird keine Fehlermeldung, keine Warnung und überhaupt nichts angezeigt. Einerseits ist das gut. Wenn Sie eine große, große Website haben, sollte selbst ein Syntaxfehler im Code des Button-Click-Handlers nicht dazu führen, dass die Site nicht vollständig vom Benutzer geladen wird.



Aber das ist natürlich schlecht. Denn die Tatsache, dass irgendwo auf der Website etwas nicht funktioniert, ist schlecht. Und es wäre großartig, bevor der Code auf eine funktionierende Site gelangt, alle Skripte auf der Site zu überprüfen und sicherzustellen, dass sie zumindest kompiliert werden. Und im Idealfall - und arbeiten. Hierfür werden verschiedene Dienstprogramme verwendet (mein Lieblingssatz ist npm + webpack + babel / tsc + karma + jsdom + mocha + chai).



Wenn wir in einer idealen Welt leben, werden alle Skripte auf Ihrer Website, auch einzeilige, mit Tests abgedeckt. Leider ist die Welt nicht ideal, und für all den Teil des Codes, der nicht durch Tests abgedeckt wird, können wir uns nur auf eine Art automatisierter Verifizierungswerkzeuge verlassen. Welches kann überprüfen:



  • JavaScript. , JavaScript, , , . /// .
  • . , , . , :



    var x = null;
    x.foo();
    
          
          





    . — null .


Zusätzlich zu Semantikfehlern kann es noch schrecklichere Fehler geben: logische Fehler. Wenn das Programm fehlerfrei läuft, aber das Ergebnis überhaupt nicht das ist, was erwartet wurde. Klassiker mit zusätzlichen Zeichenfolgen und Zahlen:



console.log( input.value ) // 1
console.log( input.value + 1 ) // 11

      
      





Bestehende statische Code-Analyse-Tools (z. B. eslint) können versuchen, eine erhebliche Anzahl potenzieller Fehler aufzuspüren, die ein Programmierer in seinem Code macht. Beispielsweise:





Beachten Sie, dass alle diese Regeln im Wesentlichen Einschränkungen sind, die der Linter dem Programmierer auferlegt. Das heißt, der Linter reduziert tatsächlich die Fähigkeiten der JavaScript-Sprache, so dass der Programmierer weniger potenzielle Fehler macht. Wenn Sie All-All-Regeln aktivieren, ist es unmöglich, Zuweisungen unter Bedingungen vorzunehmen (obwohl JavaScript dies zunächst zulässt), doppelte Schlüssel in Objektliteralen zu verwenden und kann sogar nicht aufgerufen werden console.log()



.



Das Hinzufügen von Variablentypen und die Typprüfung von Aufrufen sind zusätzliche Einschränkungen der JavaScript-Sprache, um potenzielle Fehler zu reduzieren.



Bild

Es wird versucht, eine Zahl mit einer Zeichenfolge zu multiplizieren



Ein Versuch, auf eine nicht vorhandene (im Typ nicht beschriebene) Eigenschaft eines Objekts zuzugreifen

Ein Versuch, auf eine nicht vorhandene (im Typ nicht beschriebene) Eigenschaft eines Objekts zuzugreifen.



Ein Versuch, auf eine nicht vorhandene (im Typ nicht beschriebene) Eigenschaft eines Objekts zuzugreifen

Ein Versuch, eine Funktion mit einem nicht übereinstimmenden Argumenttyp aufzurufen.



Wenn wir diesen Code ohne Typprüfung schreiben, wird der Code erfolgreich transpiliert. Kein Mittel zur statischen Code-Analyse kann diese Fehler nicht finden, wenn sie keine (expliziten oder impliziten) Informationen über die Objekttypen verwenden.



Das Hinzufügen von Eingabe zu JavaScript fügt dem vom Programmierer geschriebenen Code zusätzliche Einschränkungen hinzu, ermöglicht es Ihnen jedoch, Fehler zu finden, die sonst während der Skriptausführung auftreten würden (dh höchstwahrscheinlich im Browser des Benutzers).



JavaScript-Eingabefunktionen



Fließen Typoskript
Möglichkeit, den Typ einer Variablen, ein Argument oder einen Rückgabetyp einer Funktion festzulegen
a : number = 5;
function foo( bar : string) : void {
    /*...*/
} 

      
      



Fähigkeit, Ihren Objekttyp (Schnittstelle) zu beschreiben
type MyType {
    foo: string,
    bar: number
}

      
      



Einschränken von Werten für einen Typ
type Suit = "Diamonds" | "Clubs" | "Hearts" | "Spades";

      
      



Separate Erweiterung auf Typebene für Aufzählungen
enum Direction { Up, Down, Left, Right }

      
      



Typen "hinzufügen"
type MyType = TypeA & TypeB;

      
      



Zusätzliche "Typen" für komplexe Fälle
$Keys<T>, $Values<T>, $ReadOnly<T>, $Exact<T>, $Diff<A, B>, $Rest<A, B>, $PropertyType<T, k>, $ElementType<T, K>, $NonMaybeType<T>, $ObjMap<T, F>, $ObjMapi<T, F>, $TupleMap<T, F>, $Call<F, T...>, Class<T>, $Shape<T>, $Exports<T>, $Supertype<T>, $Subtype<T>, Existential Type (*)
      
      



Partial<T>, Required<T>, Readonly<T>, Record<K,T>, Pick<T, K>, Omit<T, K>, Exclude<T, U>, Extract<T, U>, NonNullable<T>, Parameters<T>, ConstructorParameters<T>, ReturnType<T>, InstanceType<T>, ThisParameterType<T>, OmitThisParameter<T>, ThisType<T>

      
      





Beide Engines für die Unterstützung von JavaScript-Typen verfügen ungefähr über die gleichen Funktionen. Wenn Sie jedoch aus stark typisierten Sprachen stammen, unterscheidet sich selbst typisiertes JavaScript erheblich von Java: Alle Typen beschreiben im Wesentlichen Schnittstellen, dh eine Liste von Eigenschaften (und deren Typen und / oder Argumente). Und wenn zwei Schnittstellen dieselben (oder kompatiblen) Eigenschaften beschreiben, können sie anstelle voneinander verwendet werden. Das heißt, der folgende Code ist in typisiertem JavaScript korrekt, in Java oder beispielsweise C ++ jedoch eindeutig falsch:



type MyTypeA = { foo: string; bar: number; }
type MyTypeB = { foo: string; }

function myFunction( arg : MyTypeB ) : string {
    return `Hello, ${arg.foo}!`;
}

const myVar : MyTypeA = { foo: "World", bar: 42 } as MyTypeA;
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





Dieser Code ist aus Sicht von typisiertem JavaScript korrekt, da für die MyTypeB-Schnittstelle eine Eigenschaft foo



mit einem Typ erforderlich ist string



, für eine Variable mit der MyTypeA-Schnittstelle.



Dieser Code kann mithilfe einer Literalschnittstelle für eine Variable etwas kürzer umgeschrieben werden myVar



.



type MyTypeB = { foo: string; }

function myFunction( arg : MyTypeB ) : string {
    return `Hello, ${arg.foo}!`;
}

const myVar = { foo: "World", bar: 42 };
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





Der Variablentyp myVar



in diesem Beispiel ist eine Literalschnittstelle { foo: string, bar: number }



. Es ist weiterhin mit der erwarteten Schnittstelle eines arg



Funktionsarguments kompatibel myFunction



, sodass dieser Code beispielsweise aus Sicht von TypeScript fehlerfrei ist.



Dieses Verhalten reduziert die Anzahl der Probleme beim Arbeiten mit verschiedenen Bibliotheken, benutzerdefiniertem Code und sogar beim Aufrufen von Funktionen erheblich. Ein typisches Beispiel ist, wenn eine Bibliothek gültige Optionen definiert und wir sie als Optionsobjekt übergeben:



// -  
interface OptionsType {
    optionA?: string;
    optionB?: number;
}
export function libFunction( arg: number, options = {} as OptionsType) { /*...*/ }

      
      





//   
import {libFunction} from "lib";
libFunction( 42, { optionA: "someValue" } );

      
      





Beachten Sie, dass der Typ OptionsType



weder aus der Bibliothek exportiert noch in benutzerdefinierten Code importiert wird. Dies hindert Sie jedoch nicht daran, die Funktion über die Literalschnittstelle für das zweite Argument der options



Funktion und für das Typisierungssystem aufzurufen , um dieses Argument auf Typkompatibilität zu überprüfen. Der Versuch, so etwas in Java zu tun, führt zu einer deutlichen Verwirrung unter den Compilern.



Wie funktioniert es aus Browsersicht?



Weder das TypeScript von Microsoft noch der Flow von Facebook werden von Browsern unterstützt. Ebenso haben die neuesten JavaScript-Spracherweiterungen in einigen Browsern noch keine Unterstützung gefunden. Wie wird dieser Code zum einen auf Richtigkeit überprüft und zum anderen vom Browser ausgeführt?



Die Antwort ist traspiling. Der gesamte "nicht standardmäßige" JavaScript-Code durchläuft eine Reihe von Dienstprogrammen, die den "nicht standardmäßigen" (Browsern unbekannten) Code in eine Reihe von Anweisungen verwandeln, die die Browser verstehen. Und für die Eingabe besteht die gesamte "Transformation" darin, dass alle Typverfeinerungen, alle Schnittstellenbeschreibungen, alle Einschränkungen aus dem Code einfach entfernt werden. Aus dem Code aus dem obigen Beispiel wird beispielsweise ...



/* : type MyTypeA = { foo: string; bar: number; } */
/* : type MyTypeB = { foo: string; } */

function myFunction( arg /* : : MyTypeB */ ) /* : : string */ {
    return `Hello, ${arg.foo}!`;
}

const myVar /* : : MyTypeA */ = { foo: "World", bar: 42 } /* : as MyTypeA */;
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





jene.

function myFunction( arg ) {
    return `Hello, ${arg.foo}!`;
}
const myVar = { foo: "World", bar: 42 };
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





Diese Konvertierung erfolgt normalerweise auf eine der folgenden Arten.





Beispiele für Projekteinstellungen für Flow und TypeScript (mit tsc).

Fließen Typoskript
webpack.config.js
{
  test: /\.js$/,
  include: /src/,
  exclude: /node_modules/,
  loader: 'babel-loader',
},

      
      



{
  test: /\.(js|ts|tsx)$/,
  exclude: /node_modules/,
  include: /src/,
  loader: 'ts-loader',
},

      
      



Transpilereinstellungen
babel.config.js tsconfig.json
module.exports = function( api ) {
  return {
    presets: [
      '@babel/preset-flow',
      '@babel/preset-env',
      '@babel/preset-react',
    ],
  };
};

      
      



{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": false,
    "jsx": "react",
    "lib": ["dom", "es5", "es6"],
    "module": "es2020",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "outDir": "./dist/static",
    "target": "es6"
  },
  "include": ["src/**/*.ts*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

      
      



.flowconfig
[ignore]
<PROJECT_ROOT>/dist/.*
<PROJECT_ROOT>/test/.*
[lints]
untyped-import=off
unclear-type=off
[options]

      
      





Der Unterschied zwischen babel + strip- und tsc-Ansätzen ist in Bezug auf die Montage gering. Im ersten Fall wird babel verwendet, im zweiten Fall ist es tsc.





Es gibt jedoch einen Unterschied, wenn ein Dienstprogramm wie eslint verwendet wird. TypeScript zum Flusen mit eslint verfügt über eigene Plugins, mit denen Sie noch mehr Fehler finden können. Sie erfordern jedoch, dass der Linter zum Zeitpunkt der Analyse Informationen über die Variablentypen enthält. Zu diesem Zweck sollte nur tsc als Code-Parser verwendet werden, nicht babel. Wenn jedoch tsc für den Linter verwendet wird, ist es falsch, babel zum Bauen zu verwenden (der Zoo der verwendeten Dienstprogramme sollte minimal sein!).





Fließen Typoskript
.eslint.js
module.exports = {
  parser: 'babel-eslint',
  parserOptions: {
    /* ... */

      
      



module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    /* ... */

      
      





Typen für Bibliotheken



Wenn eine Bibliothek im npm-Repository veröffentlicht wird, wird die JavaScript-Version veröffentlicht. Es wird davon ausgegangen, dass der veröffentlichte Code nicht geändert werden muss, um in einem Projekt verwendet zu werden. Das heißt, der Code hat bereits die erforderliche Traspilation über babel oder tsc übergeben. Aber dann gehen die Informationen über die Typen im Code bereits verloren. Was zu tun ist?



Im Flow wird davon ausgegangen, dass die Bibliothek zusätzlich zur "reinen" JavaScript-Version Dateien mit der Erweiterung enthält .js.flow



Enthält den Quellflusscode mit allen Typdefinitionen. Bei der Analyse des Datenflusses können diese Dateien dann zur Typprüfung verbunden werden. Beim Erstellen des Projekts und seiner Ausführung werden sie ignoriert. Es werden normale JS-Dateien verwendet. Sie können der Bibliothek Flow-Dateien hinzufügen, indem Sie sie einfach kopieren. Dies erhöht jedoch die Größe der Bibliothek in npm erheblich.



In TypeScript wird nicht empfohlen, die Quelldateien nebeneinander zu halten, sondern nur eine Liste von Definitionen. Wenn eine Datei vorhanden ist myModule.js



, sucht TypeScript bei der Analyse des Projekts nach einer Datei in der Nähe myModule.js.d.ts



, in der Definitionen (aber kein Code!) Von allen Typen, Funktionen und anderen Dingen angezeigt werden, die zum Analysieren von Typen erforderlich sind. Der tsc-Transpiler kann solche Dateien selbst aus dem Quell-TypeScript erstellen (siehe Option) declaration



in der Dokumentation).



Typen für Legacy-Bibliotheken



Sowohl für Flow als auch für TypeScript gibt es eine Möglichkeit, Typdeklarationen für Bibliotheken hinzuzufügen, die diese Beschreibungen anfangs nicht enthalten. Aber es wird auf verschiedene Arten gemacht.



Für den Flow gibt es keine "native" Methode, die von Facebook selbst unterstützt wird. Es gibt jedoch ein Flow-typisiertes Projekt , das solche Definitionen in seinem Repository sammelt. In der Tat eine parallele Möglichkeit für npm, solche Definitionen zu versionieren, und auch keine sehr bequeme "zentralisierte" Art der Aktualisierung.



In TypeScript besteht die Standardmethode zum Schreiben solcher Definitionen darin, sie in speziellen npm-Paketen mit dem Präfix "@types" zu veröffentlichen... Um Ihrem Projekt eine Beschreibung der Typen für eine Bibliothek hinzuzufügen, reicht es aus, die entsprechende @ types-Bibliothek zu verbinden, z. B. @types/react



für React oder @types/chai



für Chai.



Vergleich von Flow und TypeScript



Ein Versuch, Flow und TypeScript zu vergleichen. Ausgewählte Fakten stammen aus Nathan Sebhastians Artikel "TypeScript VS Flow", einige werden unabhängig voneinander gesammelt.



Native Unterstützung über verschiedene Frameworks hinweg. Native - kein zusätzlicher Ansatz mit einem Lötkolben und Bibliotheken und Plugins von Drittanbietern.



Verschiedene Herrscher

Fließen Typoskript
Hauptverantwortlicher Facebook Microsoft
Webseite flow.org www.typescriptlang.org
Github github.com/facebook/flow github.com/microsoft/TypeScript
GitHub startet 21,3k 70,1k
GitHub Gabeln 1,8k 9,2k
GitHub-Probleme: offen / geschlossen 2,4 k / 4,1 k 4,9 k / 25,0 k
StackOverflow Aktiv 2289 146,221
StackOverflow Häufig 123 11451


Wenn ich mir diese Zahlen anschaue, habe ich einfach nicht das moralische Recht, Flow zur Verwendung zu empfehlen. Aber warum habe ich es selbst benutzt? Weil es früher so etwas wie Flow-Runtime gab.



Flow-Laufzeit



flow-runtime ist eine Reihe von Plugins für babel, mit denen Sie Flow-Typen in die Laufzeit einbetten, zur Laufzeit Variablentypen definieren und vor allem zur Laufzeit die Variablentypen überprüfen können. Dies ermöglichte es zur Laufzeit, beispielsweise während Autotests oder manuellen Tests, zusätzliche Fehler in der Anwendung zu erkennen.



Das heißt, direkt zur Laufzeit (natürlich in der Debug-Assembly) überprüfte die Anwendung explizit alle Arten von Variablen, Argumenten, Ergebnissen von Aufrufen von Funktionen von Drittanbietern und alles, alles, alles auf Übereinstimmung mit diesen Typen.



Leider hat der Autor des Repository für das neue Jahr 2021 Informationen hinzugefügtdass er nicht mehr an der Entwicklung dieses Projekts beteiligt ist und generell zu TypeScript wechselt. Tatsächlich wurde der letzte Grund, im Fluss zu bleiben, für mich veraltet. Willkommen bei TypeScript.



All Articles