6. TypeScript

Dieses Kapitel führt in die elementaren Prinzipien und Bausteine von TypeScript ein. Dabei werden auch die Unterschiede zu JavaScript hervorgehoben.

6.1 Vorbemerkungen

Gültiges JavaScript ist immer auch gültiges TypeScript. Dennoch sollte dies dich nicht dazu verleiten, auf die Vorteile von TypeScript zu verzichten und weiter JavaScript zu schreiben. Die Sprache und dein Code leben wesentlich von der konsequenten Anwendung der Sprachmerkmale.

6.2 Variablen und Konstanten

Globale Variablen können an jeder Stelle angelegt werden, durch Zuweisen eines Wertes. Lokale Variablen werden innerhalb einer Funktion mit Hilfe des Schlüsselwortes var angelegt. Initiale Werte k sofort angeben. Mehrere Variablen können durch Komma getrennt werden.

Scope – der Sichtbarkeitsbereich

In JavaScript haben {Blöcke} (Abschnitte im Code, die in geschweiften Klammern stehen) keinen eigenen Scope. Nur Funktionen (genauer: der Funktionskörper) haben einen Scope. Variablendefinitionen innerhalb einer Funktion sind außerhalb der Funktion nicht sichtbar – sie sind im Funktions-Scope (auch als Gültigkeitsbereich bezeichnet). Dies wird mit dem Schlüsselwort var bezeichnet. In TypeScript kann man auch reine Blöcke, also die Bereiche, die in geschweiften Klammern stehen, als Scope nutzen. Dies wird dann mit dem Schlüsselwort let erreicht.

Das folgende Beispiel gibt “456” aus:

1 var foo = 123;
2 if (true) {
3     var foo = 456;
4 }
5 console.log(foo);

Mit der Benutzung von let wird dagegen 123 ausgegeben:

1 let foo = 123;
2 if (true) {
3     let foo = 456;
4 }
5 console.log(foo);

Konstanten

Für Konstanten gibt es ein eigenes Schlüsselwort: const. Konstanten müssen initialisiert werden.

1 const WIDTH = 123;

Konstanten dürfen auch Objekte enthalten:

1 const CELL = { x: 23, y: 42 };

6.3 TypeScript-Syntax

Die Namen für Variablen und Funktionen beginnen mit einem Buchstaben, _ oder $ (In “Regex-Speak”: [a-z,A-Z,_,$]), gefolgt von keinem oder mehreren Buchstaben, Zahlen, _ oder $. Das $ hat keine spezielle Bedeutung, sollte jedoch generiertem oder Bibliotheks-Code vorbehalten bleiben. Alle Variablen, Parameter oder Member werden klein geschrieben (meinVariablenName). Einzige Ausnahme: Klassen beginnen mit einem großen Buchstaben (MeinKlasse). Das _-Zeichen (Unterstrich) am Beginn eines Schlüsselwortes wird für Implementierungen verwendet (privat per Konvention).

Das einfachste Beispiel, “Hallo TypeScript”, zeigt die Nutzung:

Listing: Hallo TypeScript
1 class Hello {
2     output: string;
3 
4     constructor(){
5         this.output = "Hallo TypeScript";
6     }
7 
8 }

JavaScript-Code kann an den unterschiedlichsten Stellen einer Webseite verwendet werden. JavaScript, das durch Übersetzen aus TypeScript entstanden ist, liegt in aller Regel als Datei oder Sammlung von Dateien vor. Hier wirst du immer auf <script>-Tags zurückgreifen müssen, eine “Inline”-Verarbeitung ist dagegen nicht möglich.

Kommentare

Ein kommentierter Bereich sieht folgendermaßen aus:

1 /*
2 Ein mehrzeiliger Kommentarblock
3 Kann mit den aus C/C++/C#/Java bekannten Blockkommentarzeichen
4 erstellt werden.
5 */

Der Zeilenkommentar steht alleine auf einer Zeile oder am Ende:

1 // Dies tut das:
2 var i = 42; // Zuweisung

Literale

Die folgende Tabelle zeigt Literale für Sonderzeichen:

Tabelle: Sonderzeichen
Zeichen Bedeutung
\b BackSpace
\n NewLine
\t Tab
\f FormFeed
\r CarriageReturn

Ein Beispiel dazu:

1 var s: string ='Eine mehrzeilige\r\nZeichenkette';

Umlaute

Eine Besonderheit ist beim Umgang mit Umlauten zu beachten. Es ist nicht sichergestellt, dass die Ausgabe mit den Sonderzeichen umgehen kann. Aus diesem Grund ist es empfehlenswert, auf die Funktion unescape zurückzugreifen. Dies sieht mit Umlauten folgendermaßen aus:

1 alert("über"));

Schreibe jedoch folgendes, wenn Umlaute benutzt werden:

1 alert(unescape("%FCber"));

Damit stellst du sicher, dass die Umlaute immer korrekt interpretiert und angezeigt werden können.

Numerische Literale

Ferner gibt es noch die numerischen Literale. TypeScript unterscheidet im Wesentlichen in Ganzzahlen (Integer) und Gleitkommazahlen bei den Literalen. Intern werden, wie auch bei JavaScript, nur Gleitkommazahlen abgebildet. Es gibt keine Integer-Datentyp in TypeScript. Ganzzahlen können in folgenden drei Formen vorkommen:

Gleitkommazahlen können in zwei Schreibweisen vorkommen:

Ferner gibt es zur Darstellung von Wahrheitswerten noch boolesche Konstanten:

Sonstige Literale

Reguläre Ausdrücke werden in / (slash) geschrieben. Das Array-Literal [] wird für Arrays und Maps benutzt. Das Objekt-Literal {} erzeugt ein neues, leeres Objekt.

Operatoren

Das Zeichen + dient sowohl der Addition als auch der Verknüpfung von Zeichenketten. Wenn beide Operanden Zahlen sind, werden diese addiert, sonst erfolgt immer eine Umwandlung in Zeichenketten, welche zusammengesetzt werden.

Tabelle: Mathematische Operatoren
Operator Bedeutung Beispiel
+, += Addition x+=3
-, -= Subtraktion x=x-5
*,*= Multiplikation a=b*c
/, /= Division z=e/5
% Modulus m=5 % 3
++, -- Inkrement, Dekrement x++ oder y–
<<, <<= Bitweise Linksschieben x << 4
>>, >>= Bitweise Rechtsschieben y >> 5
>>> Bitweise Linksschieben mit Nullfüllung a >>> b
& Bitweise UND a & b
| Bitweise ODER a | b
^ Bitweise Negieren ^b
!. Nullprüfung let s = e!.name
? Optionalität var s : string?
?: Optionaler Parameter function f(x?: number){}
=> Lambda-Operator (ergibt sich zu) var inc = (i)⇒i+1

Der Lambda-Operator ist eine Kurzschreibweise für einen anonymen Funktionsaufruf. Sieh dir folgende typische Funktion an:

1 function Person(age) {
2     this.age = age
3     this.growOld = function() {
4         this.age++;
5     }
6 }

Hier wird eine Funktion growOld erzeugt, die man auch kürzer schreiben kann:

1 function Person(age) {
2     this.age = age
3     this.growOld = () => { this.age++; }
4 }

6.4 Das Typsystem

JavaScript verfügt nur über ein sehr schwaches Typsystem. Oft ist es wünschenswert, hier eine tiefere Kontrolle über eigene Typen zu haben. Dies geht – in bestimmten Grenzen – mit TypeScript. Dabei werden vor allem eigene, selbst definierte, Typen unterstützt. Für die eingebauten skalaren Typen von JavaScript ist keine Erweiterung vorgesehen. So verfügt auch TypeScript nicht über einen Integer-Typ, weil auch JavaScript diesen nicht kennt. Die skalaren Typen werden aber im Sinne der Typsicherheit besser unterstützt.

Typbezeichner in TypeScript werden der Deklaration nachgestellt. Diese sind mit einem Doppelpunkt abgetrennt (“typ” ist hier als Platzhalter gedacht):

1 var bezeichner: typ = zuweisung;
2 function name(): typ {
3 }

Da gültiges JavaScript auch gültiges TypeScript ist, kann der Typbezeichner auch weggelassen werden. In diesem Fall wird der generische Typ any angenommen. Eine gute Idee ist dies im Sinne TypeScript-optimierter Programmierung freilich nur selten. Es dient lediglich dazu, bestehende Bibliotheken mit geringem Aufwand nutzbar zu machen.

Die Basistypen

Grundsätzlich entsprechen die einfachen Typen in TypeScript denen in JavaScript. Dazu gehören:

Eine Sonderstellung ist die Enumeration (Aufzähltyp, ein Art Werteliste), die auf number basiert:

Boolean

Der Typ boolean ist elementar für viele Sprachen und wird wie üblich über die Literale false und true bedient.

1 var isDone: boolean = false;

Die Funktion Boolean(value) ermittelt, ob der Wert true oder false ist. Werte, die als false interpretiert werden, sind:

Alle anderen Werte sind true, einschließlich "0" und "false" (in Anführungszeichen), was manchmal zu Verwirrung führt.

Number

Wie in JavaScript sind Zahlen immer Gleitkommazahlen (float). Der Name des Typs ist number:

1 var height: number = 6;
2 var amount: number = 23.45;

Eine Besonderheit ist der Wert NaN - Not a Number. Dieser zeigt an, dass bei einer Operation keine Zahl ermittelt werden konnte. NaN ist ungleich zu allem, auch zu sich selbst. if (NaN === NaN) ist beispielsweise immer false.

Berechnungen mit Math

Die Funktionen entsprechen denen in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.

Zeichenketten (String)

Die Funktionen entsprechen denen in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.

Array

TypeScript benutzt für Arrays dieselbe Syntax wie JavaScript. Allerdings kann zusätzlich noch festgelegt werden, dass das gesamte Array nur bestimmte Typen enthalten darf (typsicheres oder generisches Array):

1 var list:number[] = [1, 2, 3];

Alternativ kann eine generische Schreibweise wie in C# benutzt werden:

1 var list:Array<number> = [1, 2, 3];

Bei dieser Form wird explizit der Typ Array eingesetzt. Der Typparameter steht in spitzen Klammern.

Einige Methoden für Arrays

Die Funktionen entsprechen denen in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.

Enumerationen (Enum)

Enumerationen gibt es in JavaScript nicht. Dies sind Auflistungen von numerischen Werten (1, 2, 3, …), die im Quelltext durch Zeichenfolgen repräsentiert werden. In TypeScript kann soetwas nativ definiert werden:

1 enum Color {
2     Red, 
3     Green, 
4     Blue
5 };
6 var c: Color = Color.Green;

Ohne weitere Angaben beginnt die Zählung mit 0. Der Startwert kann aber explizit gesetzt werden.

1 enum Color {
2     Red = 1,
3     Green, 
4     Blue
5 };
6 var c: Color = Color.Green;

Vor allem dann, wenn die Werte in einer Datenbank gespeichert werden, ist das explizite Setzen unbedingt notwendig. Nur so kann verhindert werden, dass bei Änderungen am Quellcode dieselben Werte weiter benutzt werden.

1 enum Color {
2     Red = 1, 
3     Green = 2, 
4     Blue = 4
5 };
6 var c: Color = Color.Green;

Bei der Übersetzung des letzten Beispiels nach JavaScript ist zu erkennen, dass der Code nicht mehr ganz trivial ist:

1 var Color;
2 (function (Color) {
3     Color[Color["Red"] = 1] = "Red";
4     Color[Color["Green"] = 2] = "Green";
5     Color[Color["Blue"] = 4] = "Blue";
6 })(Color || (Color = {}));
7 var c = Color.Green;

Der Zugriff mit dem numerischen Wert ist auch möglich. Das kann sinnvoll sein, wenn ein JSON-Objekt angeliefert wird, das lediglich den Zahlwert enthält. Der folgende Code gibt in der Alert-Box “Green” aus:

1 enum Color {
2     Red = 1, 
3     Green, 
4     Blue
5 };
6 var colorName: string = Color[2];
7 
8 alert(colorName);
Enumerationen mit Flags

Manchmal ist es sinnvoll, mehrere Aufzählungswerte kombiniert einzusetzen. Man spricht dann von Flags. Stelle dir ein Command-Pattern vor, indem Zustände von Kommandos behandelt werden. Dies sind beispielsweise:

Nun kann ein Kommando vier Zustände haben. Statt vier werden nur zwei Aufzählwerte benötigt, die kombiniert eingesetzt werden. Damit das funktioniert, müssen sie Werte aber numerisch widerspruchsfrei sein. Man kann das am Besten mit Bitwerten erreichen (0000, 0001, 0010, 0011). Für das Beispiel wäre dann Selected gleich “01” und Enabled “10”.

1 enum State {
2     Selected = 1,
3     Enabled = 1 << 1,
4     Checked = 1 << 2
5 };

Der Shift-Operator schiebt die Eins um ein Stelle nach links. Das ist bequemer zu schreiben als den kompletten Binärwert. Alternativ kannst du natürlich auch die 2er-Potenzreihe nutzen (1, 2, 4, 8, 16 etc.).

Bei der Nutzung müssen die Bitstellen wieder extrahiert werden. Dazu werden Bit-Operatoren benutzt:

 1 let state : State = State.Selected;
 2 
 3 if (state & State.Checked) {
 4   // Der Wert "Checked" wurde exklusiv gesetzt
 5 }
 6 if (state & State.Checked) === State.Checked){
 7   // Der Wert "Checked" wurde gesetzt
 8 }
 9 // Setze "Checked" und lasse alle anderen unberührt
10 state |= State.Checked;
11 // Setze "Checked" zurück und lasse alle anderen unberührt
12 state &= ~State.Checked;

Und hier folgt eine Übersicht:

Mit diesen Techniken lassen sich einzelne Werte verändern, ohne die anderen zu beeinflussen. Beim direkten Zuweisen der Werte ohne Bit-Operatoren würden die anderen Bitstellen sonst überschrieben werden. Sollen zwei oder mehr Werte zugleich gesetz werden, so können diese mit dem &-Operator kombiniert werden.

1 state = State.Checked & State.Enabled; 

Willst du die komplette Reihe mit 0 beginnend nutzen, ist folgende systematische Schreibweise empfehlenswert:

1 enum StateFlags {
2     None           = 0,
3     Enabled        = 1 << 0,
4     Selected       = 1 << 1,
5     Checked        = 1 << 2,
6     Valued         = 1 << 3
7 }
Enums erweitern

Durch die Art der Definition in JavaScript ist es möglich, Enumerationen zu erweitern. Dies erfolgt einfach durch erneute Definiton desselben Namens. Die bis dahin im Skript bereits definierten Elemente bleiben erhalten.

 1 enum Color {
 2     Red,
 3     Green,
 4     Blue
 5 }
 6 
 7 // In einer anderen Datei:
 8 enum Color {
 9     DarkRed = 3,
10     DarkGreen,
11     DarkBlue
12 }

Freilich ist es sinnvoll, hier die Startwerte zu bestimmen – oder noch besser gleich alle numerischen Werte.

Konstante Enumerationen

Ein Spezialfall sind Enums mit dem Schlüsselwort const:

1 const enum Tristate {
2     False   = 1,
3     True    = 2,
4     Unknown = 0
5 }
6 
7 let f = Tristate.False;

Die Variable f wird im JavaScript nun folgendermaßen aussehen:

1 var f = 1;

Die gesamte Deklaration der Enumeration entfällt und der Wert wird an der Stelle, wo er benutzt wird, durch eine Konstante ersetzt. Der Vorteil ist offensichtlich ein Performance-Gewinn. Der Nachteil ist der Verlust der Lesbarkeit des Codes im generierten JavaScript, was vor allem beim Debuggen auffällt.

Statische Enumerationsfunktionen

In Sprachen wie C# gibt es statische Hilfsfunktionen, die eine Basisklasse Enum bereitstellt. Dies kann in TypeScript mit dem Schlüsselwort namespace simuliert werden.

 1 enum Weekday {
 2     Monday,
 3     Tuesday,
 4     Wednesday,
 5     Thursday,
 6     Friday,
 7     Saturday,
 8     Sunday
 9 }
10 namespace Weekday {
11     export function isWeekend(day: Weekday) {
12         switch (day) {
13             case Weekday.Saturday:
14             case Weekday.Sunday:
15                 return true;
16             default:
17                 return false;
18         }
19     }
20 }
21 
22 const mon = Weekday.Monday;
23 const sun = Weekday.Sunday;
24 console.log(Weekday.isWeekend(mon));
25 console.log(Weekday.isWeekend(sun));

Durch den Eingriff in den Typ Weekend wird eine Funktion angefügt. In JavaScript ist der Enum-Typ dann der umgebende Funktionsname und in einer Funktion kann natürlich eine weitere Funktion existieren. Da Enumerationen nicht instanziiert werden, ist die Funktion statisch.

Any

Any ist ein universeller Typ, bei dem TypeScript keine weitere Typprüfung vornimmt. Der Typ ist schwächer als das var in C#. An den benutzten Stellen verhält sich TypeScript dann wieder wie JavaScript. Das ist sinnvoll, wenn Daten in einem unbekannten oder wechselnden Format angeliefert werden. Davon machen viele JavaScript-Bibliotheken Gebrauch.

1 var notSure: any = 4;
2 notSure = "Eine Zeichenkette";
3 notSure = false;

Any ist immer dann sinnvoll, wenn Teile der Applikation noch in JavaScript geschrieben wurden und ein Datenaustausch notwendig ist und die Daten in sich variabel sind. Im Grunde wird damit das Verhalten von JavaScript erreicht und die Typprüfung beim Übersetzen deaktiviert.

1 var list:any[] = [1, true, "free"];
2 list[1] = 100;
Void

Der Typ void bezeichnet “keinen Typ” (nichts) und ist eine elegantere Repräsentation von undefined in JavaScript. Es handelt sich hier um einen Funktionstyp, eine Nutzung mit Variablen ist nicht möglich, weil eine Variable die nichts enthalten darf sinnlos ist.

1 function warnUser(): void {
2     alert("Dies ist eine Warnung");
3 }

Bei Funktionen wird damit sichergestellt, dass die Funktion nicht “versehentlich” etwas zurückgeben kann.

Abbildung: Fehler im Editor, weil `return` mit `void` nicht erlaubt ist
Abbildung: Fehler im Editor, weil return mit void nicht erlaubt ist
Never

Sehr speziell ist der Typ never. Damit ausgedrückt, dass eine Funktion nicht zurückkehrt:

1 function error(message: string): never {
2     throw new Error(message);
3 }

Der Rückgabewert wird implizit benutzt, wenn der Rückgabewert nicht dem Standardpfad der Codeausführung entspricht:

1 function move1(direction: "up" | "down") {
2     switch (direction) {
3         case "up":
4             return 1;
5         case "down":
6             return -1; 
7     }
8     return error("Should never get here");
9 }

Beim Aufruf mit “left” als Parameter wird hier never zurückgegeben.

Der Typ never wurde mit TypeScript 2.0 eingeführt.

null und undefined

Auch mit der Verfügbarkeit von void kommst du nicht umhin, dich mit den impliziten Typen für undefinierte Zustände in JavaScript auseinanderzusetzen. Andernfalls wäre es mit der Abwärtskompatibilität von TypeScript vorbei. Dazu gibt es auch in TypeScript die beiden besonderen Typen null und undefined.

Seit TypeScript 2.0 werden diese Typen explizit behandelt – vorher war es eher in der Art “egal”. Die Prüfung kann im Transpiler über den Schalter --strictNullChecks aktiviert werden. Damit ist die Zuweisung zu einem beliebigen Typen nicht mehr möglich, ausgenommen natürlich any.

 1 let x: number;
 2 let y: number | undefined;
 3 let z: number | null | undefined;
 4 x = 1;  // Ok
 5 y = 1;  // Ok
 6 z = 1;  // Ok
 7 x = undefined;  // Fehler
 8 y = undefined;  // Ok
 9 z = undefined;  // Ok
10 x = null;  // Fehler
11 y = null;  // Fehler
12 z = null;  // Ok
13 x = y;  // Fehler
14 x = z;  // Fehler
15 y = x;  // Ok
16 y = z;  // Fehler
17 z = x;  // Ok
18 z = y;  // Ok

Du musst damit jeder Variablen einen Typ zuweisen, oder alternativ explizit undefined. Optionale Parameter, die nicht belegt sind, werden implizit undefined annehmen.

1 type T1 = (x?: number) => string;              
2 type T2 = (x?: number | undefined) => string;  

In dem vorstehenden Beispiel sind T1 und T2 gleich, denn der optionale Parameter x? kann undefined sein. Die Angabe bei T2 ist nicht erforderlich.

Wenn auf Funktionen zugegriffen wird, dann prüft der Transpiler, ob der Wert nicht eventuell null oder undefined ist. Wenn aber eine explizite Typprüfung im Code erfolgt, lässt er den Zugriff zu:

1 declare function f(x: number): string;
2 let x: number | null | undefined;
3 if (x) {
4     f(x);  // Passt
5 }
6 else {
7     f(x);  // Fehler
8 }

Der erste Teil des if-Ausdrucks ist zulässig, weil durch die Prüfung klar ist, das x nur vom Typ number sein kann. JavaScript evaluiert null und undefined zu false.

Symbole

Symbole sind private primitive Datentypen. Diese werden mit Hilfe eines Symbol-Konstruktors erzeugt. Symbole sind immer eindeutig und immer unveränderlich.

1 let sym1 = Symbol();
2 
3 let sym2 = Symbol("key");

Der Schlüssel (Zeile 3) ist optional. In der folgenden Definition sind sym1 und sym2 nicht gleich:

1 let sym1 = Symbol("key");
2 let sym2 = Symbol("key");

Symbole sind eher wie Zeichenketten einzuordnen. So kannst du als Eigenschaftenbezeichner dienen:

1 let sym = Symbol();
2 
3 let obj = {
4     [sym]: "value"
5 };
6 
7 console.log(obj[sym]);

Diese sind auch dort einsetzbar, wo literale Bezeichner erwartet werden:

 1 const TheClassName = Symbol();
 2 
 3 class C {
 4     [TheClassName](){
 5        return "C";
 6     }
 7 }
 8 
 9 let c = new C();
10 let className = c[TheClassName](); // "C"
Interne Symbole

Einige interne Symbole dienen der vereinfachten Benutzung interner Funktionen.

Mehr dazu findest du bei MDN – Mozilla Developer Network.

6.5 Anweisungen – Statements

Folgende Anweisungen stehen als Schlüsselwörter zur Verfügung:

Kontrollstrukturen

Dies Anweisungen entsprechen denen in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.

Fehlerbehandlung

Die Vorgehensweise entspricht der in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.

6.6 Schnittstellen

TypeScript hat den Vorteil, dass eigene Typen definiert werden können. Dies erfolgt über Schnittstellen. Die Schnittstelle beschreibt einen Typ und gibt diesem Typ einen Namen. Aus anderen Sprachen sind für dieses Konzept Klassen oder Strukturen gedacht. Aufgrund der Dynamik der Sprache JavaScript wäre der Aufwand hier verhältnismäßig hoch und damit laufzeitrelevant.

Der Schnittstellen-Implementierung liegt in TypeScript das Konzept “duck typing” zugrunde. Dabei werden die Attribute eines Objekts allein durch das Vorhandensein von bestimmten Eigenschaften und Methoden beschrieben.

Implizite Implementierung

Ein erstes Beispiel zeigt die Anwendung mit impliziter Definition. Dabei wird einem Parameter eine Typbeschreibung mitgegeben:

1 function printLabel(typeWithLabel: {label: string}) {
2   console.log(typeWithLabel.label);
3 }
4 
5 var labelType = {
6     size: 10,
7     label: "Beschreibung"
8 };
9 printLabel(labelType);

Die Typprüfung setzt auf die Beschreibung {label: string} auf. Dies ist die zu erfüllende Schnittstelle. Werden weitere Mitglieder übergeben, wie hier size, so spielt dies keine Rolle.

Explizite Implementierung

Neben der impliziten Form kann die Schnittstelle auch explizit festgelegt werden. Dies erfolgt durch das Schlüsselwort interface. Eine Implementierungsanweisung wie bei C# (Typ : interface) ist dagegen nicht erforderlich.

 1 interface SomeType {
 2   label: string;
 3 }
 4 
 5 function printLabel(val: SomeType) {
 6   console.log(val.label);
 7 }
 8 
 9 var labelType = {
10     size: 10, 
11     label: "Beschreibung"
12 };
13 printLabel(labelType);

Der Vorteil der expliziten Definition besteht in der Möglichkeit, die Angabe mehrfach verwenden können. Darüberhinaus lassen sich die Beschreibungen der Schnittstelle zentral als separate Datei ablegen.

Die Reihenfolge der Mitglieder innerhalb der Schnittstelle spielt keine Rolle. Es wird lediglich die Existenz und der skalare Typ geprüft.

Optionale Eigenschaften

Die hohe Dynamik von JavaScript ist ein zentrales Merkmal der Sprache. Typstrenge Sprachen fühlen sich dagegen sehr starr an. Beides hat Vor- und Nachteile. In TypeScript können Schnittstellen optionale Eigenschaften enthalten, um diese Dynamik zu erhalten. Damit wird der Umfang der Mitglieder festgelegt, es kann jedoch davon bei Bedarf abgewichen werden. Die Angabe erfolgt durch ein nachgestelltes (Suffix) Fragenzeichen.

 1 interface SquareConfig {
 2   color?: string;
 3   width?: number;
 4 }
 5 
 6 function createSquare(config: SquareConfig)
 7   : {color: string; area: number} {
 8   var newSquare = {
 9     color: "white", 
10     area: 100
11   };
12   if (config.color) {
13     newSquare.color = config.color;
14   }
15   if (config.width) {
16     newSquare.area = config.width * config.width;
17   }
18   return newSquare;
19 }
20 
21 var mySquare = createSquare({color: "black"});

Hier wird die explizite Schnittstelle in Zeile 6 benutzt, während der Rückgabewert der Funktion createSquare eine implizite Darstellung nutzt (Zeile 7). Der Sinn optionaler Mitglieder besteht darin, Tippfehler zu vermeiden. Wird das Objekt benutzt, also beispielsweise config.color in Zeile 13, so erfolgt eine Prüfung. Schreibe hier config.clr (‘clr’ ist hier ein Beispiel für eine nicht vorhandene Eigenschaft), wird der Compiler einen Fehler melden.

Schnittstellen erweitern

Schnittstellen können erweitert werden. Dadurch ist eine weitere Modularisierung möglich. Benutzt wird dazu das Schlüsselwort extends.

 1 interface Shape {
 2     color: string;
 3 }
 4 
 5 interface Square extends Shape {
 6     sideLength: number;
 7 }
 8 
 9 var square = <Square>{};
10 square.color = "blue";
11 square.sideLength = 10;

Wird eine Schnittstelle direkt mit dem Objektliteral benutzt, so wird der Typ in spitze Klammern geschrieben (Zeile 9 im vorherigen Beispiel). Es handelt sich hier quasi um die Erstellung eines neuen Objekts vom Typ der Schnittstelle. Eine Klasse benötigst du dazu nicht.

Es ist auch möglich, mehrere Schnittstellen zugleich zu implementieren:

 1 interface Shape {
 2     color: string;
 3 }
 4 
 5 interface PenStroke {
 6     penWidth: number;
 7 }
 8 
 9 interface Square extends Shape, PenStroke {
10     sideLength: number;
11 }
12 
13 var square = <Square>{};
14 square.color = "blue";
15 square.sideLength = 10;
16 square.penWidth = 5.0;

Funktionstypen

In den letzten Beispielen wurde bereits eine implizite Schnittstelle für einen Funktionstyp benutzt. Dies ist die normale Vorgehensweise. Es muss dazu keine Schnittstelle eingesetzt werden. Nun ist die Lesbarkeit eines solchen Konstrukts recht bedenklich. Du kannst deshalb die ganze Funktion in einer Schnittstelle definieren. Im einfachsten Fall sieht dies folgendermaßen aus:

1 interface SearchFunc {
2   (source: string, subString: string): boolean;
3 }

Damit wird die Signatur inklusive des Rückgabetyps festgelegt. Das Schlüsselwort function wird nicht benutzt, wenn der Typ der Schnittstelle die Funktion vollständig beschreibt. Einmal definiert, wird diese Schnittstelle wie zuvor eingesetzt.

 1 var mySearch: SearchFunc;
 2 mySearch = function(source: string, subString: string) {
 3   var result = source.search(subString);
 4   if (result == -1) {
 5     return false;
 6   }
 7   else {
 8     return true;
 9   }
10 }

Wird ein Funktionsname benutzt, handelt es sich um ein untergeordnetes Mitglied (Zeile 3 bzw. 8):

1 interface SearchFunc {
2   (source: string, subString: string): boolean;
3   count() : number;
4 }
5 
6 var s: SearchFunc;
7 s("von", "bis");
8 var n = s.count();

Die Methode muss nicht zwingend benannt sein, wie in Zeile 2 gezeigt. Die Namen der Parameter der Methode sind nicht relevant, um die Signatur zu erfüllen. Typ und Reihenfolge sind hier wichtig. Der folgende Code erfüllt die Schnittstelle ebenso gut:

 1 var mySearch: SearchFunc;
 2 mySearch = function(src: string, sub: string) {
 3   var result = src.search(sub);
 4   if (result == -1) {
 5     return false;
 6   }
 7   else {
 8     return true;
 9   }
10 }

Die Prüfung erfolgt schrittweise, Typ für Typ, weshalb die Reihenfolge hier passen muss.

Union-Typen

Sind Typen definiert, so lassen sich daraus Kombinationstypen erstellen. Dies erfolgt mit type:

 1 interface Square {
 2     kind: "square";
 3     size: number;
 4 }
 5 
 6 interface Rectangle {
 7     kind: "rectangle";
 8     width: number;
 9     height: number;
10 }
11 
12 interface Circle {
13     kind: "circle";
14     radius: number;
15 }
16 
17 type Shape = Square | Rectangle | Circle;

Der Typ Shape kann seinen internen konkreten Typ mit kind zurückgeben:;

1 function area(s: Shape) {
2   switch (s.kind) {
3     case "square": return s.size * s.size;
4     case "rectangle": return s.width * s.height;
5     case "circle": return Math.PI * s.radius * s.radius;
6   }
7 }

Der Union-Typ wurde in TypeScript 2.0 eingeführt.

Array-Typen

Ebenso wie bei Funktionen kann die Typbeschreibung für Arrays benutzt werden. Array-Elemente lassen sich über einen Index adressieren und können Elemente von einem beliebigen Typ enthalten, wobei dann alle Elemente denselben Typ haben. Als Index sind die Typen number und string möglich, als Element ist jeder Typ erlaubt.

Im folgenden Beispiel wird ein Array benutzt, dessen Elemente über number adressiert werden und das Elemente vom Typ string enthält.

1 interface StringArray {
2   [index: number]: string;
3 }
4 
5 var myArray: StringArray;
6 myArray = ["Joerg", "Matthias"];

Du kannst nun auf das Array mit myArray[0] zugreifen und erhalten ‘Joerg’ als Antwort.

Die Indizierung mit string und number ist parallel möglich. Allerdings muss der Wert, der von dem numerischen Index zurückgegeben wird, ein Untertyp des Typs sein, den der Zeichenkettenindex zurückgibt.

Diese Form ist meist ausreichend, hat aber auch Beschränkungen. So ist folgendes nicht möglich:

1 interface Dictionary {
2   [index: string]: string;
3   length: number;    // Fehler
4 } 

Hier wird erwartet, dass length einen Typ benutzt, der vom Typ des Indexers abstammt. Der benutzte Typ number stammt aber nicht von string.

Hybride Schnittstellen

Hybride Schnittstellen beschreiben sowohl Eigenschaften als auch Funktionen und deren Parameter. Du kannst auch untergeordnete Mitglieder haben:

 1 interface Counter {
 2     (start: number): string;
 3     interval: number;
 4     reset(): void;
 5 }
 6 
 7 var c: Counter;
 8 c(10);
 9 c.reset();
10 c.interval = 5.0;

Beachte in dem oben gezeigten Beispiel, dass hier keine Implementierung vorliegt und der Code keine sinnvolle Funktion hat. Die Schnittstelle schreibt hier lediglich vor, wie die mögliche Implementierung auszusehen hat.

6.7 Klassen

Klassen und damit traditionelle Vererbungskonzepte sind in JavaScript normalerweise kein Thema. Funktionen und prototypische Vererbung sind sehr eigene und flexible Konzepte, die bei konsequenter Anwendung auch für komplexere Aufgaben durchaus geeignet sind. Viele Entwickler haben jedoch Schwierigkeiten, JavaScript korrekt zu schreiben, wenn es um objektorientierte Lösungswege geht. Das liegt vor allem daran, dass Umsteiger schwer akzeptieren, dass JavaScript sich sehr grundlegend von anderen Sprachen unterscheidet und nur äußerliche Ähnlichkeiten aufweist.

Mit ECMAScript 6 (ES 6) wird deshalb ein class-Schlüsselwort eingeführt, dass die Umsetzung objektorientierter Konzepte erleichtert. Dies ändert übrigens nichts am internen Verhalten und die Natur von JavaScript bleibt weiter prototypisch. Es handelt sich lediglich um sogenannten syntaktischen Zucker – Vereinfachungen der Syntax.

TypeScript nimmt den Ansatz aus ES 6 voraus und bietet Klassen direkt an. Die Umsetzung erfolgt durch den Transpiler mit dem generischen, prototypischen Vererbungsverfahren, dass JavaScript direkt beherrscht. Das solltest du im Hinterkopf behalten, da bei bestimmten Szenarien das Verhalten doch nicht vollständig dem klassischer objektorientierter Sprachen entspricht.

Zuerst ein Beispiel, wie eine Klasse definiert werden kann:

 1 class Greeter {
 2 
 3   constructor(message: string) {
 4       this.greeting = message;
 5   }
 6 
 7   greet() {
 8       return "Hallo " + this.greeting;
 9   }
10 
11   greeting: string;
12 }
13 
14 var greeter = new Greeter("Joerg");

Diese Klasse hat ein Feld greeting, eine Methode greet() und einen Konstruktur – insgesamt also drei Mitglieder. Wird auf Mitglieder intern zugegriffen, erfolgt dies mit this.

Instanzen werden mit dem Operator new erzeugt. Die Parameter sind die im Konstruktor definierten. Der Konstruktor wird immer conctructor genannt.

Vererbung

Ein fundamentales Konzept objektorientierter Sprachen ist die Vererbung. Bestehende Klassen dienen als Grundlage weiterer Klassen, was Pflege und Nutzung vereinfacht.

In TypeScript erfolgt die Vererbung über das Schlüsselwort extends:

 1 class Animal {
 2     name:string;
 3     constructor(theName: string) { 
 4       this.name = theName; 
 5     }
 6     move(meters: number = 0) {
 7       alert(this.name + " bewegte sich " + meters + " m.");
 8     }
 9 }
10 
11 class Snake extends Animal {
12     constructor(name: string) { super(name); }
13     move(meters = 5) {
14       alert("Schlängeln...");
15       super.move(meters);
16     }
17 }
18 
19 class Horse extends Animal {
20     constructor(name: string) { super(name); }
21     move(meters = 45) {
22       alert("Galoppieren...");
23       super.move(meters);
24     }
25 }
26 
27 var sch = new Snake("Eine Schlange");
28 var pfd: Animal = new Horse("Ein Pferd");
29 
30 sch.move();
31 pfd.move(34);

Hier wird die Klasse Animal als Grundlage für zwei weitere Klassen Horse und Snake benutzt, die die Eigenschaften und Methoden der Basisklasse übernehmen. Dabei können die Mitglieder nicht nur benutzt, sondern auch überschrieben werden – also quasi durch eine angepasste Version ersetzt werden. Dies passiert im Beispiel mit der Methode move. Damit die geerbte Version nicht verloren geht, wird auf die Basisklasse mit super zugegriffen. Innerhalb der Klasse selbst ist dagegen wieder this zu benutzen.

Schutz von Mitgliedern

Eng verbunden mit der Vererbung ist der Schutz von Mitgliedern. JavaScript bietet hier keine explizite Unterstützung. Durch den Sichtbereich (scope) der Variablen innerhalb einer Funktion können Mitglieder jedoch technisch gekapselt werden. Das ist freilich ebenso wenig intuitiv wie die prototypische Vererbung.

TypeScript bietet hier eine Vereinfachung mit Hilfe des Schlüsselworts private an. Öffentlich (public) sind alle Mitglieder standardmäßig. Du kannst aber zur Förderung der Lesbarkeit das Schlüsselwort public benutzen. Den generierten JavaScript-Code ändert dies nicht.

1 class Animal {
2     private name:string;
3     constructor(theName: string) { this.name = theName; }
4     move(meters: number) {
5         alert(this.name + " bewegt sich " + meters + " m.");
6     }
7 }

Klassenmitglieder können auch mit protected geöffnet oder geschützt werden. Das geht auch auf dem Konstruktor. Diese Form erlaubt den Zugriff nur in erbenden Klassen.

 1 class Base {
 2     protected name: string;
 3 }
 4 
 5 class Derived extends Base {
 6     name = "derived";
 7 }
 8 
 9 let c = new Derived();
10 c.name = "test";

Der Zugriff in Zeile 9 (c.name) misslingt, weil name protected ist. Das Feld ist außerhalb der Klasse nicht sichtbar.

Typvergleich

Das Typsystem von TypeScript ist vergleichsweise primitiv. Ein Objekt ist strukturell identisch, wenn alle Mitglieder denselben Typ haben. Das war schon die Grundlage bei den Schnittstellen. Herkunft, Vererbung oder Art der Erstellung spielen keine Rolle. Die Kompatibilität ergibt sich allein aus der Struktur.

Sind private Mitglieder dabei, sieht dies anders aus. Hier wird davon ausgegangen, dass eine Typgleichheit nur besteht, wenn beide Objekte auf derselben Deklaration beruhen. Ein “fremdes” privates Mitglied ist nie mit einem anderen privaten Mitglied kompatibel.

Das folgende Beispiel zeigt, wie dies gemeint ist:

 1 class Animal {
 2     private name:string;
 3     constructor(theName: string) { 
 4       this.name = theName; 
 5     }
 6 }
 7 
 8 class Rhino extends Animal {
 9 	constructor() { super("Rhino"); }
10 }
11 
12 class Employee {
13     private name:string;
14     constructor(theName: string) { 
15       this.name = theName;
16     }	
17 }
18 
19 var animal = new Animal("Goat");
20 var rhino = new Rhino();
21 var employee = new Employee("Bob");
22 
23 animal = rhino;
24 animal = employee; // --> Fehler

In Zeile 24 tritt hier ein Fehler auf, weil die beiden Typen Employee und Animal nicht kompatibel sind. Beide haben zwar ein privates Mitglied name, aber dies ist jeweils nicht sichtbar und deshalb wird es beim Typvergleich nicht mit einbezogen.

Automatische Felder

Die Deklaration eines Felds kann direkt im Konstruktor erfolgen, wenn ihm ein Zugriffsmodifizierer vorangestellt wird:

1 class Animal {
2     constructor(private name: string) { }
3     move(meters: number) {
4         alert(this.name + " moved " + meters + " m.");
5     }
6 }

Mit private wird ein geschütztes Feld erzeugt sowie mit public ein öffentliches. Es hat den Namen des Parameters. Eine explizite Deklaration in der Klasse ist weder notwendig noch möglich.

Eigenschaften

Der Zugriff auf Felder kann mit private geschützt werden. Ist jedoch ein feingranularer Zugriff auf Daten notwendig, werden dazu in den meisten Sprachen Eigenschaften benutzt. JavaScript bietet hier einige Unterstützung durch eine spezielle Deklarationssyntax an. Diese ist jedoch eher seltsam anmutend und wird wenig benutzt. Mit ES 6 wird es eine spezielle vereinfachte Syntax für Eigenschaften geben. Diese Form nimmt TypeScript voraus.

Aus Feldern entstehen Eigenschaften durch explizite Get- und Set-Zweige. Diese regeln das Lesen (get) und Schreiben (set) der Werte in die Mitglieder der Instanz der Klasse.

Ein einfache Klasse ohne derartige Kontrolle kennst du bereits:

1 class Employee {
2     fullName: string;
3 }
4 
5 var employee = new Employee();
6 employee.fullName = "Joerg Krause";
7 if (employee.fullName) {
8     alert(employee.fullName);
9 }

Damit besteht praktisch unkontrollierter Zugriff auf fullName. Die Lösung, um dies zu verhindern, ist eine Eigenschaft. Wie in anderen Sprachen auch handelt sich dabei erneut um einen syntaktischen Kniff – intern handelt es sich um zwei Methoden, die Zugriff auf ein Feld haben. Eine Methode schreibt hinein, eine liest den Wert aus.

In TypeScript werden solche Methoden auch so geschrieben, lediglich die Schlüsselwörter get und set deuten auf die Nutzung als Eigenschaft hin:

 1 var passcode = "secret passcode";
 2 
 3 class Employee {
 4     private _fullName: string;
 5 
 6     get fullName(): string {
 7         return this._fullName;
 8     }
 9 	
10     set fullName(newName: string) {
11         if (passcode 
12          && passcode == "secret passcode") {
13             this._fullName = newName;
14         }
15         else {
16             alert("Error: Unauthorized update of employee!");
17         }
18     }
19 }
20 
21 var employee = new Employee();
22 employee.fullName = "Bob Smith";
23 if (employee.fullName) {
24     alert(employee.fullName);
25 }

Im Beispiel ist der Zugriff auf das private Feld _fullName über die Eigenschaft fullName geschützt. Zeile 22 zeigt, dass die Methoden indirekt benutzt werden – der Zugriff erfolgt wie bei einem Feld.

Eigenschaften können selbst Modizierer wie private haben. Der Einsatz erfolgt auf set und auf get.

Implementierung von Schnittstellen

Schnittstellen wurden bereits benutzt, um Typdeklarationen zu erstellen. Diese dienen jedoch auch – wie in anderen Sprachen – zur Definition einer öffentlichen Fassade einer Klasse. Mit Schnittstellen wird eine bestimmte Implementierung erzwungen.

1 interface ClockInterface {
2     currentTime: Date;
3 }
4 
5 class Clock implements ClockInterface  {
6     currentTime: Date;
7     constructor(h: number, m: number) { }
8 }

In diesem Beispiel muss ein Feld mit dem Namen currentTime und dem Datentyp Date in der Klasse vorhanden sein, weil die Schnittstelle es so fordert. Zusätzliche Mitglieder sind jederzeit möglich. Die Mitglieder können Felder, Eigenschaften oder Methoden sein:

 1 interface ClockInterface {
 2     currentTime: Date;
 3     setTime(d: Date);
 4 }
 5 
 6 class Clock implements ClockInterface  {
 7     currentTime: Date;
 8     setTime(d: Date) {
 9         this.currentTime = d;
10     }
11     constructor(h: number, m: number) { }
12 }

Schnittstellen dienen dazu, eine reine öffentliche Sicht auf die Klasse zu ermöglichen. Die betroffenen Mitglieder sind immer öffentlich. Damit wird auch der Objektvergleich vereinfacht, weil keine Trennung in öffentliche und private Mitglieder erfolgt.

Statische Klassen

Klassen sind Baupläne für Objekte. Die Objekte werden Instanzen genannt. Allerdings kann auch der Bauplan aktiven Code enthalten und direkt benutzt werden. Das ist praktisch, wenn du keine Instanzen benötigst und nur direkt mit einer Struktur arbeiten möchten. Dies ist auch sinnvoll, wenn Mitglieder zwischen Instanzen geteilt werden – also alle Instanzen auf dieselben Mitglieder zugreifen.

1 interface ClockInterface {
2     new (hour: number, minute: number);
3 }
4 
5 class Clock implements ClockInterface  {
6     currentTime: Date;
7     constructor(h: number, m: number) { }
8 }

Wenn eine Klasse eine Schnittstelle implementiert, wird nur die Instanz der Klasse geprüft. Statische Mitglieder werden nicht betrachtet. Der Konstruktor ist ein statisches Mitglied (er muss ja bereits vor der Instanz da sein). Er wird deshalb nicht in die Typprüfung mit einbezogen. Mit statischen Elementen kannst du deshalb immer direkt arbeiten:

 1 interface ClockStatic {
 2     new (hour: number, minute: number);
 3 }
 4 
 5 class Clock  {
 6     currentTime: Date;
 7     constructor(h: number, m: number) { }
 8 }
 9 
10 var cs: ClockStatic = Clock;
11 var newClock = new cs(7, 30);

Die Signatur des Konstruktors wird in der Schnittstelle beschrieben. Dazu wird der Name new benutzt. Aufgrund des strukturellen Typvergleichs entspricht die Signatur von ClockStatic der von Clock. Clock hat lediglich weitere Mitglieder. Der Typ für die Variable cs ist ClockStatic. Die Schnittstelle bestimmt, dass es eine Konstruktorfunktion gibt und diese ist es die der Operator new in Zeile 11 aufruft. Ausgeführt wird dann, weil die Klasse als Objekt zugewiesen wurde (Zeile 10), dann der dazu passende Kosntruktor aus Clock (Zeile 7). Etwas sinnvolles tut das Konstrukt hier nicht, es dient lediglich der Veranschaulichung. Schauen man sich die übersetzte Version an, verschwinden die Typdefinitionen völlig (JavaScript):

1 var Clock = (function () {
2     function Clock(h, m) {
3     }
4     return Clock;
5 }());
6 var cs = Clock;
7 var newClock = new cs(7, 30);

Die Schnittstelle dient nur der Stützung der Typsicherheit, funktional ist sie irrelevant.

Statische Eigenschaften

Neben der Betrachtung der gesamten Klasse als statisch kann dies auch für einzelne Mitglieder erfolgen. Das Schlüsselwort dafür ist, wie üblich, static.

Das folgende Beispiel zeigt, wie sich Instanzen das Feld origin (Zeile 2) teilen.

 1 class Grid {
 2   static origin = {x: 0, y: 0};
 3   calculateDistance(point:  {  
 4                         x: number; 
 5                         y: number;
 6                     }) {
 7     var xDist = (point.x - Grid.origin.x);
 8     var yDist = (point.y - Grid.origin.y);
 9     return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
10   }
11   constructor (public scale: number) { }
12 }
13 
14 var grid1 = new Grid(1.0);  // 1x scale
15 var grid2 = new Grid(5.0);  // 5x scale
16 
17 alert(grid1.calculateDistance({x: 10, y: 10}));
18 alert(grid2.calculateDistance({x: 10, y: 10}));

Wenn ein Objekt Daten in das statische Feld schreibt, gilt dieser Wert in allen Instanzen. Der Zugriff auf das statische Feld erfolgt über den Klassennamen, wie in Zeile 7 und 8 gezeigt (Grid.origin.x).

Konstruktoren

Konstruktoren wurden bei einigen vorangegangenen Beispielen bereits benutzt. Es sind Methoden, die aufgerufen werden, wenn mit new eine Instanz gebildet wird.

 1 class Greeter {
 2     greeting: string;
 3     constructor(message: string) {
 4         this.greeting = message;
 5     }
 6     greet() {
 7         return "Hello, " + this.greeting;
 8     }
 9 }
10 
11 var greeter: Greeter;
12 greeter = new Greeter("world");
13 alert(greeter.greet());

Da JavaScript dies nur über Funktionen abbilden kann, ist der vom Transpiler erzeugte Code weniger intuitiv. In JavaScript sieht es folgendermaßen aus:

 1 var Greeter = (function () {
 2     function Greeter(message) {
 3         this.greeting = message;
 4     }
 5     Greeter.prototype.greet = function () {
 6         return "Hello, " + this.greeting;
 7     };
 8     return Greeter;
 9 })();
10 
11 var greeter;
12 greeter = new Greeter("world");
13 alert(greeter.greet());

Der Rückgabewert des Funktionsmoduls ist die Methode Greeter (Zeile 8 mit Verweis auf Zeile 2). Auf diese wird dann new angewendet (Zeile 12). Der Konstruktor wird außerdem mit den Mitgliedern prototypisch erweitert, sodass diese Mitglieder in allen Instanzen verfügbar sind.

Interessant ist nun die Umsetzung statischer Mitglieder in diesem Konstrukt.

 1 class Greeter {
 2     static standardGreeting = "Hello, there";
 3     greeting: string;
 4     greet() {
 5         if (this.greeting) {
 6             return "Hello, " + this.greeting;
 7         }
 8         else {
 9             return Greeter.standardGreeting;
10         }
11     }
12 }
13 
14 var greeter1: Greeter;
15 greeter1 = new Greeter();
16 alert(greeter1.greet());
17 
18 var greeterMaker: typeof Greeter = Greeter;
19 greeterMaker.standardGreeting = "Hey there!";
20 var greeter2:Greeter = new greeterMaker();
21 alert(greeter2.greet());

Hier ist greeter1 ähnlich wie im vorhergehenden Beispiel. Anders sieht es bei greeterMaker aus. Diese Variable enthält keine Instanz, sondern den Typ der Klasse selbst. Darauf ist der Zugriff auf die Konstruktorfunktion auf direktem Wege mittels new möglich. Wird also auf diesen Typ mit new zugegriffen, erhält man wieder ein Objektinstanz vom Typ der Klasse. Alle Instanzen haben überdies Zugriff auf das statische Feld standardGreeting im Beispiel, was auch auf dem Typ selbst möglich ist (Zeile 18 macht davon Gebrauch).

Schnittstellen aus Klassen

In klassischen objektorientierten Sprachen implementieren Klassen Schnittstellen. Schnittstellen sind der Ursprung einer Definitionskette für Typen. In TypeScript kannst du dies dagegen auch umkehren. Da Schnittstellen lediglich Typdefinitionen sind, können diese selbst auf anderen Typen aufbauen. Eine Klasse kann nun benutzt werden, um eine Schnittstelle zu erweitern:

 1 class Point {
 2     x: number;
 3     y: number;
 4 }
 5 
 6 interface Point3d extends Point {
 7     z: number;
 8 }
 9 
10 var point3d: Point3d = {x: 1, y: 2, z: 3};

Bei der Zuweisung des Objektliterals in diesem Beispiel wird die Gültigkeit des Typs gegen die Schnittstellen geprüft. Wie deren Deklaration erfolgte, ist nicht von Bedeutung.

6.8 Module

Mit den Grundlagen gerüstet zeigt dieses Kapitel, wie TypeScript benutzt werden kann, um größere Projekte sauber strukturiert umzusetzen.

Ein wichtiges Konzept dabei sind Module. Module ermöglichen einen modularen Aufbau des Quellcodes. Dies ist notwendig, weil die klassische Zerlegung in Klassen und Typen und das gemeinsame Übersetzen in ein finales Konvolut nicht funktionieren – es entsteht keine DLL aus all dem Code.

Module dienen der Organisation von Code. Es gibt in TypeScript interne und externe Module. Die internen Module werden auch als Namesraum (namespace) bezeichnet, während externe Module den generischen Begriff module nutzen.

Schau dir als Beispiel zuerst folgenden Code an, der eine einfache Eingabeprüfung ausführen kann. Alles befindet sich in einer einzigen Datei:

 1 interface StringValidator {
 2     isAcceptable(s: string): boolean;
 3 }
 4 
 5 var lettersRegexp = /^[A-Za-z]+$/;
 6 var numberRegexp = /^[0-9]+$/;
 7 
 8 class LettersOnlyValidator implements StringValidator {
 9     isAcceptable(s: string) {
10         return lettersRegexp.test(s);
11     }
12 }
13 
14 class ZipCodeValidator implements StringValidator {
15     isAcceptable(s: string) {
16         return s.length === 5 && numberRegexp.test(s);
17     }
18 }
19 
20 // Übungsbeispiel
21 var strings = ['Hallo', '12683', '23'];
22 // Die Validatoren
23 var validators: { [s: string]: StringValidator; } = {};
24 validators['ZIP code'] = new ZipCodeValidator();
25 validators['Letters only'] = new LettersOnlyValidator();
26 // Ergebnis der Validierung
27 strings.forEach(s => {
28     for (var name in validators) {
29         console.log('"' + s + '" ' + (validators[name].isAcceptable(\
30 s) ? ' passt ' : ' passt nicht ') + name);
31     }
32 });

Das funktioniert, ist aber relativ unübersichtlich. Eine Trennung von Schnittstelle, Klassen und diese nutzendem Code wäre absolut sinnvoll.

Module erstellen

Wenn mehr Klassen hinzukommen, wird der Code nicht nur unschön sondern irgendwann unbeherrschbar. Die Typen sind darüberhinaus alle im globalen Namensraum, was generell keine gute Idee ist.

Die Lösung ist die Verpackung des Codes in einen Namensraum. Dazu dient in TypeScript das Schlüsselwort namespace.

Innerhalb des Moduls werden die Teile, die öffentlich sichtbar sein sollen, exportiert. Dies erfolgt mit dem Zugriffsmodifizierer export.

 1 namespace Validation {
 2 
 3     export interface StringValidator {
 4         isAcceptable(s: string): boolean;
 5     }
 6 
 7     var lettersRegexp = /^[A-Za-z]+$/;
 8     var numberRegexp = /^[0-9]+$/;
 9 
10     export class LettersOnlyValidator implements StringValidator {
11         isAcceptable(s: string) {
12             return lettersRegexp.test(s);
13         }
14     }
15 
16     export class ZipCodeValidator implements StringValidator {
17         isAcceptable(s: string) {
18             return s.length === 5 && numberRegexp.test(s);
19         }
20     }
21 }

Bei der Benutzung wird nun den Typen der Name des Moduls vorangestellt, hier also der Name Validation (Zeilen 4-6):

 1 // Beispiel
 2 var strings = ['Hallo', '12683', '23'];
 3 // Benutzte Validatoren
 4 var validators: { [s: string]: Validation.StringValidator; } = {};
 5 validators['ZIP code'] = new Validation.ZipCodeValidator();
 6 validators['Letters only'] = new Validation.LettersOnlyValidator();
 7 // Auswertung
 8 strings.forEach(s => {
 9     for (var name in validators) {
10         console.log('"' + s + '" ' + (validators[name].isAcceptable(\
11 s) ? ' passt ' : ' passt nicht ') + name);
12     }
13 });

Module über Dateigrenzen

Module sind vor allem dann sinnvoll, wenn Teile auf verschiedene Dateien aufgeteilt werden. Die Trennung von Typen auf Dateiebene ist in Java oder C# generell üblich. In JavaScript ist es etwas komplexer. Hier müssen die Typen wieder mittels Bundling-Techniken zusammengeführt werden. Module erleichtern die Organisation und Steuerung.

Listing: Modul mit Schnittstelle (Validation.ts)
1 namespace Validation {
2     export interface StringValidator {
3         isAcceptable(s: string): boolean;
4     }
5 }
Listing: Modul mit Klasse (LettersOnlyValidator.ts)
1 /// <reference path="Validation.ts" />
2 namespace Validation {
3     var lettersRegexp = /^[A-Za-z]+$/;
4     export class LettersOnlyValidator implements StringValidator {
5         isAcceptable(s: string) {
6             return lettersRegexp.test(s);
7         }
8     }
9 }
Listing: Eine weitere Klasse (ZipCodeValidator.ts)
1 /// <reference path="Validation.ts" />
2 namespace Validation {
3     var numberRegexp = /^[0-9]+$/;
4     export class ZipCodeValidator implements StringValidator {
5         isAcceptable(s: string) {
6             return s.length === 5 && numberRegexp.test(s);
7         }
8     }
9 }

Der letzte Baustein ist die Nutzung der vorher definierten Module:

Listing: Nutzung der Module (Test.ts)
 1 /// <reference path="Validation.ts" />
 2 /// <reference path="LettersOnlyValidator.ts" />
 3 /// <reference path="ZipCodeValidator.ts" />
 4 
 5 // Testdaten
 6 var strings = ['Hello', '98052', '101'];
 7 // Validatoren
 8 var validators: { [s: string]: Validation.StringValidator; } = {};
 9 validators['ZIP code'] = new Validation.ZipCodeValidator();
10 validators['Letters only'] = new Validation.LettersOnlyValidator();
11 // Auswertung
12 strings.forEach(s => {
13     for (var name in validators) {
14         console.log('"' + s + '" ' + (validators[name].isAcceptable(\
15 s) ? ' passt ' : ' passt nicht ') + name);
16     }
17 });

Aus diesen Dateien entstehen ebenso viele JavaScript-Kompilate. Stelle sicher, dass diese alle eingebunden werden. Wenn du TypeScript auf der Kommandzeile benutst, kannst du den Transpiler anweisen, dies sofort zu erledigen. Dazu dient der Schalter –out file.js**, wobei *file.js der Name der Zieldatei ist.

Externe Module

Externe Module sind ein Konzept, das in JavaScript derzeit nur in zwei speziellen Umgebungen benutzt wird: NodeJS (serverseitig) und RequireJS (clientseitig). Beide nutzen ein Exportverfahren, um Module gezielt bereitzustellen und überdies auch Instanzen der Modulklassen zu erstellen, soweit dies vom Entwickler des Moduls gewünscht wird. Wenn du nur im Browser entwickelst, dann sind externe Module weniger wichtig. Externe Module werden normalerweise bei Bedarf nachgeladen, was im Browser nicht immer gewünscht ist. Die Entscheidung, ob dynamisch Dateien vom Server nachgeladen werden, hängt auch von der Infrastruktur, dem Nutzungsszenario, Sicherheitsüberlegungen und dem erwarteten Nutzerverhalten ab.

Export von Modulen

In TypeScript ist jede alleinstehende Datei, die ein export auf oberster Ebene enthält, automatisch ein externes Modul. Das zuletzt gezeigte Beispiel sieht bei diesem Verfahren nun folgendermaßen aus:

Listing: Die Schnittstelle (StringValidator.ts)
1 export interface StringValidator {
2     isAcceptable(s: string): boolean;
3 }
Listing: Nutzung des Moduls (LettersOnlyValidator.ts)
1 import validation = require('./Validation');
2 var lettersRegexp = /^[A-Za-z]+$/;
3 export class LettersOnlyValidator implements validation.StringValida\
4 tor {
5     isAcceptable(s: string) {
6         return lettersRegexp.test(s);
7     }
8 }
Listing: Nutzung des Moduls (ZipCodeValidator.ts)
1 import validation = require('./Validation');
2 var numberRegexp = /^[0-9]+$/;
3 export class ZipCodeValidator implements validation.StringValidator {
4     isAcceptable(s: string) {
5         return s.length === 5 && numberRegexp.test(s);
6     }
7 }
Import von Modulen

Der Import nutzt das Schlüsselwort import:

Listing: Nutzung des Moduls (Test.ts)
 1 import validation = require('./Validation');
 2 import zip = require('./ZipCodeValidator');
 3 import letters = require('./LettersOnlyValidator');
 4 
 5 // Some samples to try
 6 var strings = ['Hello', '98052', '101'];
 7 // Validators to use
 8 var validators: { [s: string]: validation.StringValidator; } = {};
 9 validators['ZIP code'] = new zip.ZipCodeValidator();
10 validators['Letters only'] = new letters.LettersOnlyValidator();
11 // Show whether each string passed each validator
12 strings.forEach(s => {
13     for (var name in validators) {
14         console.log('"' + s + '" ' + (validators[name].isAcceptable(\
15 s) ? ' passt ' : ' passt nicht ') + name);
16     }
17 });

Der Unterschied liegt in der Art der Referenzierung. Dies erfolgt mit dem Schlüsselwort import und der Aufruf mittels require. Die Plattformen NodeJs bzw. RequireJs kennen den Befehl require nativ.

1 import someMod = require('someModule');

Damit das funktioniert, muss der Transpiler den Export passend für die Plattform erzeugen:

Die plattformspezifischen Techniken sind dann dafür verantwortlich, die Dateien, die die Module enthalten zu laden und zu verarbeiten. Welcher Code jeweils erzeugt wird, zeigt folgendes Beispiel. Zuerst ein einfaches Modul:

1 import m = require('mod');
2 export var t = m.something + 1;

Für RequireJS sieht es folgendermaßen aus:

1 define(["require", "exports", 'mod'], function(require, exports, m) {
2     exports.t = m.something + 1;
3 });

Für NodeJS sieht es dagegen folgendermaßen aus (der Befehl require steht in NodeJS nativ zur Verfügung):

1 var m = require('mod');
2 exports.t = m.something + 1;

Sollen Instanzen oder statische Klassen aus dem Modul gezielt bereitgestellt werden, so wird der Aufruf export = Typ am Ende des Moduls benutzt:

Listing: Modul StringValidator (Validation.ts)
1 export interface StringValidator {
2     isAcceptable(s: string): boolean;
3 }
Listing: Modul LettersOnlyValidator (LettersOnlyValidator.ts)
1 import validation = require('./Validation');
2 var lettersRegexp = /^[A-Za-z]+$/;
3 class LettersOnlyValidator implements validation.StringValidator {
4     isAcceptable(s: string) {
5         return lettersRegexp.test(s);
6     }
7 }
8 export = LettersOnlyValidator;
Listing: Modul ZipCodeValidator (ZipCodeValidator.ts)
1 import validation = require('./Validation');
2 var numberRegexp = /^[0-9]+$/;
3 class ZipCodeValidator implements validation.StringValidator {
4     isAcceptable(s: string) {
5         return s.length === 5 && numberRegexp.test(s);
6     }
7 }
8 export = ZipCodeValidator;
Listing: Modul nutzen (Test.ts)
 1 import validation = require('./Validation');
 2 import zipValidator = require('./ZipCodeValidator');
 3 import lettersValidator = require('./LettersOnlyValidator');
 4 
 5 // Beispieldaten
 6 var strings = ['Hallo', '12683', '23'];
 7 // Validatoren
 8 var validators: { [s: string]: validation.StringValidator; } = {};
 9 validators['ZIP code'] = new zipValidator();
10 validators['Letters only'] = new lettersValidator();
11 // Auswertung
12 strings.forEach(s => {
13     for (var name in validators) {
14         console.log('"' + s + '" ' + (validators[name].isAcceptable(\
15 s) ? ' passt ' : ' passt nicht ') + name);
16     }
17 });
Weitere Import-Varianten

Ein einfacher Import (ein Modul aus einem Namensraum), sieht folgendermaßen aus:

1 import { ZipCodeValidator } from "./ZipCodeValidator";
2 
3 let myValidator = new ZipCodeValidator();

Der importierte Typ kann auch gleich umbenannt werden.

Alias beim Import

Der Import eines Moduls kann auch benutzt werden, um lange Namen bei häufiger Nutzung abzukürzen. Beim Import wird dem Modul ein neuer Name gegeben. Verwechsle dies nicht mit der Benennung einer Instanz eines importierten Moduls bei der Benutzung von require. Alias-Namen sind lediglich Ersatznamen für ohnehin vorhandene Namen.

Schaue zuerst folgende verschachtelte Modul-Definition an:

1 module Shapes {
2     export module Polygons {
3         export class Triangle { }
4         export class Square { }
5     }
6 }

Der Zugriff ist nun über Shapes.Polygons.Triangle usw. möglich, wobei die Zeichenfolge Shapes.Polygons als Namensraum betrachtet wird. Der Zugriff darauf kann nun folgendermaßen abgekürzt werden:

1 import poly = Shapes.Polygons;
2 var tr = new poly.Triangle(); 

Da es sich nicht um einen Modul-Import, sondern nur um einen Namensimport handelt, ist das Schlüsselwort require nicht erforderlich. import verhält sich hier im Grund wie var. Beim Zugriff wird eine eigenständige Instanz erstellt. Das führt dazu, dass Veränderungen an der Variablen sich nicht auf das Original auswirken. So würde ein poly = null im zuvor gezeigten Beispiel nicht dazu führen, das Shapes.Polygons auf null gesetzt wird.

In der import .. from Form geht das auch:

1 import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
2 let myValidator = new ZCV();
Laden mehrerer Importe

Statt eines konkreten Namens geht auch folgendes:

1 import * as validator from "./ZipCodeValidator";
2 let myValidator = new validator.ZipCodeValidator();

Optionales Laden von Modulen

Manchmal ist es sinnvoll, dass du Module nur bei Bedarf lädst. Durch die begrenzten Ressourcen in Skripting-Umgebungen solltest du solche Überlegungen immer anstellen. Erneut betrachtest du auch die Infrastruktur, in der die Anwendung läuft. Das Nachladen benutzter Module muss nicht zwingend die bessere Option sein gegenüber dem Laden unbenutzter Module.

Einen Teil erledigt der Transpiler automatisch. Module können zwei Arten von Code enthalten. Zum einen natürlich TypeScript, das direkt in JavaScript-Code überführt wird. Dieser Teil wird normal geladen und verarbeitet. Es gibt aber auch einige Teile, die lediglich Typ-Informationen liefern. Diese Module müssen im JavaScript nicht mehr geladen werden. Diese stützen nur den TypeScript-Compiler.

Das folgende Beispiel zeigt, wie ein Modul dynamisch geladen werden kann. Das erste Skript ist für NodeJS, das zweite hingegen für RequireJS. Die Variable needZipValidation ist ein boolescher Wert, der andernorts gesetzt werden muss.

1 declare var require;
2 import Zip = require('./ZipCodeValidator');
3 if (needZipValidation) {
4     var x: typeof Zip = require('./ZipCodeValidator');
5     if (x.isAcceptable('.....')) { /* ... */ }
6 }
1 declare var require;
2 import Zip = require('./ZipCodeValidator');
3 if (needZipValidation) {
4     require(['./ZipCodeValidator'], (x: typeof Zip) => {
5         if (x.isAcceptable('...')) { /* ... */ }
6     });
7 }

Der Trick besteht in der Verwendung von typeof. Damit wird der Typ des importierten Moduls abgefragt.

Ambiente Deklarationen

Noch etwas neues ist hier benutzt worden: das Schlüsselwort declare. Dies dient dazu, Variablen zu deklarieren, die andernorts erzeugt werden, für die aber keine Definition vorliegt. Klingt eigenwillig, wird aber klar wenn du dir vorstellst, dass Teile des Codes auf purem JavaScript stammen und dort eben nichts existiert, was dem Transpiler Hinweise auf die Existenz gibt. Er wird also möglicherweise hier einen Fehler melden.

Der Typ, der hier angenommen wird, ist immer any, weil aus der Benutzung der genaue Typ meist nicht abgeleitet werden kann.

Als Faustregel gilt: Benutze declare nur dann, wenn du mit reinem JavaScript-Code arbeiten, beispielsweise aus Bibliotheken.

Einbinden von JavaScript-Bibliotheken

Viele Bibliotheken stehen nicht in TypeScript zur Verfügung. Du kannst dennoch direkt eingebunden werden. Die meisten JavaScript-Bibliotheken exportieren lediglich einige wenige globale Objekte. Um die Typbeschreibungen für solche JavaScript-Bibliotheken in TypeScript bekannt zu machen, werden separate Module eingesetzt. Typischerweise werden dafür dann Dateinamen der Art .d.ts benutzt. Es handelt sich praktisch um Schnittstellenbeschreibungen ohne aktiven Code.

Derartige Module werden auch auch ambient bezeichnet.

Ambiente interne Module

Eine sehr populäre Grafikbibliothek ist D3. Sie steht nicht in TypeScript zur Verfügung. Um sie nicht nur nutzen, sondern auch die Vorteile des TypeScript-Compilers genießen zu können, ist eine Moduldefinition sinnvoll. Die Bibliothek selbst wird wie üblich über ein <script>-Tag eingebunden. Für die Nutzung wird nun ein Modul erstellt. Das folgende Skript deutet dies lediglich an, die gesamte Schnittstellenbeschreibung für D3 ist weitaus umfangreicher.

 1 declare module D3 {
 2     export interface Selectors {
 3         select: {
 4             (selector: string): Selection;
 5             (element: EventTarget): Selection;
 6         };
 7     }
 8 
 9     export interface Event {
10         x: number;
11         y: number;
12     }
13 
14     export interface Base extends Selectors {
15         event: Event;
16     }
17 }
18 
19 declare var d3: D3.Base;
Ambiente externe Module

Speziell in Umgebungen wie NodeJS ist das modulare Nachladen von Funktionen die normale Vorgehensweise. Auch diese Module stehen selten als natives TypeScript zur Verfügung. Definitionen für den TypeScript-Compiler werden wieder in .d.ts-Dateien erstellt. Es ist freilich sinnvoll, hier nicht eine Datei pro Modul zu erstellen, sondern alle Module in einer Modulbeschreibungsdatei zusammenzufassen.

Diese zentrale Datei könnte den Namen node.d.ts haben. Hier ein Ausschnitt am Beispiel der Module “url” und “path”:

 1 declare module "url" {
 2     export interface Url {
 3         protocol?: string;
 4         hostname?: string;
 5         pathname?: string;
 6     }
 7 
 8     export function parse(urlStr: string, 
 9                           parseQueryString?, 
10                           slashesDenoteHost?): Url;
11 }
12 
13 declare module "path" {
14     export function normalize(p: string): string;
15     export function join(...paths: any[]): string;
16     export var sep: string;
17 }

Die Referenzierung erfolgt beispielsweise mittels einer Direktive: /// <reference path="node.d.ts" />. Das Laden des Moduls erfolgt wiee sonst in Node üblich wieder mit require:

import url = require('url');

Das vollständige Skript auf einen Blick sieht nun folgendermaßen aus:

1 ///<reference path="node.d.ts"/>
2 import url = require("url");
3 var myUrl = url.parse("http://www.typescriptlang.org");

Standard-Export

Mit default kann ein Standardmodul erklärt werden:

1 declare let $: JQuery;
2 export default $;
1 import $ from "JQuery";
2 $("button.continue").html("Weiter...");

Werte-Export

Auch einfache Werte können exportiert werden:

1 export default "123";
1 import num from "./export";
2 console.log(num); 

Die Ausgabe ist hier: “123”;

Typische Probleme mit Modulen

In diesem Abschnitt wird auf einige typische Probleme mit Modules eingegangen.

Falsche Referenzierung

Häufig wird die Referenzierung mit /// <reference> vorgenommen, statt mit import. Dazu solltest du dich nochmal alle drei Ladeverfahren ins Gedächtnis rufen:

  1. Laden einer .ts-Datei mit import x = require(...);. Die Datei sollte entsprechende Import- und Export-Deklarationen und die Implementierung enthalten.
  2. Laden einer .d.ts-Datei, was mit dem Weg in 1. vergleichbar ist, nur dass hier die Implementierung fehlt (weil sie direkt als JavaScript vorliegt).
  3. Eine externe Modul-Deklaration wird benutzt.

Eine externe Modul-Deklaration nutzt declare mit dem Modulnamen in Anführungszeichen. ~ declare module “SomeModule” { export function fn(): string; } ~

Die Referenzierung mit /// <reference> sieht dagegen folgendermaßen aus:

1 /// <reference path="myModules.d.ts" />

Das Tag zeigt auf die Deklaration des Moduls.

Nutzlose Namensräume

Wenn du ein Programm von internen auf externe Module umstellen, nutze möglicherweise folgende Deklaration:

1 export module Shapes {
2     export class Triangle { /* ... */ }
3     export class Square { /* ... */ }
4 }

Das ist kritisch, weil Shapes hier die inneren Klassen kapselt und es dafür keinen Grund gibt. Benutzer des Moduls müssen einen weiteren Namensraum einfügen, ohne dass dies besonders sinnvoll wäre:

1 import shapes = require('./Shapes');
2 var t = new shapes.Shapes.Triangle();

Hier ist die Nennung shapes.Shapes irritierend.

Eine Kernfunktion externer Module ist die Kapselung des Namensraums. Zwei externe Module liefern niemals Namen in denselben Sichtbereich. Beim Import entscheidet der Benutzer des Moduls selbst, unter welchem Namen er es nutzt. Damit ist es nicht notwendig, die Kapselung in einem weiteren Namensraum selbst vorzunehmen.

Namensräume dienen dazu, logische Konstrukte zu gruppieren und damit Namenskollisionen zu vermeiden. Ein externes Modul ist ein solches logisches Konstrukt. Die Bildung eines Namens für die Gruppe erfolgt beim Import. Damit ist der Vorgang in sich abgeschlossen.

Hier nun ein besseres Beispiel für ein Modulkonzept. Zuerst werden die Exportfunktionen gezeigt:

1 export class Triangle { /* ... */ }
2 export class Square { /* ... */ }

Die beiden Dateien liegen, hier angenommen, in einem Ordner mit dem Namen shapes. Der Import sieht dann folgendermaßen aus:

1 import shapes = require('./shapes');
2 var t = new shapes.Triangle(); 
Nachteile externer Module

Module und JavaScript-Dateien bilden eine 1:1-Beziehung. Das ändert sich durch TypeScript nicht. Beim Übersetzen werden externe Module nicht zu einer Datei zusammengefasst, auch wenn die Verbindungsoption –out des Transpilers benutzt wird.

6.9 Typings

Die Verarbeitung der Typdefinitionen ist eine reine Entwicklerunterstützung und hat im Browser nichts zu suchen. Es handelt sich hier also lediglich um Bausteine, die in der NodeJs-basierten Erstellungsumgebung zum Tragen kommen. Die betroffenen Bibliotheken selbst können aber sehr wohl browserbasierte Anwendungen antreiben.

Fertige ambiente Bibliotheken

Für viele wichtige JavaScript-Bibliotheken liegen inzwischen fertige Moduldeklaration in TypeScript vor, unter anderem für D3. Das Projekt nennt sich ‘definitly typed’ und ist über Github beziehbar. Die Homepage zum Projekt ist:

Die (sehr lange) Liste der aktuell unterstützen Bibliotheken findest du [hier}(https://github.com/DefinitelyTyped/DefinitelyTyped):

Das Verwalten vieler Bibliotheken ist schon eine Herausforderung. Typischerweise kommt hierfür für die Client-Seite Bower zum Einsatz, während der Entwickler seine Werkzeuge mittels npm (Node Package Manager) verwaltet. Einige Bibliotheken, die zwingend auf TypeScript angewiesen sind, wie beispielsweise Angular 2, sind nur über npm verfügbar, weil sie nicht direkt benutzt werden können. Das Angular 2 von npm wird erst transpiliert und dann bereitgestellt.

Der TypeScript Definition Manager

Kommen jetzt noch Typdefinitionen hinzu, wäre eine Abtrennung der Verwaltung sinnvoll. Dies erledigt das Programm Typings – der TypeScript Definition Manager. Installiere in deiner Entwicklungsumgebung zuerst Typings:

npm install typings --global

Dies ist ein NodeJS-basiertes Kommandzeilenwerkzeug (cli = command line interface). Es erlaubt einige Kommandos:

1 typings install d3=github:DefinitelyTyped/DefinitelyTyped/d3/d3.d.ts\
2 #1c05872e7811235f43780b8b596bfd26fe8e7760 --global --save

Die Quellen npm und dt sind nur einige Möglichkeiten, manche Typdefinitionen sind auch auf bitbucket oder anderen Quellen. Ziehe unbedingt die Dokumentation der jeweiligen Bibliothek zu Rate. Typdeklarationen enthalten in der Regel nur Schnittstellen und Export-Anweisungen. Die geladenenen Definitionen werden in typings.json verwaltet und in einem Ordner typings abgelegt.

Typ-Deklarationsdateien via npm

Deklarationsdateien haben das Format .d.ts. Dies gilt, egal ob es sich um solche mit Definitly Typed beschaffte, selbst erstellte oder via npm geladene handelt.

Der Vorteil der npm-Version besteht in der Minimierung von Abhängigkeiten. Dies ist ein Problem mit Typings. Mit npm beschaffst du die Dateien wie folgt (hier am Beispiel lodash):

1 npm install @types/lodash --save

Importiere dann die Deklarationen in ihr Skript:

1 import * as _ from "lodash";

Der Name, der hier benutzt wird, ist “_”.

Du kannst Pakete über folgende Site suchen: http://microsoft.github.io/TypeSearch/

Das Ergebnis führt zu npm.