Wenn Sie ein Programm schreiben, können Sie die Verkapselung auf verschiedenen Ebenen umsetzen. Auf unterster Eben verkapseln Funktion bestimmtes Verhalten und Datenstrukturen wie Objekte, während Listen Daten verkapseln. Danach können Sie die Funktionen und Daten zu Klassen gruppieren oder sie als Hilfsfunktionen mit einem eigenen Namensraum, mit eigener Datenbank oder als Datenspeicher auslagern. Üblicherweise verwendet man dabei eine einzelne Klasse oder einen Satz von Hilfsfunktionen pro Datei. Auf höherer Ebene könnten Sie mehrere Klassen oder Hilfsfunktionen zu einem Package zusammenfassen, das Sie per NPM veröffentlichen.
Wenn wir von Modulen sprechen, müssen wir unterscheiden, wie der Compiler (TSC) oder Ihr Build-System (Webpack, Gulp etc.) Module auflöst und wie die Module zur Laufzeit tatsächlich in Ihrer Applikation geladen werden (<script />-Tags, SystemJS etc.). In JavaScript werden diese Aufgaben üblicherweise von separaten Programmen erledigt. Dadurch werden Erwägungen zu Modulen nicht unbedingt einfacher. Die Modulstandards CommonJS und ES2015 erleichtern die Zusammenarbeit der drei Programme, während Modul-Bundler wie Webpack helfen, die drei Arten der Auflösung, die hinter den Kulissen passieren, zu abstrahieren.
In diesem Kapitel geht es hauptsächlich um die erste Form dieser drei Programme: die Auflösung und Kompilierung von Modulen durch TypeScript. Eine detaillierte Diskussion über die Funktionsweise von Build-Systemen und Runtime-Loadern im Bezug auf Module finden Sie in Kapitel 12. Hier geht es um folgende Themen:
Aber zuerst ein bisschen Hintergrundinformation.
Da TypeScript zu JavaScript kompiliert und damit interoperabel ist, muss es auch die verschiedenen von JavaScript-Programmierern verwendeten Modulstandards unterstützen.
Am Anfang (1995) besaß JavaScript noch keinerlei Unterstützung für Modulsysteme. Ohne Module wurde alles im globalen Namensraum deklariert. Dadurch wurden die Erstellung und das Skalieren von Applikation stark erschwert. Schnell konnten Variablennamen knapp werden, oder es kam zu Kollisionen zwischen verschiedenen Namen. Ohne explizite APIs für jedes Modul ist es schwer, herauszufinden, welche Teile des Moduls Sie tatsächlich benutzen sollen und welche Teile private Implementierungsdetails sind.
Um diese Probleme zu lösen, simulierte man Module entweder anhand von Objekten oder über sofort ausgeführte Funktionsausdrücke (Immediately Invoked Function Expressions, IIFEs). Anschließend wurden sie der globalen Variablen window zugewiesen, wodurch sie von anderen Modulen einer Applikation (bzw. anderen auf der gleichen Webseite gehosteten Applikationen) genutzt werden konnten. Das sah ungefähr so aus:
window.emailListModule = {
renderList() {}
// ...
}
window.emailComposerModule = {
renderComposer() {}
// ...
}
window.appModule = {
renderApp() {
window.emailListModule.renderList()
window.emailComposerModule.renderComposer()
}
}
Da das Laden und Ausführen von JavaScript das UI des Browsers blockiert, wurde der Browser des Benutzers mit dem Wachsen einer Applikation und ihres Codes immer langsamer. Daher begannen schlaue Programmierer, JavaScript nach dem Laden der eigentlichen Seite bei Bedarf dynamisch nachzuladen und nicht mehr alles auf einmal vorzuhalten. Fast 10 Jahre nach der ersten Veröffentlichung von JavaScript enthielten Dojo (Alex Russell, 2004), YUI (Thomas Sha, 2005) und LABjs (Kyle Simpson, 2009) Modul-Loader, mit denen JavaScript-Code erst nach dem Laden der Seite selbst, bei Bedarf – »lazy« (und oftmals asynchron) eingebunden werden konnte. Dieses »faule« und asynchrone Laden von Modulen hatte drei Konsequenzen:
Das Laden eines Moduls mit LABjs sah beispielsweise so aus:
$LAB
.script('/emailBaseModule.js').wait()
.script('/emailListModule.js')
.script('/emailComposerModule.js')
Ungefähr zur gleichen Zeit wurde NodeJS (Ryan Dahl, 2009) entwickelt. Seine Entwickler hatten aus den wachsenden Problemen von JavaScript und anderen Sprachen gelernt. Daher entschieden sie sich, ein Modulsystem direkt in die Plattform zu integrieren. Wie bei jedem guten Modulsystem musste es die gleichen drei Kriterien erfüllen wie die Loader von LABjs und YUI. NodeJS verwendete hier den CommonJS-Modulstandard, der so aussah:
// emailBaseModule.js
var emailList = require('emailListModule')
var emailComposer = require('emailComposerModule')
module.exports.renderBase = function() {
// ...
}
In der Zwischenzeit gewann der von Dojo und RequireJS vorangetriebene AMD-Modulstandard (James Burke, 2008) an Fahrt. Er unterstütze eine ähnliche Funktionalität und besaß ein eigenes Build-System für das Bundling von JavaScript-Code:
define('emailBaseModule',
['require', 'exports', 'emailListModule', 'emailComposerModule'],
function(require, exports, emailListModule, emailComposerModule) {
exports.renderBase = function() {
// ...
}
}
)
Ein paar Jahre später kam Browserify heraus (James Halliday, 2011). Dadurch erhielten Frontend-Entwickler die Möglichkeit, CommonJS auch im Frontend zu benutzen. CommonJS wurde der De-facto-Standard für das Bundling von Modulen sowie als Syntax für Importe und Exporte.
In einigen Bereichen gab es mit CommonJS allerdings Probleme. Hierzu gehört, dass require-Aufrufe grundsätzlich synchron ablaufen und dass der CommonJS-Algorithmus für das Auflösen der Module für das Web nicht ideal ist. Hinzu kommt, dass Code, der CommonJS verwendet, in manchen Fällen nicht statisch analysierbar ist (als TypeScript-Programmierer sollten Sie an dieser Stelle besonders hellhörig werden). Der Grund ist, dass module.exports überall vorkommen kann (selbst in toten Codezweigen, die niemals erreicht werden). Aufrufe von require können ebenfalls an beliebiger Stelle vorkommen und beliebige Strings und Ausdrücke enthalten. Dadurch ist es unmöglich, ein JavaScript-Programm statisch zu linken und zu verifizieren, ob all die referenzierten Dateien tatsächlich existieren und exportieren, was sie vorgeben zu exportieren.
Vor diesem Hintergrund führte ES2015, die sechste Ausgabe der Sprache ECMA-Script, einen neuen Standard für Im- und Exporte ein. Er besaß eine klare Syntax und konnte statisch analysiert werden. Das sah dann so aus:
// emailBaseModule.js
import emailList from 'emailListModule'
import emailComposer from 'emailComposerModule'
export function renderBase() {
// ...
}
Dies ist der Standard, der heute in JavaScript und TypeScript verwendet wird. Beim Schreiben dieses Buchs wurde der Standard allerdings noch nicht nativ von jeder JavaScript-Runtime unterstützt. Daher müssen wir den Code bei Bedarf in ein unterstütztes Format herunterkompilieren (CommonJS für NodeJS-Umgebungen, globale Variablen oder ein Modul-ladefähiges Format für Browser-Umgebungen).
TypeScript bietet mehrere Möglichkeiten, den Code eines Moduls einzubinden und zu exportieren: mit globalen Deklarationen, über die standardmäßigen import- und export-Anweisungen aus ES2015 und mit rückwärtskompatiblen import-Aufrufen aus CommonJS-Modulen. Zusätzlich können wir mit dem Build-System von TSC Module für eine Vielzahl von Umgebungen kompilieren: globale Variablen, ES2015, CommonJS, AMD, SystemJS oder UMD (eine Mischung aus CommonJS und AMD – je nachdem, was in der Umgebung des Consumers zur Verfügung steht).
Solange Sie nicht vor einem Rudel Wölfe um Ihr Leben laufen, sollten Sie in Ihrem TypeScript-Code die import- und export-Anweisungen aus ES2015 verwenden und auf die Benutzung von CommonJS-, globalen oder Namensraum-basierten Modulen verzichten. Sie sehen aus wie ganz normaler JavaScript-Code:
export function foo() {}
export function bar() {}
// b.ts
import {foo, bar} from './a'
foo()
export let result = bar()
Der ES2015-Modulstandard unterstützt Standard-(»default«-)Exporte:
// c.ts
export default function meow(loudness: number) {}
// d.ts
import meow from './c' // Note the lack of {curlies}
meow(11)
Außerdem wird die Wildcard-Schreibweise (*) unterstützt, mit der alles aus einem Modul importiert werden kann:
// e.ts
import * as a from './a'
a.foo()
a.bar()
Und das Re-Exportieren einiger (oder aller) Exporte eines Moduls:
// f.ts
export * from './a'
export {result} from './b'
export meow from './c'
Da wir TypeScript und nicht JavaScript schreiben, können wir Typen und Interfaces genauso exportieren wie Werte. Und da Typen und Werte in eigenen Namensräumen leben, ist es vollkommen in Ordnung, zwei Dinge mit dem gleichen Namen zu exportieren: eines auf Wertebene und eines auf Typebene. Wie bei anderem Code auch, leitet TypeScript selbstständig ab, ob Sie bei der Verwendung den Typ oder den Wert gemeint haben:
// g.ts
export let X = 3
export type X = {y: string}
// h.ts
import {X} from './g'
let a = X + 1 // X bezieht sich auf den Wert X
let b: X = {y: 'z'} // X bezieht sich auf den Typ X
Modulpfade sind Dateinamen im Dateisystem. Das verknüpft die Module mit ihrer Position im Dateisystem, ist aber ein wichtiges Merkmal für Modul-Loader, die diese Organisation kennen müssen, um Modulnamen auf bestimmte Dateien umsetzen zu können.
Je größer Ihre Applikation wird, desto länger dauert es bis zur initialen Darstellung. Das gilt besonders für Frontend-Applikationen, bei denen der Flaschenhals oftmals durch das Netzwerk bestimmt wird. Er kann aber auch bei Backend-Applikationen auftreten, die länger zum Starten brauchen, wenn Sie auf der oberen Ebene mehr Code importieren, der aus dem Dateisystem geladen, geparst, kompiliert und ausgewertet werden muss, während anderer Code am Laufen gehindert wird.
Im Frontend lässt sich das Problem zum Beispiel durch Code Splitting lösen (mal abgesehen davon, einfach weniger Code zu schreiben): Der Code wird auf eine Reihe automatisch erzeugter JavaScript-Dateien verteilt, anstatt alles in eine große Datei zu verpacken. Durch das Aufteilen können außerdem mehrere Stücke parallel geladen werden, wodurch einzelne Netzwerkanfragen nicht mehr so groß ausfallen (siehe Abbildung 10-1).
Abbildung 10-1: Netzwerk-Wasserfalldiagramm für JavaScript-Code, der von facebook.com geladen wird
Eine weitere Optimierung ist das »lazy loading« von Codestücken, die erst geladen werden, wenn sie tatsächlich gebraucht werden. Wirklich große Frontend-Applikationen, wie beispielsweise Facebook oder Google, verwenden diese Optimierung standardmäßig. Ansonsten könnte es passieren, dass Clients beim ersten Laden der Seite Gigabytes an JavaScript-Code laden, was Minuten oder gar Stunden dauern kann (einmal abgesehen von Nutzern, die auf die Verwendung solcher Dienste verzichten, sobald sie ihre Handyrechnung sehen).
Das »lazy loading« ist auch noch aus anderen Gründen sinnvoll. Die beliebte Bibliothek Moment.js (https://momentjs.com) für die Arbeit mit Kalenderdaten und Uhrzeiten enthält Pakete für jedes auf der Welt verwendete Datumsformat, nach locale getrennt. Jedes Paket ist ungefähr 3 KB groß. Das Laden aller Pakete für einen Benutzer würde die Performance und Bandbreite unnötig beeinträchtigen. Stattdessen ist es sinnvoll, die locale des Benutzers zu ermitteln und nur das wirklich benötigte Datumspaket zu laden.
LABjs und seine Geschwister haben das Konzept des »lazy loading« (Laden bei Bedarf) eingeführt. Durch dynamische Importe wurde es formalisiert. Es sieht so aus:
let locale = await import('locale_us-en')
Sie können import entweder als Anweisung verwenden, um Code statisch einzubinden (wie wir es bisher getan haben), oder als Funktion, die ein Promise für Ihr Modul zurückgibt (wie im oben stehenden Beispiel).
Wenn Sie import einen beliebigen Ausdruck übergeben, der zu einem String ausgewertet wird, verlieren Sie Ihre Typsicherheit. Um dynamische Importe sicher zu nutzen, müssen Sie mindestens einen der folgenden Schritte ausführen:
Wenn Sie die zweite Option verwenden, wird das Modul häufig statisch importiert, aber nur an einer Typposition verwendet. Dadurch kompiliert TypeScript den statischen Import weg (weitere Informationen finden Sie unter »Die types-Direktive« auf Seite 267). Zum Beispiel:
import {locale} from './locales/locale-us'
async function main() {
let userLocale = await getUserLocale()
let path = ./locales/locale-${userLocale}
let localeUS: typeof locale = await import(path)
}
Hier haben wir locale aus ./locales/locale-us importiert, es aber nur für seinen Typ verwendet, den wir per typeof locale ausgelesen haben. Das mussten wir tun, weil TypeScript den Typ von import(path) nicht statisch ermitteln konnte, denn path ist ein berechneter Wert und kein statischer String. Da wir locale nicht als Wert benutzt haben, sondern nur um ihren Typ zu ermitteln, hat TypeScript den statischen Import wegkompiliert (tatsächlich erzeugt TypeScript in diesem Beispiel überhaupt keine Top-Level-Exporte). So erhalten wir ausgezeichnete Typsicherheit und einen dynamisch berechneten Import.
TSC-Setting: module TypeScript unterstützt dynamische Imports nur im esnext-Modulmodus. Um dynamische Imports nutzen zu können, verwenden Sie in den compilerOptions Ihrer tsconfig.json die Einstellung {"module": "esnext"}. Weitere Informationen finden Sie unter »TypeScript auf dem Server ausführen« auf Seite 263 und »TypeScript im Browser ausführen« auf Seite 263. |
Wenn Sie ein JavaScript-Modul einbinden, das den CommonJS- oder AMD-Standard verwendet, können Sie die gewünschten Namen auf die gleiche Art wie bei ES2015-Modulen einfach importieren:
import {something} from './a/legacy/commonjs/module'
Normalerweise sind die Standardexporte von CommonJS nicht mit den Standardimporten von ES2015 interoperabel. Sie müssen also einen Wildcard-Import verwenden:
import * as fs from 'fs'
fs.readFile('some/file.txt')
Um die Kanten ein wenig zu glätten, können Sie in den compilerOptions Ihrer tsconfig.json die Einstellung {"esModuleInterop": true} angeben. Dadurch kann das Wildcard-Zeichen weggelassen werden:
import fs from 'fs'
fs.readFile('some/file.txt')
Wie zu Beginn des Kapitels gesagt: Selbst wenn dieser Code kompiliert, heißt das nicht, dass er auch zur Laufzeit funktioniert. Unabhängig davon, welche Modulstandard Sie verwenden – import/export, CommonJS, AMD, UMD oder globale Browservariablen –, Ihr Modul-Bundler und -Loader müssen das Format kennen, damit Sie Ihren Code bei der Kompilierung korrekt verpacken und aufteilen bzw. zur Laufzeit korrekt laden können. Mehr hierzu finden Sie unter Kapitel 12. |
TypeScript kennt beim Parsen Ihrer TypeScript-Dateien zwei Modi: den Modulmodus und den Skriptmodus. TypeScript verwendet hier genau einen Anhaltspunkt: Enthält Ihre Datei irgendwelche import- oder export-Anweisungen? Falls ja, wird der Modulmodus verwenden.
Bisher haben wir den Modulmodus verwendet, und das werden Sie auch die meiste Zeit tun. Im Modulmodus benutzen Sie import oder import(), um Code aus anderen Dateien einzubinden, und export, um Ihren Code anderen Dateien zur Verfügung zu stellen. Sofern Sie UMD-Module von Drittherstellern verwenden (zur Erinnerung, UMD versucht, sich an die jeweilige Umgebung anzupassen, indem es, je nach Bedarf, CommonJS, RequireJS oder globale Browservariablen verwendet), können Sie deren globale Exporte nicht direkt nutzen, sondern müssen diese zuerst importieren.
Im Skriptmode stehen auf der obersten Ebene deklarierte Variablen anderen Dateien Ihres Projekts ohne explizite Importe zur Verfügung. Gleichzeitig können Sie globale Exporte aus UMD-Modulen von Drittherstellern sicher einbinden, ohne sie vorher explizit zu importieren. Ein paar Anwendungsfälle sind:
Sie werden fast immer den Modulmodus verwenden wollen. TypeScript wählt ihn automatisch für Sie aus, wenn Sie echten Code schreiben, der anderen Code per import einbindet oder anderen Dateien per export zur Verfügung stellt.
In TypeScript gibt es noch eine weitere Möglichkeit, Code zu verkapseln: mit dem Schlüsselwort namespace (»Namensraum«). Das Konzept der Namensräume dürfte Java-, C#-, C++-, PHP- und Python-Programmierern bekannt sein.
Wenn Sie von einer Sprache kommen, die Namensräume verwendet, sollten Sie wissen, dass TypeScript Namensräume zwar unterstützt, sie aber nicht der bevorzugte Weg für die Verkapselung von Code sind. Wenn Sie nicht sicher sind, ob Sie Namensräume oder Module verwenden sollen, verwenden Sie Module. |
Namensräume abstrahieren die feinen Details, z.B. wie Dateien im Dateisystem organisiert sind. Sie müssen nicht wissen, dass Ihre .mine-Funktion im Ordner schemes/scams/bitcoin/apps liegt. Stattdessen können Sie auf einen kurzen, bequemen Namensraum darauf zugreifen, zum Beispiel: Schemes.Scams.Bitcoin.Apps.mine.1
Angenommen, wir haben zwei Dateien: ein Modul zum Ausführen von HTTP-GET-Requests und einen Consumer, der das Modul für die Durchführung von Requests nutzt:
// Get.ts
namespace Network {
export function get<T>(url: string): Promise<T> {
// ...
}
}
// App.ts
namespace App {
Network.get<GitRepo>('https://api.github.com/repos/Microsoft/typescript')
}
Ein Namensraum braucht einen Namen (z.B. Network) und kann Funktionen, Variablen, Typen, Interfaces und andere Namensräume exportieren. Sämtlicher Code innerhalb eines namespace-Blocks, der nicht explizit exportiert wird, ist für diesen Block privat. Da Namensräume andere Namensräume exportieren können, ist es einfach, verschachtelte Namensräume zu modellieren. Vielleicht wird unser Network-Modul langsam zu groß, und Sie wollen es in kleinere Submodule aufteilen. Das können wir über Namensräume erledigen:
export namespace HTTP {
export function get<T>(url: string): Promise<T> {
// ...
}
}
export namespace TCP {
listenOn(port: number): Connection {
//...
}
// ...
}
export namespace UDP {
// ...
}
export namespace IP {
// ...
}
}
Jetzt liegen alle unsere Netzwerk-Hilfsfunktionen in eigenen Unter-Namensräumen unter Network. Diese lassen sich nun aus jeder anderen Datei als Network. HTTP.get oder Network.TCP.listenOn aufrufen. Interfaces können auf die gleiche Weise wie Namensräume erweitert (augmented) werden, wodurch sie leicht auf mehrere Dateien verteilt werden können. TypeScript fügt identisch benannte Namensräume rekursiv für uns zusammen:
// HTTP.ts
namespace Network {
export namespace HTTP {
export function get<T>(url: string): Promise<T> {
// ...
}
}
}
// UDP.ts
namespace Network {
export namespace UDP {
export function send(url: string, packets: Buffer): Promise<void> {
// ...
}
}
}
// MyApp.ts
Network.HTTP.get<Dog[]>('http://url.com/dogs')
Network.UDP.send('http://url.com/cats', new Buffer(123))
Wenn Ihre Namensraum-Hierarchien zu lang werden, können Sie Aliase verwenden, um eine Hierarchie zur leichteren Benutzung zu verkürzen. Beachten Sie, dass die Destrukturierung (wie beim Import von ES2015-Modulen) für Aliase nicht unterstützt wird.
namespace A{
export namespace B {
export namespace C {
export let d = 3
}
}
}
// MyApp.ts
import d = A.B.C.d
let e = d * 3
Kollisionen zwischen identisch benannten Exporten sind nicht erlaubt:
// HTTP.ts
namespace Network {
export function request<T>(url: string): T {
// ...
}
}
// HTTP2.ts
namespace Network {
// Error TS2393: Duplicate function implementation.
export function request<T>(url: string): T {
// ...
}
}
Die Ausnahme für die »Keine Kollisionen«-Regel sind überladene ambiente Funktionsdeklarationen, die Sie zum Verfeinern von Funktionstypen verwenden können.
// HTTP.ts
namespace Network {
export function request<T>(url: string): T
}
// HTTP2.ts
namespace Network {
export function request<T>(url: string, priority: number): T
}
// HTTPS.ts
namespace Network {
export function request<T>(url: string, algo: 'SHA1' | 'SHA256'): T
}
Im Gegensatz zu Im- und Exporten haben die module-Einstellungen aus tsconfig.json’s für Namensräume keine Wirkung, die grundsätzlich zu globalen Variablen kompiliert werden. Wir begeben uns kurz hinter die Kulissen, um einen Blick auf die erzeugten Ausgaben zu werfen. Angenommen, wir haben folgendes Modul:
// Flowers.ts
namespace Flowers {
export function give(count: number) {
return count + ' flowers'
}
}
TSC macht daraus die folgenden JavaScript-Ausgaben:
let Flowers
(function (Flowers) {
function give(count) {
return count + ' flowers'
}
Flowers.give = give
})(Flowers || (Flowers = {}))
Benutzen Sie nach Möglichkeit Module anstelle von Namensräumen
Anstelle von Namensräumen sollten Sie reguläre Module verwenden (die per import und export benutzt werden können). So halten Sie sich besser an die JavaScript-Standards und machen Ihre Abhängigkeiten expliziter.
Explizite Abhängigkeiten haben eine Menge Vorteile, wie bessere Lesbarkeit, das Erzwingen von Modul-Isolation (weil Namensräume automatisch zusammengefasst werden, Module aber nicht) und statischer Analyse. Das ist besonders für große Frontend-Projekte wichtig, bei denen das Entfernen von totem Code und das Aufteilen des kompilierten Codes auf mehrere Dateien für die Performance eine besonders große Rolle spielt.
Sollen TypeScript-Programme in einer NodeJS-Umgebung ausgeführt werden, sind Module ebenfalls die richtige Wahl, weil in NodeJS die Unterstützung für CommonJS bereits eingebaut ist. In Browserumgebungen bevorzugen manche Programmierer aus Gründen der Einfachheit Namensräume. Für mittlere bis große Projekte sollten Sie aber auch hier auf jeden Fall Module verwenden.
Bisher haben wir drei Arten kennengelernt, auf die TypeScript Dinge für uns kombiniert:
Wie Sie sich vermutlich gedacht haben, sind dies drei Sonderfälle eines viel allgemeineren Verhaltens von TypeScript. TypeScript besitzt einen großen Schatz an Verhaltensweisen, mit denen Namen auf verschiedene Weise kombiniert werden können. Das ermöglicht die Verwendung verschiedener Muster, die sonst nur schwer ausgedrückt werden können (siehe Tabelle 10-1).
Tabelle 10-1: Können die Deklarationen verschmolzen werden?
Wenn Sie beispielsweise einen Wert und ein Typalias im gleichen Geltungsbereich deklarieren, erlaubt TypeScript das und leitet ab, ob Sie den Typ oder den Wert meinten – je nachdem, ob Sie den Namen an der Position eines Werts oder eines Typs benutzen. Auf diese Weise lässt sich das in »Das Companion-Objektmuster« auf Seite 141 beschriebene Entwurfsmuster implementieren. Das bedeutet auch, dass Sie ein Interface und einen Namensraum verwenden können, um Companion-Objekte zu implementieren. Sie sind also nicht auf Werte und Typaliase beschränkt. Oder Sie können das »module merging« nutzen, um die Moduldeklaration eines Drittherstellers zu erweitern (mehr dazu unter »Ein Modul erweitern« auf Seite 282). Oder Sie können ein Enum mit statischen Methoden versehen, indem Sie das Enum mit einem Namensraum kombinieren (probieren Sie das ruhig einmal aus!).
Das moduleResolution-Flag
Aufmerksamen Lesern ist bereits das moduleResolution-Flag in ihrer tsconfig.json aufgefallen. Es steuert den von TypeScript verwendeten Algorithmus zum Auflösen von Modulnamen Ihrer Applikation. Das Flag unterstützt zwei Modi:
In diesem Kapitel ging es um das Modulsystem von TypeScript. Wir haben mit einer kurzen Geschichte der JavaScript-Modulsyteme begonnen. Danach ging es um ES2015-Module, das sichere »lazy loading« (Nachladen bei Bedarf) von Code mithilfe dynamischer Importe, die Zusammenarbeit mit CommonJS- und AMD-Modulen sowie den Vergleich zwischen Modul- und Skriptmodus. Im Anschluss haben wir uns mit Namensräumen, der Kombination von Namensräumen und dem Verschmelzen von Deklarationen in TypeScript beschäftigt.
Wenn Sie Applikationen in TypeScript entwickeln, sollten Sie sich möglichst strikt an ES2015-Module halten. TypeScript ist es zwar egal, welches Modulsystem Sie einsetzen, aber die Verwendung von ES2015 macht die Integration mit Build-Werkzeugen deutlich leichter (mehr dazu auch unter Kapitel 12).