Typsicher und komfortabel mit TypeScript

TypeScript erweitert JavaScript um ein statisches Typsystem. Die Programmiersprache lässt sich nahtlos in JavaScript-Projekten verwenden.

In Pocket speichern vorlesen Druckansicht 13 Kommentare lesen

(Bild: Shutterstock)

Lesezeit: 19 Min.
Von
  • Nils Hartmann
Inhaltsverzeichnis

Durch die rasante Verbreitung immer komplexerer Anwendungen im Browser hat die Sprache JavaScript in den letzten Jahren einen wahren Boom erlebt. Dass sie kein statisches Typsystem besitzt, kann allerdings zu Laufzeitfehlern und unwartbarem Code führen. Diese Lücke schließt die Sprache TypeScript, die auf JavaScript aufbaut und ein mächtiges und flexibles statisches Typsystem zur Verfügung stellt.

Microsoft hat die Sprache entwickelt und setzt sie unter anderem für Office365 und VS Code ein. Der Erfinder von TypeScript ist Anders Hejlsberg, der auch als Vater von C# und Turbo Pascal gilt. Der Sourcecode der Programmiersprache steht unter der Apache-Lizenz.

In einem Interview im Jahr 2018 hat Rod Johnson, der Erfinder des Spring-Frameworks für Java gesagt, TypeScript sei für ihn die "zurzeit wichtigste Sprache" mit einem "massiven Wachstum". Laut Johnson sei es zwar möglich, eine gänzlich neue "ideale" Programmiersprache zu entwickeln, die "50 mal besser" als bisherige sei, aber man müsse sich fragen, wie einfach deren Einführung ist. Auf der anderen Seite könne man auch eine Sprache erschaffen, die nur zweimal besser als eine bestehende Sprache ist, dafür aber einfach zu adaptieren. Dieser Kandidat sei TypeScript.

Damit spielt Johnson auf ein zentrales Prinzip von TypeScript an. Demnach sollte die Sprache mit und in bestehenden JavaScript-Projekten jederzeit funktionieren. Im optimalen Fall solle es reichen, das Projekt mit TypeScript zu ergänzen und Schritt für Schritt die Typsicherheit hinzuzufügen. TypeScript soll der Migration nicht im Weg stehen und im Zweifel sogar Code mit Typfehlern akzeptieren, damit während einer Migrationsphase immer gewährleistet ist, dass sich die Anwendung weiterhin ausführen lässt.

Die Strategie scheint aufgegangen zu sein: Die großen Webframeworks Angular, Vue und Svelte sind mittlerweile in TypeScript implementiert, und auch React bringt TypeScript-Support mit. Viele populäre Bibliotheken wie Jest und Redux sind auf TypeScript portiert und alle verbreiteten Editoren und IDEs kennen die Sprache.

Damit IDEs und Editoren übergreifend und einfach TypeScript-Support anbieten können, bringt die Programmiersprache einen lokalen Language Server für das Language Server Protocol (LSP) mit. Er arbeitet unabhängig vom Werkzeug und parst den Code. Unter anderem untersucht er, ob der Code in Ordnung ist oder welche Fehler er enthält. Außerdem kann er Refactorings durchführen. Eine IDE, die TypeScript-Support anbieten möchte, muss somit lediglich den Language Server einbinden und das Ergebnis darstellen. Dadurch verhalten sich nahezu alle IDEs im Zusammenspiel mit TypeScript identisch, inklusive Fehlermeldungen und Refactoring-Funktionen. Außerdem können sie zum Release einer neuen TypeScript-Version unmittelbar die neuen Features anbieten.

TypeScript ist eine Obermenge von JavaScript. Jeder gültige JavaScript-Code ist somit gültiger TypeScript-Code, aber die Programmiersprache fügt neue Syntax-Konstrukte hinzu, insbesondere für Typannotation.

Folgendes Listing zeigt exemplarisch die dynamische Natur des JavaScript-Typsystems. Es deklariert eine Variable, die zunächst zur Laufzeit den Typ string annimmt. Durch das Zuweisen einer Zahl ist sie später vom Typ number, danach wird sie zu einer Funktion und schließlich undefined. Der Typ einer Variablen lässt sich zur Laufzeit mit dem typeof-Operator ermitteln.

let person = "Susi";      
console.log(typeof person); // Ausgabe: "string"
console.log(person.toUpperCase()); // Ausgabe: SUSI

person = 32;              
console.log(typeof person); // Ausgabe: "number"
console.log(person + 1); // Ausgabe: 33

person = function() { return "Kate" } 
console.log(typeof person); // Ausgabe: "function"
console.log(person()); // Ausgabe: Kate

person = undefined;
console.log(typeof person); // Ausgabe: "undefined"

Das Beispiel zeigt syntaktisch einwandfreien JavaScript-Code, der zur Laufzeit funktioniert, da eine Variable ihren Typ ständig dynamisch anpassen kann: JavaScript hat ein dynamisches Typsystem. Die Flexibilität hat ihren Preis, denn beim Betrachten eines Codeausschnittes lässt sich der Typ einer Variable zur Laufzeit schwer bestimmen. Zwar helfen einige Werkzeuge dabei, die aber ebenfalls je nach Code an ihre Grenzen stoßen.

Die Funktion sayHello im folgenden Codebeispiel erwartet genau einen Parameter, aber ohne in die Implementation zu schauen, lässt sich nicht ohne Weiteres erkennen, von welchem Typ. Da in JavaScript keine Prüfung der Typen stattfindet, ist der Code der Funktion syntaktisch korrekt und lässt sich ausführen. Allerdings würde beim zweiten Aufruf der Funktion im Listing ein Laufzeitfehler auftreten, weil sie sich zwar grundsätzlich mit einer Zahl aufrufen lässt, aber für den Datentyp number keine toUpperCase-Funktion definiert ist.

function sayHello(person) {
  return "Hello, " + person.toUpperCase(); 
} 

sayHello("Klaus"); // OK
sayHello(7); // Fehler in sayHello: 
             // TypeError: person.toUpperCase 
             // is not a function

Die Wahrscheinlichkeit, dass das dynamische Typsystem zu Fehlern führt, wächst mit der Größe der Anwendung. Die Analyse von Code ist aufwendig, und das automatische Refactoring ist oft gar nicht oder nur risikobehaftet möglich.

Viele Programmiersprachen verwenden ein statisches Typsystem. Dabei erhält eine Variable beim Anlegen einen Typ, der entweder manuell festgesetzt ist oder den die Sprache ermittelt. Einmal festgelegt, ändert sich der Typ nie mehr. Die Typinformation können Tools wie Compiler beim Entwickeln oder Bauen der Anwendung überprüfen. TypeScript verfolgt diesen Ansatz. Der im ersten Listing gezeigte Code würde in TypeScript einen Compile-Fehler verursachen, obwohl er keine explizite Typzuweisung enthält.

Sonderheft "Programmiersprachen – Next Generation"

Dieser Artikel stammt aus dem neuen iX-Developer-Sonderheft "Programmiersprachen – Next Generation". Es beschäftigt sich auf 156 Seiten schwerpunktmäßig mit den Sprachen TypeScript, Kotlin, Rust und Go. Daneben wirft es einen Blick auf aktuelle Entwicklungen bei Java, C++ und der Programmierung von Quantencomputern.

Die PDF-Ausgabe des Sonderhefts ist zum Preis von 12,99 Euro im heise-shop verfügbar. Die gedruckte Ausgabe lässt sich für 14,90 Euro bestellen – bis zum 15. September ohne Versandkosten. Daneben bietet der heise Shop ein Bundle aus gedruckter Ausgabe plus PDF für 19,90 Euro an. Das das Heft ist zudem in gut sortierten Kiosken und Buchhandlungen verfügbar.

TypeScript kann häufig den Typ einer Variablen beim Anlegen über Typinferenz ermitteln. Für den Beispielcode kann TypeScript für die Variable person den Typ string herleiten und wirft in den folgenden Zuweisungen auf eine number beziehungsweise function jeweils Compile-Fehler aus. Da in TypeScript undefined und null zwei eigene Typen sind, würde die letzte Zeile im ersten Listing ebenfalls einen Compile-Fehler verursachen. Die IDE kann beim Entwickeln den hergeleiteten Typ anzeigen (siehe Abbildung 1).

Ein Tooltip im Editor zeigt den hergeleiteten TypeScript-Typ für person an (Abb. 1).

Wenn die automatische Herleitung eines Typs nicht funktioniert, lässt sich der Typ explizit hinter dem Namen der Variable angeben:

let person: string = "Susi";
console.log(person.toUpperCase()); // Ausgabe: SUSI
person = undefined; // Fehler: Type 'undefined' is not 
                    // assignable to type 'string'

Das manuelle Setzen eines Typs kann sinnvoll sein, wenn beispielsweise die Variable person nicht nur den Typ string, sondern weitere Typen wie null oder undefined annehmen darf. Dafür existiert der sogenannte Union Type, der mehrere Typen zusammenfasst. Der folgende Code erlaubt es, der person-Variable neben einer Zeichenkette die Typen null oder undefined zuzuweisen. Die Union-Type-Definition lässt sich folgendermaßen lesen: person ist string oder null oder undefined.

let person: string | null | undefined = "Susi";

console.log(person.toUpperCase()); // Ausgabe: SUSI
person = undefined; // OK
person = null;      // OK
person = "Klaus";   // OK

TypeScript kann nicht nur den Typ von Variablen herleiten, sondern auch den Rückgabetyp von Funktionen. Die folgende Funktion 5 liefert über return einen String zurück. Damit kennt TypeScript den Rückgabetyp. Der Typ der Variable g wird bei der Zuweisung ebenfalls zu string, auf dem der Aufruf von toUpperCase erlaubt ist.

function greet() {
  return "Hello" 
}  

let g = greet(); 
g.toUpperCase(); // OK g ist ein String

Die Typen der Funktionsparameter kann TypeScript nicht herleiten. Der Code muss für jeden Parameter eine Typannotation aufweisen. Die Syntax ist identisch mit der Typannotation an Variablen:

function greet(person: string) {
return "Hello, " + person.toUpperCase()
}

greet("Susi"); // OK
greet(null); // Fehler: Argument of type 'null' is not
             // assignable to parameter of type 'string'