Üblicherweise gilt es als unsicher, bei der Erstellung von JavaScript-Applikationen Prototypen für eingebaute Typen zu erweitern. Diese Faustregel stammt noch aus der Zeit vor jQuery, als weise JavaScript-Zauberer Bibliotheken wie MooTools (https://mootools.net) geschaffen haben, die eingebaute Prototyp-Methoden erweitert und direkt überschrieben haben. Als zu viele Magier am gleichen Zaubertrank mitkochen wollten, kam es jedoch zu Problemen. Ohne statische Typsysteme wurden diese Konflikte erst sichtbar, wenn sich verärgerte Benutzer zur Laufzeit darüber beschwerten.
Wenn Sie keinen JavaScript-Hintergrund haben, überrascht es Sie vielleicht, dass JavaScript-eigene Methoden zur Laufzeit beliebig verändert werden können (wie [].push, 'abc'.toUpperCase oder Object.assign). Weil JavaScript so dynamisch ist, gibt es Ihnen für jedes eingebaute Objekt Zugriff auf dessen Prototyp – Array. prototype, Function.prototype, Object.prototype etc.
Damals war die Erweiterung dieser Prototypen ziemlich unsicher. Durch statische Typsysteme wie TypeScript sind Erweiterungen inzwischen deutlich sicherer geworden.6
Im folgenden Beispiel wollen wir den Array-Prototyp auf sichere Weise um eine zip-Methode erweitern. Dafür werden zwei Dinge gebraucht: Zuerst muss der Typ des Array-Prototyps in einer .ts-Datei (z.B. zip.ts) erweitert werden. Danach können wir den Prototyp mit unserer neuen zip-Methode versehen:
// TypeScript über .zip informieren
interface Array<T> {
zip<U>(list: U[]): [T, U][]
}
// .zip implementieren
Array.prototype.zip = function<T, U>(
this: T[],
list: U[]
): [T, U][] {
return this.map((v, k) =>
tuple(v, list[k])
)
}
Da unsere Datei keine expliziten Im- oder Exporte enthält (d.h., sie befindet sich im Skriptmodus, wie in »Modulmodus oder Skriptmodus« auf Seite 224 beschrieben), können wir das globale Array-Interface direkt erweitern. Hierfür deklarieren wir ein Interface mit exakt dem gleichen Namen (Array<T>) und überlassen es TypeScript, beide Interfaces miteinander zu kombinieren. Befänden sich unserer Dateien im Modulmodus (z.B. weil wir für unsere zip-Implementierung etwas importieren müssen), hätten wir unsere globale Erweiterung mit einer declare global-Typdeklaration umgeben müssen (siehe »Typdeklarationen« auf Seite 234):
declare global {
interface Array<T> {
zip<U>(list: U[]): [T, U][]
}
}
global ist ein spezieller Namensraum, der alle global definierten Werte enthält (also alles, was Sie in einer Datei im Modulmodus verwenden können, ohne es zuvor per import zu laden, siehe Kapitel 10), mit dem Sie Namen im globalen Geltungsbereich aus einer Datei im Modulmodus erweitern können.
Beachten Sie, dass wir durch die Deklaration von interface Array<T> den globalen Namensraum von Array für unser gesamtes TypeScript-Projekt erweitern. Das heißt, selbst wenn Sie zip.ts nicht aus Ihrer Datei importieren, geht TypeScript davon aus, dass [].zip zur Verfügung steht. Um Array.prototype zu erweitern, müssen wir aber sicherstellen, dass jede Datei, die zip verwendet, zuerst zip.ts lädt, um die zip-Methode an Array.prototype zu installieren. Wie können wir sicherstellen, dass zip.ts auf jeden Fall zuerst geladen wird?
Ganz einfach: Wir aktualisieren unsere tsconfig.json, sodass zip.ts explizit von unserem Projekt ausgeschlossen ist. Auf diese Weise müssen Consumer zip.ts auf jeden Fall explizit per import laden:
{
*exclude*: [
"./zip.ts"
]
}
Jetzt können wir zip nach Belieben und vollkommen sicher verwenden:
import './zip'
[1, 2, 3]
.map(n => n * 2) // number[]
.zip(['a', 'b', 'c']) // [number, string][]
Wenn wir das ausführen, wird zuerst map auf das Array angewandt; im zweiten Schritt behandeln wir das Ergebnis mit zip.
[
[2, 'a'],
[4, 'b'],
[6, 'c']
]
In diesem Kapitel haben wir die am weitesten fortgeschrittenen Merkmale des Typsystems von TypeScript behandelt: die verschiedenen Aspekte der Varianz, flussbasierte Typableitung, Verfeinerung, Typerweiterung, Totalität, abgebildete und bedingungsabhängige Typen. Danach haben wir einige fortgeschrittene Muster für die Arbeit mit Typen hergeleitet: Type Branding für die Simulation nominaler Typen, die Nutzung der distributiven Eigenschaft konditionaler Typen für die Arbeit an Typen auf Typebene und die sichere Erweiterung von Prototypen.
Es ist nicht schlimm, wenn Sie nicht alles verstanden haben oder sich nicht alles merken konnten. Lesen Sie dieses Kapitel einfach später noch einmal durch und verwenden Sie es als Referenz, wenn Sie Schwierigkeiten haben, etwas sicherer auszudrücken.