App-Entwicklung mit JavaScript, Teil 3: Aber bitte mit Typen

ÜberKreuz  –  2 Kommentare
Anzeige

Im letzten Teil unserer Serie zu JavaScript ging es um die Evolution der Skriptsprache durch die Ausgaben ECMAScript 2015 und 2016. Diese brachten viele schmerzlich vermisste Konstrukte endlich auch ins Web. Klassische Anwendungsentwickler, die meist über statisch typisierte Sprachen wie Java, C# oder C++ ins Web kommen, vermissen aber meist die statische Typisierung. Wo JavaScript nicht helfen kann, eilt Microsoft mit TypeScript zur Hilfe.

App-Entwicklung mit JavaScript

Gerade im Bereich der Anwendungsentwicklung kann statische Typisierung hilfreich sein. JavaScript lässt Entwickler mit seiner dynamischen Typisierung und erzwungenen Typumwandlung hier im Stich. Hier hilft TypeScript, eine Übermenge von JavaScript, die den kompletten Sprachumfang von JavaScript unterstützt und um weitere Konstrukte erweitert. Das sind neben der optionalen Verwendung von Typen zum Beispiel Zugriffsmodifikatoren, Generics, Schnittstellen oder erfolgversprechende Vorschläge für zukünftige Ausgaben von JavaScript wie die async/await-Operatoren oder Dekoratoren. Designer von TypeScript ist übrigens kein Geringerer als Anders Hejlsberg, der sich bereits für die .NET-Sprache C# und vorher auch für Delphi verantwortlich zeichnete – beides Programmiersprachen mit statischer Typisierung.

Um die Funktionsweise von TypeScript zu verdeutlichen, bediene ich mich eines kurzen Beispiels aus der Objektorientierung. Ich definiere eine Schnittstelle Motor, die von einer konkreten Implementierung erfüllt werden muss: Jeder Motor hat eine Typbezeichnung, die als String hinterlegt wird, und kann angelassen werden. Das wird als Methode abgebildet, die per booleschem Wert zurückgibt, ob das Anlassen erfolgreich war. Die Definition von Typen ist in TypeScript als Opt-in realisiert. Wird kein Typ definiert, kann das Feld oder die Variable alle möglichen Werte entgegennehmen beziehungsweise im Falle einer Methode zurückliefern. Andernfalls erfolgt die Typdefinition jeweils nach der Deklaration des Bezeichners, getrennt durch einen Doppelpunkt:

interface Motor {
typ: string;
anlassen(): boolean;
}

class DieselMotor implements Motor {
public typ: string = "1.8 l TDI";

public anlassen(): boolean {
return true;
}
}

Die Klasse DieselMotor zeigt die Implementierung der Schnittstelle Motor: Würde man versuchen, dem Feld typ einen numerischen Wert zuzuweisen, reklamiert der Sprachservice von TypeScript diesen semantischen Fehler. Der Service ist jedoch nur eine Hilfe für die integrierte Entwicklungsumgebung und Entwickler. Der eigentliche TypeScript-Compiler wird die Zuweisung dennoch transpilieren, da sie gültiges JavaScript darstellt. Auf den semantischen Fehler weist der Compiler aber hin. Da Dieselmotoren natürlich immer anspringen, gebe ich der Einfachheit halber in der anlassen-Methode immer true zurück. Bringe ich nun die abstrakte Klasse Fahrzeug ins Spiel, da der Motor ja auch irgendetwas antreiben muss.

abstract class Fahrzeug {
protected abstract _räder: number;

constructor(protected _motor: Motor) {
if (!_motor.anlassen()) {
throw new Error("Motor springt nicht an!");
}
}

public fahre(): void {
console.log('Ich fahre mit meinen ${this._räder} Rädern!');
}
}

Ganz analog zu statisch typisierten Sprachen können wir hier einfach das abstract-Schlüsselwort verwenden. Die Klasse definiert das abstrakte Feld _räder, die durch die ableitende Klasse implementiert werden muss. Der Konstruktor erfordert einen Motor als Parameter, der durch Verwendung des protected-Schlüsselworts im Konstruktor automatisch als gleichnamige Eigenschaft für sich selbst und alle ableitenden Klassen verfügbar gemacht wird. Auch die Zugriffsmodifikatoren sind ein TypeScript-Feature, das allein von TypeScript überwacht wird. Im späteren JavaScript-Quellcode kann dennoch jede Property ungeachtet der Modifikatoren geändert werden. Wird in TypeScript kein Modifikator angegeben, ist der jeweilige Member daher implizit "public". Kann der Motor nicht angelassen werden, wird ein Fehler geworfen. Die Methode fahre hingegen gibt unter Zuhilfenahme des aus ECMAScript 2015 bereits bekannten Template-Literals aus, auf wie vielen Rädern das Fahrzeug fährt. Nun benötigt man eine konkrete Implementierung:

class Auto extends Fahrzeug {
protected _räder: number = 4;
private _name: string;

public set name(value: string) {
this._name = value.toUpperCase();
}

public get name(): string {
return this._name;
}
}

Die Klasse Auto definiert das Feld _räder und weist diesem den Wert 4 zu. Zudem werden hier noch Getter und Setter für Eigenschaften gezeigt, die bereits Einzug in ECMAScript 2015 gehalten haben. Setzt man den Namen des Autos über den Setter, wird dieser automatisch in Großbuchstaben umgewandelt. Der so entstehende Quellcode erinnert doch stark an das, was Entwickler aus C#, Java oder C++ kennen. Bringen wir das Beispiel also zum Laufen – beziehungsweise zum Fahren:

const dieselMotor: Motor = new DieselMotor();
const auto: Auto = new Auto(dieselMotor);

auto.fahre();
auto.name = "golf";
console.log(auto.name);

Die Ausgabe in der JavaScript-Konsole lautet erwartungsgemäß:

Ich fahre mit meinen 4 Rädern!
GOLF

Das ist zugegebenermaßen ein eher einfaches Beispiel. TypeScript erlaubt aber beispielsweise auch die Verwendung generischer Typen oder der auf Promises basierenden async/await-Schlüsselwörter. Das nachfolgende Beispiel, wie es in einem Service zum Abruf von Daten durchaus zum Einsatz kommen könnte, erinnert stark an C#-Code, sodass sich .NET-Entwickler auch im Web schnell heimisch fühlen dürften:

Codevervollständigung ist dank der Typinformation kein Problem
public async getItem(id: number): Promise<Data> {
return httpDownloader.download(`https://example.com/?id=${id}`);
}

// const data: Data = await downloader.downloadData(1);

Die Typinformationen können dazu genutzt werden, um eine Codevervollständigung à la IntelliSense anzubieten. Editoren wie Visual Studio, Visual Studio Code oder WebStorm machen Gebrauch hiervon.

TypeScript zaubert aus dem obigen Beispiel das hier verlinkte JavaScript. Es fällt auf, dass das Motor-Interface komplett verschwunden ist. Schnittstellen werden bei der Transpilierung komplett entfernt. Ansonsten wird die prototypische Vererbung von JavaScript offenbar, die TypeScript elegant wegkapselt.

Bleibt aber die Frage, wie man das entstehende, doch recht unleserliche JavaScript debuggen kann. Die Antwort hierfür lautet Sourcemaps. Diese erlauben es dem Debugger, Rückschlüsse auf die Zeilenposition im ursprünglichen TypeScript zu ziehen. Im Ergebnis kann man somit TypeScript-Quellcode im Debugger debuggen, obwohl die Ausführungsumgebung TypeScript nicht versteht. Dieser erstklassige Support schließt Breakpoints, Callstacks und Watches mit ein – jeweils mit Zeilenangabe im TypeScript-Quelltext.

Der Einfluss von TypeScript ist schon heute beachtlich. Was vor wenigen Jahren noch unmöglich schien, machen Google und Microsoft derzeit in vorbildlicher Weise vor: Angular, das Framework von Google zur Entwicklung von Single-Page Web Applications (SPA), wird beispielsweise in TypeScript geschrieben. Google plante zunächst die Einführung eines eigenen Supersets über TypeScript namens AtScript. Doch kurzerhand übernahm Microsoft Dekoratoren in den Sprachumfang von TypeScript.

Mit TypeScript kann man also auch statische Typisierung ins Web bringen. Das erlaubt eine stabile Programmierung auch oder gerade von großen Frontend- oder Backendanwendungen. Da JavaScript im Browser oder zum Beispiel auch durch Node.js auch auf dem Server ausgeführt werden kann, ergibt sich zudem die interessante Möglichkeit, ein- und denselben TypeScript- oder JavaScript-Code zwischen Frontend und Backend zu teilen. Diesen Code bezeichnet man dann auch als isomorph oder universell. Den letzten Teil zur JavaScript-Serie gibt es in der kommenden Woche.

Anzeige