In den folgenden Kapiteln stelle ich Ihnen TypeScript vor, gebe Ihnen einen Überblick über die Funktionsweise des TypeScript-Compilers (TSC) und zeige Ihnen, welche Fähigkeiten TypeScript besitzt und welche Muster Sie damit entwickeln können. Wir beginnen mit dem Compiler.
Je nachdem, welche Programmiersprache(n) Sie bereits benutzt haben (d.h., bevor Sie dieses Buch gekauft und sich für ein Leben in Sicherheit entschieden haben), haben Sie vermutlich ein anderes Verständnis davon, wie Programme funktionieren. Im Vergleich zu anderen beliebten Sprachen wie JavaScript oder Java funktioniert TypeScript eher ungewöhnlich. Daher ist es sinnvoll, erst einmal auf dem gleichen Stand zu sein, bevor wir weitermachen.
Beginnen wir ganz allgemein: Programme sind Dateien, die von Ihnen – den Programmierern – geschriebenen Text enthalten. Der Text wird von einem speziellen Programm namens Compiler untersucht, interpretiert (»geparst«) und in einen abstrakten Syntaxbaum (»abstract syntax tree«, AST) umgewandelt. Das ist eine Datenstruktur, die z.B. Leerzeichen, Kommentare und Ihre Meinung zur »Leerzeichen oder Tabs«-Debatte ignoriert. Danach konvertiert der Compiler den AST in eine niedriger angesiedelte (lower level) Form namens Bytecode. Diesen Bytecode können Sie dann einem Programm namens Runtime (oder Laufzeitumgebung) übergeben, das den Bytecode auswertet und das Ergebnis zurückgibt. Wenn Sie ein Programm ausführen, weisen Sie also tatsächlich die Laufzeitumgebung an, den Bytecode auszuführen, den der Compiler aus dem AST erzeugt hat, nachdem er diesen aus Ihrem Quellcode geparst hat. Die Details können sich unterscheiden, sind aber für die meisten Hochsprachen gleich oder zumindest ähnlich.
Noch einmal, die Schritte sind:
Eine Besonderheit von TypeScript ist, dass es nicht direkt in Bytecode kompiliert wird, sondern nach ... JavaScript-Code! Diesen können Sie dann wie üblich in Ihrem Browser, mit NodeJS oder manuell auf dem Papier ausführen (Letzteres nur für den Fall, dass Sie dies erst nach dem Aufstand der Maschinen lesen).
Sehr wahrscheinlich fragen Sie sich jetzt: »Moment mal! Im vorigen Kapitel haben Sie gesagt, dass TypeScript meinen Code sicherer macht. An welcher Stelle passiert das denn jetzt?«
Gute Frage: Den wichtigen Schritt habe ich übersprungen: Nachdem der TypeScript-Compiler den AST für Ihr Programm erzeugt, aber bevor es den Code ausgibt, führt er einen Typecheck (bitte merken Sie sich dieses Wort!) für Ihren Code aus.
Typechecker
Ein spezielles Programm, das sicherstellt, dass Ihr Code typsicher ist.
Das Typechecking ist die wahre Magie hinter TypeScript. So stellt TypeScript sicher, dass Ihr Programm wie erwartet funktioniert und es keine offensichtlichen Fehler gibt und dass der/die hübsche Barista von gegenüber Sie auch wirklich zurückruft. (Haben Sie etwas Geduld. Er/sie ist vermutlich bloß gerade sehr beschäftigt.)
Wenn wir das Typechecking und die Ausgabe von JavaScript mit einbeziehen, sieht die Kompilierung von TypeScript ungefähr so aus wie in Abbildung 2-1:
Abbildung 2-1: TypeScript kompilieren und ausführen
Die Schritte 1–3 werden von TSC übernommen, die Schritte 4–6 werden von der JavaScript-Runtime ausgeführt, die in Ihrem Browser, NodeJS oder einer anderen von Ihnen verwendeten JavaScript-Engine lebt.
JavaScript-Compiler und -Runtimes werden oft zu einem gemeinsamen Programm namens Engine kombiniert. Das ist das Ding, mit dem Sie als Programmierer normalerweise interagieren. So funktionieren beispielsweise V8 (die Engine hinter NodeJS, Chrome und Opera), SpiderMonkey (Firefox), JSCore (Safari) und Chakra (Edge). Sie geben JavaScript das Aussehen einer interpretierten Sprache. |
In den Schritten 1 und 2 werden dabei die Typen Ihres Programms benutzt, in Schritt 3 jedoch nicht. Das darf man ruhig noch mal wiederholen: Wenn TSC Ihren Code von TypeScript nach JavaScript kompiliert, erfolgt dies ohne Prüfung der Typen.
Die Typen in Ihrem Programm haben also keinen Einfluss auf die von Ihrem Programm erzeugten Ausgaben und werden nur für das Typechecking verwendet. Dadurch ist es quasi narrensicher, mit den Typen Ihres Programms herumzuspielen, sie zu aktualisieren und zu verbessern, ohne dass Ihre Applikation dabei versehentlich Schaden nehmen könnte.
Alle modernen Sprachen besitzen das eine oder andere Typsystem.
Typsystem
Ein Satz von Regeln, die der Typechecker verwendet, um Typen in Ihrem Programm zuzuweisen.
Allgemein gibt es zwei Arten von Typsystemen: solche, in denen Sie dem Compiler in expliziter Syntax mitteilen müssen, welche Typen die Dinge haben, und Typsysteme, die Typen automatisch ableiten. Beide Ansätze haben ihre Vor- und Nachteile.1
TypeScript ist von beiden Arten der Typsysteme inspiriert: Sie können Ihre Typen explizit annotieren oder Sie können die meisten Typen automatisch ableiten lassen.
Um TypeScript Ihre Typen explizit mitzuteilen, verwenden Sie Annotationen. Diese haben die Form Wert: Typ und sagen dem Typechecker: »Der Typ dieses Werts lautet Typ. Am besten sehen wir uns hierzu ein paar Beispiele an (in den Kommentaren zu jeder Zeile sehen Sie die tatsächlich von TypeScript abgeleiteten Typen):
let a: number = 1 // a ist eine Zahl
let b: string = 'hello' // b ist ein String
let c: boolean[] = [true, false] // c ist ein Array mit booleschen Werten
Wollen Sie, dass TypeScript die Typen für Sie ableitet, können Sie die Annotationen weglassen und TypeScript die Arbeit erledigen lassen:
let a = 1 // a ist eine Zahl
let b = 'hello' // b ist ein String
let c = [true, false] // c ist ein Array mit booleschen Werten
Man erkennt schnell, wie gut TypeScript Typen für Sie ableiten kann. Auch wenn Sie die Annotationen weglassen, bleiben die Typen gleich! In diesem Buch werden wir Annotationen nur bei Bedarf verwenden. Ansonsten überlassen wir es nach Möglichkeit TypeScript, die Ableitungen für uns vorzunehmen.
Allgemein gilt es als guter Programmierstil, TypeScript so viele Typen wie möglich automatisch ableiten zu lassen und so wenig explizit typisierten Code wie möglich zu verwenden. |
Als Nächstes werfen wir einen genaueren Blick auf das Typsystem von TypeScript und sehen, welche Unterschiede und Gemeinsamkeiten im Vergleich zum Typsystem von JavaScript bestehen. Einen Überblick sehen Sie in Tabelle 2-1. Ein gutes Verständnis dieser Unterschiede ist der Schlüssel zum Verständnis der Funktionsweise von TypeScript.
Tabelle 2-1: Ein Vergleich der Typsysteme von JavaScript und TypeScript
Typsystem-Merkmal |
JavaScript |
TypeScript |
Wie werden Typen begrenzt (bounding)? |
Dynamisch |
Statisch |
Werden Typen automatisch konvertiert? |
Ja |
Nein (meistens) |
Wann werden Typen überprüft? |
Zur Laufzeit |
Bei der Kompilierung |
Zu welchem Zeitpunkt werden Fehler ausgelöst? |
Zur Laufzeit (meistens) |
Bei der Kompilierung (meistens) |
JavaScripts dynamische Typbindung bedeutet, dass es Ihr Programm ausführen muss, um die darin verwendeten Typen zu ermitteln. JavaScript kennt Ihre Typen vor der Ausführung des Programms nicht.
TypeScript ist eine graduell typisierte Sprache. Dadurch funktioniert TypeScript am besten, wenn es die Typen aller Dinge in Ihrem Programm bereits bei der Kompilierung kennt. Damit das Programm kompiliert werden kann, muss aber nicht zwingend alles bekannt sein. Selbst in einem nicht typisierten Programm kann TypeScript einige Typen für Sie ableiten und Fehler abfangen. Ohne dass alle Typen bekannt sind, ist es aber fast unvermeidlich, dass einige Fehler es bis zu Ihren Benutzern schaffen.
Diese graduelle Typisierung ist besonders nützlich, wenn Legacy-Code von untypisiertem JavaScript zu typisiertem TypeScript migriert werden muss (mehr hierzu in »Schrittweise Migration von JavaScript zu TypeScript« auf Seite 240). Grundsätzlich sollten Sie versuchen, eine hundertprozentige Typabdeckung zu erzielen – es sei denn, Sie befinden sich gerade mitten in der Migration einer Codebasis. Dieser Ansatz wird auch in diesem Buch umgesetzt. In Ausnahmefällen weise ich ausdrücklich darauf hin.
JavaScript ist schwach typisiert. Das heißt, wenn Sie etwas Ungültiges tun wie etwa die Addition einer Zahl mit einem Array (siehe Kapitel 1), wird eine Reihe von Regeln angewandt, um herauszufinden, was Sie tatsächlich gemeint haben. So wird versucht, das Beste aus dem übergebenen Code zu machen. Sehen wir uns am Beispiel von 3 + [1] einmal genau an, wie JavaScript vorgeht:
Das könnten wir auch expliziter schreiben (sodass JavaScript die Schritt 1, 3 und 4 vermeidet):
3 + [1]; // ergibt "31"
(3).toString() + [1].toString() // ergibt ebenfalls "31"
JavaScript versucht, Ihnen durch seine intelligente Typumwandlungen zu helfen; TypeScript beschwert sich dagegen, sobald Sie etwas Ungültiges tun. Wenn Sie den gleichen JavaScript-Code an TSC übergeben, erhalten Sie eine Fehlermeldung:
3 + [1]; // Error TS2365: Operator '+' cannot be applied
// to types '3' and 'number[]'.
(3).toString() + [1].toString() // ergibt "31"
Sobald Sie etwas tun, das nicht korrekt erscheint, beschwert sich TypeScript. Wenn Sie Ihre Absichten dagegen explizit erklären, steht es Ihnen aber auch nicht im Weg. Das erscheint sinnvoll: Niemand im Vollbesitz seiner geistigen Kräfte würde versuchen, eine Zahl und ein Array miteinander zu addieren, und als Ergebnis einen String erwarten (abgesehen vielleicht von der JavaScript-Hexe Bavmorda, die ihre Zeit damit verbringt, im Licht schwarzer Kerzen im Keller ihres Start-ups Dämonenbeschwörungen zu programmieren).
Fehler, die durch diese Art impliziter Typumwandlungen in JavaScript verursacht werden, lassen sich oft nur schwer finden und sind für viele JavaScript-Programmierer kein Spaß. Sie erschweren einzelnen Entwicklern die Arbeit und machen es noch schwerer, den Code für ein größeres Team zu skalieren. Schließlich muss jeder Programmierer verstehen, von welchen impliziten Annahmen Ihr Code ausgeht.
Kurz gesagt: Wenn Sie eine Typumwandlung vornehmen müssen, tun Sie das explizit.
Meistens ist es JavaScript egal, welche Typen Sie ihm übergeben. Stattdessen versucht es, das Übergebene so gut wie möglich in das umzuwandeln, was Sie eigentlich erwarten.
Im Gegensatz dazu überprüft TypeScript Ihre Typen während der Kompilierungsphase (erinnern Sie sich an Schritt 2 am Anfang dieses Kapitels?). Sie müssen den Code also nicht erst laufen lassen, um den Error aus dem vorigen Beispiel zu sehen. TypeScript führt eine statische Analyse Ihres Codes durch, um Fehler wie diese zu finden, und sagt Bescheid, bevor der Code ausgeführt wird. Wird der Code nicht kompiliert, heißt das ziemlich sicher, dass Sie einen Fehler gemacht haben, den Sie vor der Ausführung beheben sollten.
Abbildung 2-2 zeigt, was passiert, wenn ich das letzte Codebeispiel in VSCode (den Codeeditor meiner Wahl) eingebe.
Abbildung 2-2: Von VSCode ausgegebener TypeError
Wenn Ihr bevorzugter Codeeditor eine gute TypeScript-Erweiterung besitzt, wird der Fehler bereits bei der Eingabe (mit einer roten Unterschlängelung) deutlich hervorgehoben. Die Rückmeldung kommt bereits beim Schreiben des Codes, und Sie können den Fehler sofort beheben.
In JavaScript werden Ausnahmen und implizite Typumwandlungen zur Laufzeit ausgelöst bzw. vorgenommen.2 Das heißt, Sie müssen Ihr Programm erst ausführen, damit Sie eine nützliche Rückmeldung darüber erhalten, ob Sie etwas Ungültiges getan haben. Im besten Fall passiert das als Teil eines Unit Tests, schlimmstenfalls bekommen Sie eine unfreundliche E-Mail von einem Benutzer.
TypeScript löst sowohl Syntax- als auch Typ-bezogene Fehler während der Kompilierungsphase aus. Dadurch werden Fehler wie diese schon bei der Eingabe in Ihrem Codeeditor angezeigt. Wenn Sie noch nie mit einer inkrementell kompilierten, statisch typisierten Sprache gearbeitet haben, kann das eine großartige Erfahrung sein.3
Trotzdem gibt es eine Reihe von Fehlern, die auch TypeScript nicht während der Kompilierung abfangen kann. Hierzu gehören Stack-Überläufe, unterbrochene Netzwerkverbindungen und falsch formatierte Benutzereingaben. Ausnahmen wie diese werden zur Laufzeit ausgelöst. Allerdings kann TypeScript die meisten Fehler, die in einer reinen JavaScript-Welt erst zur Laufzeit auftreten, so umwandeln, dass sie schon bei der Kompilierung erkannt werden.
Nachdem Sie einen ersten Eindruck des TypeScript-Compilers und des Typsystems bekommen haben, wollen wir Ihren Codeeditor einrichten, damit wir möglichst schnell mit echtem Code arbeiten können.
Beginnen Sie mit dem Download eines Codeeditors, in dem Sie Ihren Code schreiben. Ich persönlich mag VSCode, weil dieser Editor besonders gut für die Arbeit mit TypeScript geeignet ist. Andere Editoren wie Sublime Text, Atom, Vim, Web-Storm oder ein anderer Editor Ihrer Wahl funktionieren aber auch. Programmierer sind bei der Wahl ihrer IDE sehr eigen, daher überlasse ich Ihnen diese Entscheidung. Wenn Sie VSCode verwenden wollen, folgen Sie den Installationsanweisungen auf der Website (https://code.visualstudio.com/).
TSC selbst ist ein Kommandozeilenprogramm, das in TypeScript geschrieben ist. Das heißt, Sie benötigen NodeJS, um den Compiler auszuführen. Folgen Sie den Anweisungen auf der offiziellen NodeJS-Website (https://nodejs.org), um es auf Ihrem Rechner zum Laufen zu bringen.4
Zu NodeJS gehört NPM, ein Paketmanager, mit dem Sie die Abhängigkeiten Ihrer Projekte verwalten und den Build-Prozess steuern können. Wir beginnen mit der Installation von TSC und TSLint (einem Linter für TypeScript). Öffnen Sie hierfür Ihr Terminal, erstellen Sie einen neuen Ordner und initialisieren Sie darin NPM:
# Einen neuen Ordner anlegen
mkdir chapter-2
# Ein neues NPM-Projekt initialisieren (folgen Sie den Anweisungen)
npm init
# TSC, TSLint und die Typendeklarationen für NodeJS TSC installieren
npm install --save-dev typescript tslint @types/node
Für jedes TypeScript-Projekt gibt es in dessen Wurzelverzeichnis eine Datei namens tsconfig.json. In tsconfig.json wird beispielsweise definiert, welche Dateien kompiliert werden sollen, in welches Verzeichnis sie kompiliert werden sollen und welche JavaScript-Version ausgegeben werden soll.
Erstellen Sie im Wurzelverzeichnis Ihres Projekts eine neue Datei namens tsconfig.json (touch tsconfig.json). Danach können Sie die Datei in Ihrem Codeeditor öffnen und Folgendes eingeben:5
{
"compilerOptions": {
"lib": ["es2015"],
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"strict": true,
"target": "es2015"
},
"include": [
"src"
]
}
Werfen wir zunächst einen Blick auf die verschiedenen Optionen und ihre Bedeutung (Tabelle 2-2):
Option |
Beschreibung |
include |
In welchen Ordnern soll TSC nach TypeScript-Dateien suchen? |
lib |
Welche APIs soll TSC in der Umgebung erwarten, in der Sie Ihren Code ausführen? Hierzu gehören Dinge wie ES5s Function.prototype.bind, ES2015s Object.assign und der DOM-eigene document. querySelector. |
module |
Für welches Modulsystem soll TSC Ihren Code kompilieren (CommonJS, SystemJS, ES2015 etc.)? |
outDir |
In welchem Ordner soll TSC den erzeugten JavaScript-Code speichern? |
Bei der Überprüfung auf ungültigen Code sollten Sie so strikt wie möglich sein. Diese Option erzwingt, dass Ihr gesamter Code korrekt typisiert sein muss. Wir benutzen diese Einstellung für alle Beispiele dieses Buchs. Sie sollten das Gleiche für Ihre TypeScript-Projekte tun. |
|
target |
In welche JavaScript-Version soll TSC Ihren Code kompilieren (ES3, ES5, ES2015, ES2016 etc.)? |
Dies sind nur ein paar der in tsconfig.json verfügbaren Optionen. Tatsächlich gibt es noch Dutzende weiterer Optionen, und ständig kommen neue hinzu. In der Praxis werden Sie hier nur wenige Änderungen vornehmen müssen. Die meisten Anpassungen beziehen sich auf die Einstellung für module and target, falls Sie eine neuere Modulverwaltung nutzen wollen, und das Hinzufügen von "dom" zu lib, wenn Sie TypeScript für den Browser schreiben wollen (mehr dazu in Kapitel 12), oder die Anpassung für strict, wenn Sie bereits vorhandenen JavaScript-Code nach TypeScript migrieren wollen. Eine vollständige und aktuelle Liste aller unterstützten Optionen finden Sie in der offiziellen Dokumentation auf der TypeScript-Website (http://bit.ly/2JWfsgY).
Die Verwendung von tsconfig.json ist praktisch, weil Sie die TSC-Konfigurierung in eine Versionsverwaltung mit aufnehmen können. Sie können die meisten TSC-Optionen aber auch von der Kommandozeile aus vornehmen. Eine Liste der verfügbaren Kommandozeilen-Optionen erhalten Sie durch Eingabe des Befehls ./node_ modules/.bin/tsc --help.
Ihr Projekt sollte außerdem eine tslint.json-Datei enthalten, die die Konfigurierung von TSLint enthält. Hier wird festgelegt, welche stilistischen Konventionen Sie in Ihrem Code verwenden wollen (Tabs oder Leerzeichen etc.).
Auch wenn die Verwendung von TSLint optional ist, rate ich dringend dazu, sie für alle TypeScript-Projekte zu benutzen, um einen konsistenten Programmierstil durchzusetzen. Noch wichtiger: Auf diese Weise vermeiden Sie während der Code-Reviews endlose Diskussionen mit Ihren Kollegen über den Programmierstil. |
Folgender Befehl erzeugt eine tslint.json-Datei, die eine TSLint-Standardkonfiguration enthält:
./node_modules/.bin/tslint --init
Danach können Sie die Regeln überschreiben, um sie an Ihren eigenen Programmierstil anzupassen. Meine tslint.json-Datei sieht beispielsweise so aus:
{
"defaultSeverity": "error",
"extends": [
],
"rules": {
"semicolon": false,
"trailing-comma": false
}
}
Eine vollständige Liste der verfügbaren Regeln finden Sie in der TSLint-Dokumentation (https://palantir.github.io/tslint/rules/). Sie können auch eigene Regeln oder eigene Presets hinzufügen (zum Beispiel für ReactJS (https://www.npmjs.com/package/tslint-react)).
Nachdem die Dateien tsconfig.json und tslint.json vorbereitet sind, können Sie einen src-Ordner anlegen. Dieser enthält Ihre erste TypeScript-Datei:
mkdir src
touch src/index.ts
Die Ordnerstruktur Ihres Projekts sollte nun so aussehen::
chapter-2/
├--node_modules/
├--src/
│ └--index.ts
├--package.json
├--tsconfig.json
└--tslint.json
Öffnen Sie src/index.ts in Ihrem Codeeditor und geben Sie folgende TypeScript-Anweisung ein:
console.log('Hello TypeScript!')
Danach können Sie den TypeScript-Code kompilieren und ausführen:
# TypeScript mit TSC kompilieren
./node_modules/.bin/tsc
# Den Code mit NodeJS ausführen
node ./dist/index.js
Wenn Sie alle hier genannten Schritte korrekt befolgt haben, sollten Sie in der Konsole jetzt einen einzelnen Log-Eintrag sehen:
Hello TypeScript!
Das war schon alles. Sie haben gerade von Grund auf Ihr erstes TypeScript-Projekt eingerichtet und ausgeführt. Herzlichen Glückwunsch!
Vermutlich ist dies das erste Mal, dass Sie ein TypeScript-Projekt von Anfang an eingerichtet haben. Daher habe ich Ihnen jeden einzelnen Schritt gezeigt, damit Sie verstehen, wie alles zusammengehört. Für diese Schritte gibt es eine Reihe von Abkürzungen, mit denen es beim nächsten Mal schneller geht:
|
Nachdem Ihre Umgebung eingerichtet ist, können Sie nun src/index.ts in Ihrem Codeeditor öffnen und folgenden Befehl eingeben:
let a = 1 + 2
let b = a + 3
let c = {
apple: a,
banana: b
}
let d = c.apple * 4
Wenn Sie jetzt den Cursor über a, b, c und d bewegen, sehen Sie, wie TypeScript die Typen aller Variablen für Sie ableitet (Typinferenz): a ist eine Zahl (number), b ist ist eine Zahl (number), c ist ein Objekt mit einer bestimmten Form, und d hat ebenfalls den Typ number (Abbildung 2-3).
Abbildung 2-3: Automatische Typableitung (Inferenz) durch TypeScript
Spielen Sie ein wenig mit dem Code:
Wenn Sie immer noch nicht genug haben, versuchen Sie, ein Stück Code zu schreiben, für den TypeScript den Typ nicht selbst ermitteln kann.