Wie Wire von JavaScript zu TypeScript wechselte – Tipps für die Migration

Seite 2: Wie es anfing – die ersten drei Monate mit TypeScript

Inhaltsverzeichnis

Während der ersten drei Monate der Umstellungsphase hat Wire zunächst den Fokus auf die Einarbeitung in die neue Sprache gelegt. Dabei stellte sich schnell heraus, dass TypeScript die Paradigmen der funktionalen und objektorientierten Programmierung beherrscht und mit der starken Typisierung die Anzahl von Fehlern verringert. Die kontextberücksichtigenden Vervollständigungsvorschläge beschleunigen zudem den Programmiervorgang.

Besonders die starke Typisierung kommt bei Programmierern gut an. Denn nicht nur Tippfehler werden erkannt, sondern auch die Verfügbarkeit von Objekten oder Werten wird fortlaufend mitgeprüft. Das schließt viele Fehlerquellen aus. Gleichzeitig kann TypeScript jedes Vanilla-JavaScript nutzen, ohne den JavaScript-Code verändern zu müssen, und kann so vielseitig zum Einsatz kommen. Die vollständige Übersetzung des JavaScript-Codes von Wire dauert noch an, aber die Migration ist bereits gestartet. Durch den Wechsel im laufenden Betrieb entstand eine weitere Herausforderung – doch durch eine schrittweise Vorgehensweise ergeben sich für den Endnutzer keine Serviceeinschränkungen.

Bei der Einführung von TypeScript in ein JavaScript-Großprojekt sollte der TypeScript-Compiler zuerst nur zur reinen Typenüberprüfung verwendet werden. Dazu muss man die Option noEmit im Compiler einstellen, damit er keine Ausgabe generiert und somit nur als Prüfwerkzeug funktioniert. Dieser Schritt ist minimal-invasiv, da man keinen Code verändern muss.

Im Nachgang lässt sich TypeScript so einstellen, dass JSDoc-Kommentare aus JavaScript auf ihre Richtigkeit überprüft werden. Dadurch erhält man einen ersten Nutzen aus der Einführung von TypeScript und validiert gleichzeitig existierenden Code. Alles, was es dafür braucht, sind die Optionen allowJs (zur Überprüfung von JavaScript-Code) sowie lib (zur Festlegung der Sprachumgebung) und target (zur Festlegung der Zielsprache). Die Beispielkonfiguration tsconfig.json gestaltet sich wie folgt:

{
"compilerOptions": {
"allowJs": true,
"lib": ["dom", "es6"],
"noEmit": true,
"target": "es5"
}
}

Legt man die Datei im Hauptverzeichnis ab, lässt sich mit tsc eine erste Überprüfung allen JavaScript-Codes ab dem Hauptverzeichnis ausführen. Allerdings wird der TypeScript-Compiler noch nichts zu beanstanden haben, da man den eigenen JavaScript-Code zuerst mit einem // @ts-check-Kommentar am Anfang der jeweiligen Datei aufrüsten muss, damit TypeScript die JavaScript-Codezeilen auswerten kann. Die Auswertung lässt sich dann in den eigenen Build-Prozess (beispielsweise in Form eines "npm-Skripts") mit aufnehmen. Hier das Beispiel AuthService.js:

// @ts-check

class AuthService {
/**
* @param {string} accessToken - Bearer token
*/
constructor(accessToken) {
this.accessToken = accessToken;
}
}

const service = new AuthService('1234');

Noch bevor man mit der TypeScript-Migration beginnt, sollte man das eigene Projekt auf Kompatibilität überprüfen. Eventuelle externe Abhängigkeiten sollten nach Möglichkeit auch über Typdefinitionen verfügen, damit man diese reibungslos im TypeScript-Ökosystem benutzen kann. Ob ein JavaScript-Projekt eine Typdefinition hat, lässt sich daran erkennen, ob eine index.d.ts genannte Datei im Hauptverzeichnis enthalten ist.

Alternativ kann es einen Eintrag zu einer Typdefinition in Form einer types- oder typings-Eigenschaft (ältere Schreibweise) im Projektmanifest (üblicherweise package.json genannt) geben. Sollte beides nicht zutreffen, kann nach einer externen Typendefinition über Microsofts TypeSearch oder das DefinitelyTyped Repository gesucht werden. Letztlich bleibt auch die Möglichkeit, selbst eine Typdefinition für externe Abhängigkeiten zu schreiben.

Großprojekte enthalten meist eine Vielzahl an Quellcode-Dateien. Diese sind je nach Aufgabenbereich mehr oder weniger komplex. Um mit der TypeScript-Migration zu starten, sollte man zuerst die Dateien mit den wenigsten Importierungen auflisten. TypeScript überprüft nämlich nicht nur die Code-Datei selbst, sondern auch deren Referenzen. Gute Kandidaten für die erste Migrierungsphase sind Hilfsfunktionen. Alternativ kann man als Grundregel aufstellen, dass neue Code-Dateien in TypeScript geschrieben werden, denn mit TypeScript lassen sich JavaScript- und TypeScript-Dateien simultan einbinden und benutzen.

Falls nicht schon erledigt, sollte man vor der Migration umfangreiche Modultests erstellen, um sicherzustellen, dass der Code nach der Migration noch inhaltlich korrekt ist. Diese Tests können weiterhin in JavaScript bestehen bleiben und mit der bisherigen Testinfrastruktur ausgeführt werden. TypeScript-Code wird nämlich zu JavaScript-Code kompiliert und als eben solcher ausgeführt. In den Tests ist demnach nur darauf zu achten, dass der kompilierte TypeScript-Code aus dem Ausgabepfad eingebunden wird. Dieser Pfad lässt sich in der tsconfig.json-Datei über die Kompilierungsoption outDir bestimmen. Die Standardeinstellung speichert kompilierte Dateien im selben Ort ab wie die TypeScript-Quelldateien.

Hat man die Dateien mit den geringsten Widerständen identifiziert, lässt sich mit der Migration zu TypeScript beginnen. Um ein grobes Gefühl für den Arbeitsaufwand zu bekommen, kann man einfach eine JavaScript-Dateiendung in .ts umbenennen und den TypeScript-Compiler (tsc) mit ausgeschalteter noEmit-Option starten. Sollte es bei diesem Schritt zu Problemen mit der Typdefinition externer Abhängigkeiten kommen, lässt sich für den Anfang der Typ auf any setzen, wodurch keine genauere Typisierung stattfindet. Dieser Schritt hilft zum schnellen Voranschreiten und ist für das erste Grundgerüst ratsam. Auf lange Sicht sollte man jedoch jedes Vorkommen von any kritisch betrachten, da es eben den größten Vorteil von TypeScript aushebelt.

Ist die erste TypeScript-Kompilierung erfolgreich, lässt sich der Compiler dazu anleiten, bei der Kompilierung genauer hinzusehen. Zuerst sollte man ungewollte any-Typisierungen loswerden. Wird im TypeScript-Code eine Variable angelegt und nicht zeitgleich zugewiesen, verwendet der Compiler implizit den Typ any. Um dieses Verhalten zu unterbinden, sollten Programmierer die Option noImplicitAny anschalten. Weitere empfehlenswerte Überprüfungen, inklusive des Checks für any, lassen sich gesammelt über die Option strict in der Datei tsconfig.json aktivieren.

TypeScript beherrscht außerdem Analyseaufgaben, die typischerweise Linter übernehmen, wie die Überprüfung auf ungenutzte Parameter (noUnusedParameters) oder nicht zugewiesene Klasseneigenschaften (strictPropertyInitialization). Diese Regeln lassen sich schrittweise im eigenen Projekt einführen. Eine strikte Konfiguration sieht dann wie folgt aus:

{
"compilerOptions": {
"allowJs": true,
"lib": ["dom", "es6"],
"noEmit": false,
"noImplicitReturns": true,
"noEmitOnError": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"target": "es5"
}
}

Um die Coderegeln von TypeScript zu erweitern, kann das Programmierwerkzeug TSLint eingeführt werden. Es dient in erster Linie zur Wahrung der Form, sodass Code, den verschiedene Entwicklern schreiben, auch konsistent formatiert wird. Das hilft vor allem bei der Durchsicht von Pull Requests, da der Linter eine einheitliche Formatierung gewährleistet und so nur die Stellen markiert, die sich fachlich geändert haben. Die Bestrebungen des TSLint-Teams gehen dahin, dass Regeln von TSLint (für TypeScript) und ESLint (für JavaScript) zusammengelegt werden, sodass man einen bestehenden Regelsatz aus seinem JavaScript-Projekt weiterhin verwenden kann.

Die aktuelle Version von TypeScript bildet eine Obermenge vom gängigen JavaScript, weshalb sich JavaScript- in TypeScript-Code importieren lässt. Es ist aber auch möglich, TypeScript- in JavaScript-Code zu importieren. Dafür braucht es allerdings einen Erstellungsprozess, den man über webpack steuert. Das Werkzeug ist in der Lage, bei der Projekterstellung alle Vorkommnisse von TypeScript in JavaScript umzuwandeln und die jeweiligen Referenzen im JavaScript-Projekt aufzulösen. Für die Umwandlung ist webpack mit dem Kompilierungswerkzeug Babel zu verknüpfen, was eine Installation der jeweiligen Werkzeuge erfordert:

yarn add --dev webpack webpack-cli @babel/core babel-loader @babel/preset-typescript

Zusätzlich ist eine webpack-Konfiguration im Projektverzeichnis anzulegen (webpack.config.js):

module.exports = {
entry: {
myproject: './src/index.js',
},
mode: 'development',
module: {
rules: [
{
exclude: /(node_modules)/,
loader: 'babel-loader',
test: /\.[tj]sx?$/,
},
],
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
};

Ebenfalls erforderlich ist eine Basiskonfiguration von Babel (babel.config.js):

module.exports = {
plugins: [],
presets: ['@babel/preset-typescript'],
};

Mitsamt den gezeigten Einstellungen lässt sich nun über die webpack-Kommandozeile unter Aufruf von webpack ein Paket erzeugen, das in dist/myproject.js abgespeichert wird. Alle JavaScript-Dateien, die ausgehend vom webpack-Einstiegspfad (src/index.js) gefunden werden, können nun TypeScript-Code importieren. Diese Funktionen sind besonders gewinnbringend, wenn man neu erstellten TypeScript-Code im JavaScript-Projekt einführen und publizieren möchte.