let
und const
for..of
Dieses Kapitel führt in die elementaren Prinzipien und Bausteine von TypeScript ein. Dabei werden auch die Unterschiede zu JavaScript hervorgehoben.
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.
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.
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). Dieses Verhalten wird mit dem Schlüsselwort var
erreicht. Ohne Bezeichner sind Variablen immer global.
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. Dies entspricht dem Verhalten von JavaScript ES2015, wird aber vom TypeScript-Transpiler auch dann durchgesetzt, wenn die Zielplattform ES5 ist.
Das folgende Beispiel mit var
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
);
Für Konstanten gibt es ein eigenes Schlüsselwort: const
. Konstanten müssen sofort initialisiert werden.
1
const
WIDTH
=
123
;
Konstanten dürfen auch Objekte enthalten:
1
const
CELL
=
{
x
: 23
,
y
: 42
};
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:
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.
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
Die folgende Tabelle zeigt Literale für Sonderzeichen:
Zeichen | Bedeutung |
---|---|
\b | BackSpace |
\n | NewLine |
\t | Tab |
\f | FormFeed |
\r | CarriageReturn |
Ein Beispiel dazu:
var
s
: string
=
'Eine mehrzeilige\r\nZeichenkette'
;
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:
alert
(
"über"
));
Schreibe jedoch folgendes, wenn Umlaute benutzt werden:
alert
(
unescape
(
"%FCber"
));
Damit stellst du sicher, dass die Umlaute immer korrekt interpretiert und angezeigt werden können.
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:
true
false
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.
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.
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
}
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
let
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.
Grundsätzlich entsprechen die einfachen Typen in TypeScript denen in JavaScript. Dazu gehören:
boolean
number
string
array
Eine Sonderstellung ist die Enumeration (Aufzähltyp, ein Art Werteliste), die auf number
basiert:
enum
Dazu folgt ein eigener Abschnitt, da es hier viele Möglichkeiten gibt.
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:
false
null
undefined
""
(leere Zeichenkette)0
NaN
Alle anderen Werte sind true
, einschließlich "0"
und "false"
(in Anführungszeichen), was manchmal zu Verwirrung führt.
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
.
Die Funktionen entsprechen denen in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.
Die Funktionen entsprechen denen in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.
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.
Die Funktionen entsprechen denen in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.
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
);
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:
|=
: Füge einen Wert hinzu&=
und ~
: Entferne einen Wert|
: Kombiniere Flags&
und Testwert: Prüfe einen WertMit 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 gesetzt 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
}
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.
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.
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
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
;
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.
return
mit void
nicht erlaubt istSehr speziell ist der Typ never
. Damit wird 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.
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 Typ 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 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"
Einige interne Symbole dienen der vereinfachten Benutzung interner Funktionen.
instanceof
benutzt.for of
benutzt wird. match
stützt.replace
stützt.search
stützt.split
stützt.Mehr dazu findest du bei MDN – Mozilla Developer Network.
Folgende Anweisungen stehen als Schlüsselwörter zur Verfügung:
if
switch
while
do
for
, in
, of
break
continue
return
try
, catch
, throw
function
interface
, class
, extends
, implements
super
var
, let
, const
Dies Anweisungen entsprechen denen in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.
Die Vorgehensweise entspricht der in JavaScript und wurden bereits im vorhergehenden Kapitel behandelt.
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.
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.
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.
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 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 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
;
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.
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.
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 beschreiben sowohl Eigenschaften als auch Funktionen und deren Parameter. Diese können 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.
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 (ES2015) wurde 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 ES2015 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.
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.
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.
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.
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.
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 ES2015 wurde eine spezielle vereinfachte Syntax für Eigenschaften eingeführt. Diese Form nimmt auch TypeScript.
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
.
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.
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 Konstruktor 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.
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 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).
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.
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.
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 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.
1
namespace
Validation
{
2
export
interface
StringValidator
{
3
isAcceptable
(
s
: string
)
:
boolean
;
4
}
5
}
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
}
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:
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 sind ein Konzept, das in JavaScript derzeit nur in zwei speziellen Umgebungen benutzt wird: NodeJS (serverseitig) und RequireJS (clientseitig). Ansonsten benutzen diverse Frameworks wie Angular oder React sogenannte Packer, die ihrerseits auf ein Modulformat setzen, dass weitgehend unabhängig von JavaScript ist und eigene Modulformate wie CommonJS, AMD oder UMD einsetzt. Meist wird dann implizit RequireJS benutzt.
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.
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:
1
export
interface
StringValidator
{
2
isAcceptable
(
s
: string
)
:
boolean
;
3
}
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
}
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
}
Der Import nutzt das Schlüsselwort import
:
1
import
validation
=
require
(
'./Validation'
);
2
import
zip
=
require
(
'./ZipCodeValidator'
);
3
import
letters
=
require
(
'./LettersOnlyValidator'
);
4
5
// Testwerte
6
var
strings
=
[
'Hello'
,
'98052'
,
'101'
];
7
// Validator
8
var
validators
:
{
[
s
: string
]
:
validation
.
StringValidator
;
}
=
{};
9
validators
[
'ZIP code'
]
=
new
zip
.
ZipCodeValidator
();
10
validators
[
'Letters only'
]
=
new
letters
.
LettersOnlyValidator
();
11
// Test
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:
1
export
interface
StringValidator
{
2
isAcceptable
(
s
: string
)
:
boolean
;
3
}
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
;
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
;
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
});
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.
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 Modulimport, sondern nur um einen Namensimport handelt, ist das Schlüsselwort require
nicht erforderlich. import
verhält sich hier im Grunde 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 Form import .. from
geht das auch:
1
import
{
ZipCodeValidator
as
ZCV
}
from
"./ZipCodeValidator"
;
2
let
myValidator
=
new
ZCV
();
Statt eines konkreten Namens geht auch folgendes:
1
import
*
as
validator
from
"./ZipCodeValidator"
;
2
let
myValidator
=
new
validator
.
ZipCodeValidator
();
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. Sie 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.
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 arbeitest, beispielsweise aus 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.
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
;
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"
);
Mit default
kann ein Standardmodul erklärt werden:
1
declare
let
$
: JQuery
;
2
export
default
$
;
1
import
$
from
"JQuery"
;
2
$
(
"button.continue"
).
html
(
"Weiter..."
);
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”;
In diesem Abschnitt wird auf einige typische Probleme mit Modules eingegangen.
Häufig wird die Referenzierung mit /// <reference>
vorgenommen, statt mit import
. Dazu solltest du dich nochmal alle drei Ladeverfahren ins Gedächtnis rufen:
import x = require(...);
. Die Datei sollte entsprechende Import- und Export-Deklarationen und die Implementierung enthalten.Eine externe Modul-Deklaration nutzt declare
mit dem Modulnamen in Anführungszeichen.
1
declare
module
"SomeModule"
{
2
export
function
fn
()
:
string
;
3
}
Die Referenzierung mit /// <reference>
sieht dagegen folgendermaßen aus:
1
/// <reference path="myModules.d.ts" />
Das Tag zeigt auf die Deklaration des Moduls.
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
();
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.
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.
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.
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:
typings search tape
: Suche nach einer Definition (hier: tape).typings search --name react
: Suche nach einem Definitionsnamen.typings install debug --save
: Installation einer Typdeklaration (hier für eine Bibliothek mit dem Namen debug). Die Quelle ist hier npm, was über die Eigenschaft defaultSource
in der Konfigurationsdatei .typingsrc
bestimmt werden kann.typings install dt~mocha --global --save
: –global macht die Definition global verfügbar. Der Präfix dt~ bestimmt, dass die Definition direkt von “Definitly Types* auf Github geholt wird. Die Syntax ist <quelle>~<name>. Alternartiv kann die Quelle auch als Schalter angegeben werden: –source npm. Dies dürfte der häufigste Fall sein. Soll ein bestimmtes Release geladen werden, wird der ganze Pfad von Github benötigt:
1
typings
install
d3
=
github
:
DefinitelyTyped
/
DefinitelyTyped
/
d3
/
d3
.
d
.
ts
\
2
#
1
c05872e7811235f43780b8b596bfd26fe8e7760
--
global
--
save
typings info env~node --versions
: Dies ist die Suche für eine bestimmte Umgebung (hier: NodeJs).typings install env~node@0.10 --global --save
: Dito, aber für eine bestimmte Version von NodeJs (hier: 0.10).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.
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:
Das Ergebnis führt zu npm.