In diesem Anhang finden Sie die Grundbausteine und -muster, die bei der Typisierung von Drittanbieter-Modulen immer wieder auftreten. Eine eingehende Diskussion der Typisierung von Drittanbieter-Code finden Sie unter »JavaScript, für das es keine Typdeklarationen auf DefinitelyTyped gibt« auf Seite 249.
Da sich die Moduldeklarationen in .d.ts-Dateien befinden müssen und keine Werte enthalten dürfen, müssen Sie bei der Deklaration von Modultypen das Schlüsselwort declare verwenden, damit die vom Modul exportierten Werte tatsächlich den richtigen Typ haben. In Tabelle D-1 finden Sie eine kurze Zusammenfassung üblicher Deklarationen mit den entsprechenden Typdeklarationen.
Tabelle D-1: TypeScript und die entsprechenden Typdeklarationen
.ts |
.d.ts |
var a = 1 |
declare var a: number |
let a = 1 |
declare let a: number |
const a = 1 |
declare const a: 1 |
function a(b) { return b.toFixed() } |
declare function a(b: number): string |
class A { b() { return 3 } } |
declare class A { b(): number } |
namespace A {} |
declare namespace A {} |
type A = number |
type A = number |
interface A { b?: string } |
interface A { b?: string } |
Wie Sie Ihre Deklarationsdateien schreiben, hängt stark davon ab, ob Ihr Modul den globalen Namensraum, ES2015 oder CommonJS für die Exporte verwendet.
Wenn Ihr Modul dem globalen Namensraum Werte zuweist, ohne tatsächlich etwas zu exportieren, können Sie einfach eine Datei im Skriptmodus erstellen (mehr dazu unter »Modulmodus oder Skriptmodus« auf Seite 224). Danach stellen Sie Ihren Variablen-, Funktions- und Klassendeklarationen ein declare voran (alle anderen Deklarationen wie enum, type etc. bleiben davon unberührt):
// Globale Variable
declare let someGlobal: GlobalType
// Globale Klasse
declare class GlobalClass {}
// Globale Funktion
declare function globalFunction(): string
// Globales Enum
enum GlobalEnum {A, B, C}
// Globaler Namensraum
namespace GlobalNamespace {}
// Globales Typalias
type GlobalType = number
// Globales Interface
interface GlobalInterface {}
Alle oben genannten Deklarationen stehen jeder Datei Ihres Projekts global zur Verfügung, ohne dass hierfür ein expliziter Import gebraucht wird. Sie können someGlobal also in Ihrem Projekt benutzen, ohne es erst zu importieren. Allerdings muss someGlobal zur Laufzeit dem globalen Namensraum zugewiesen werden (window für Browser, global für NodeJS).
Verwenden Sie keine import- und export-Anweisungen in Ihren Deklarationsdateien, damit die Datei im Skriptmodus bleibt.
Verwendet Ihr Modul ES2015-Exporte, also das Schlüsselwort export, können Sie declare (das sicherstellt, dass eine globale Variable definiert ist) einfach durch export (das sicherstellt, dass eine ES2015-Bindung exportiert wird) austauschen:
// Standard-Export
declare let defaultExport: SomeType
export default defaultExport
// Benannter Export
export class SomeExport {
a: SomeOtherType
}
export class ExportedClass {}
// Funktions-Export
export function exportedFunction(): string
// Enum-Export
enum ExportedEnum {A, B, C}
// Namensraum-Export
export namespace SomeNamespace {
let someNamespacedExport: number
}
// Typ-Export
export type SomeType = {
a: number
}
// Interface-Export
export interface SomeOtherType {
b: string
}
Vor ES2015 war CommonJS der De-facto-Modulstandard und ist beim Schreiben dieses Buchs immer noch der Standard für NodeJS. Auch er verwendet das Schlüsselwort export, allerdings in einer etwas anderen Schreibweise:
declare let defaultExport: SomeType
export = defaultExport
Hier haben wir unsere Exporte export zugewiesen, anstatt es (wie bei ES2015-Exporten) als Modifier zu verwenden.
Eine Typdeklaration für ein Drittanbieter-Modul im CommonJS-Standard kann genau einen Export enthalten. Um mehrere Dinge zu exportieren, nutzen wir zusammengefasste Deklarationen (siehe Anhang C).
Um mehrere Exporte (ohne Standard-Export) zu typisieren, verwenden wir einen einzelnen Namensraum (namespace):
declare namespace MyNamedExports {
export let someExport: SomeType
export type SomeType = number
export class OtherExport {
otherType: string
}
}
export = MyNamedExports
Oder wie wäre es mit einem CommonJS-Modul, das einen Standard-Export und benannte Exporte verwendet? Hier können wir die Deklarationsverschmelzung nutzen:
export let someExport: SomeType
export type SomeType = number
}
declare function MyExports(a: number): string
export = MyExports
Die Typisierung eines UMD-Moduls ist mit der eines ES2015-Moduls fast identisch. Der einzige Unterschied ist, dass Sie die spezielle export as namespace-Syntax verwenden müssen, um Ihr Modul global für Dateien im Skriptmodus verfügbar zu machen (siehe »Modulmodus oder Skriptmodus« auf Seite 224). Zum Beispiel:
// Standard-Export
declare let defaultExport: SomeType
export default defaultExport
// Benannter Export
export class SomeExport {
a: SomeType
}
// Typ-Export
export type SomeType = {
a: number
}
export as namespace MyModule
Beachten Sie die letzte Zeile. Wenn Ihr Projekt eine Datei im Skriptmodus enthält, kann diese Ihr Modul direkt (ohne sie vorher zu importieren) über den globalen Namensraum MyModule nutzen.
let a = new MyModule.SomeExport
Die Erweiterung der Typdeklaration eines Moduls ist zwar seltener als die Typisierung eines Moduls, kann aber ein Thema sein, wenn Sie ein JQuery-Plug-in oder ein Lodash-Mixin schreiben. Anstelle eines Lodash-Mixins verwenden Sie in diesem Fall eine reguläre Funktion und anstelle des jQuery-Plug-ins-… – Moment! Warum benutzen Sie eigentlich immer noch jQuery?
Wenn Sie den globalen Namensraum eines anderen Moduls erweitern wollen, erstellen Sie einfach eine Datei im Skriptmodus (siehe »Modulmodus oder Skriptmodus« auf Seite 224) und reichern (»augment«) diese an. Das funktioniert allerdings nur für Interfaces und Namensräume, weil TypeScript hier die Zusammenfassung für Sie übernimmt.
Als Beispiel wollen wir jQuery um eine großartige neue marquee-Methode erweitern. Wir beginnen mit der Installation von jquery:
npm install jquery --save
npm install @types/jquery --save-dev
Dann erstellen wir in unserem Projekt eine neue Datei, z.B. jquery-extensions.d.ts, und fügen dann Ihre marquee-Methode dem globalen JQuery-Interface hinzu (ich habe herausgefunden, dass jQuery seine Methoden am JQuery-Interface definiert, indem es seine Typdeklarationen durchgeht):
interface JQuery {
marquee(speed: number): JQuery<HTMLElement>
}
Jetzt können wir die marquee-Methode in allen Dateien verwenden, die jQuery nutzen (natürlich brauchen wir hierfür auch Laufzeit-Implementierungen für marquee):
import $ from 'jquery'
$(myElement).marquee(3)
Die gleiche Technik verwenden wir übrigens auch bei der Erweiterung der eingebauten globalen Variablen in »Prototypen sicher erweitern« auf Seite 156.
Die Erweiterung von Modulen ist etwas komplizierter und hat viele Fallstricke. Sie müssen Ihre Erweiterung korrekt typisieren und Ihre Module zur Laufzeit in der richtigen Reihenfolge laden. Dabei muss sichergestellt sein, dass die Typen Ihrer Erweiterungen angepasst werden, wenn sich die Struktur der Typdeklarationen für das erweiterte Modul ändert.
Als Beispiel typisieren wir einen neuen Export für React. Wir beginnen mit der Installation von React und seinen Typdeklarationen:
npm install react --save
npm install @types/react --save-dev
Danach nutzen wir das »Module Merging« (Zusammenfassen von Modulen, siehe »Deklarationsverschmelzung (declaration merging)« auf Seite 229) und deklarieren einfach ein Modul mit dem gleichen Namen wie unser React-Modul:
import {ReactNode} from 'react'
declare module 'react' {
export function inspect(element: ReactNode): void
}
Im Gegensatz zu unserem Beispiel zur Erweiterung von globalen Variablen macht es hier keinen Unterschied, ob unsere Erweiterungsdateien sich im Modul- oder im Skriptmodus befinden.
Wie wäre es mit der Erweiterung eines bestimmten Exports aus einem Modul? Angenommen, wir wollen – inspiriert von ReasonReact (https://reasonml.github.io/reason-react) – einen eigenen Reducer für unsere React-Komponenten erstellen (mit einem Reducer kann ein expliziter Satz von Zustandsübergängen für eine React-Komponente definiert werden). Beim Schreiben dieses Buchs wurde der Typ React.Component in den Typdeklarationen für React als Kombination aus einem Interface und einer Klasse definiert, die in einem einzelnen UMD-Export zusammengefasst wurde:
export = React
export as namespace React
declare namespace React {
interface Component<P = {}, S = {}, SS = any>
extends ComponentLifecycle<P, S, SS> {}
class Component<P, S> {
constructor(props: Readonly<P>)
// …
}
// ...
}
Jetzt wollen wir Component mit unserer eigenen reducer-Methode erweitern. Hierfür fügen wir folgende Zeile in ein react-extensions.d.ts-Datei im Wurzelverzeichnis unseres Projekts ein:
import 'react'
declare module 'react' {
interface Component<P, S> {
reducer(action: object, state: S): S
}
}
Nachdem diese Typen deklariert sind (wir gehen davon aus, dass das entsprechende Laufzeitverhalten für diese Aktualisierung ebenfalls irgendwo implementiert wurde), können wir unsere reducer-Methode nun direkt in React-Komponenten typsicher nutzen:
import * as React from 'react'
type Props = {
// ...
}
type State = {
count: number
item: string
}
type Action =
| {type: 'SET_ITEM', value: string}
| {type: 'INCREMENT_COUNT'}
| {type: 'DECREMENT_COUNT'}
class ShoppingBasket extends React.Component<Props, State> {
reducer(action: Action, state: State): State {
switch (action.type) {
case 'SET_ITEM':
return {...state, item: action.value}
case 'INCREMENT_COUNT':
return {...state, count: state.count + 1}
case 'DECREMENT_COUNT':
return {...state, count: state.count - 1}
}
}
}
Wie bereits zu Beginn dieses Abschnitts gesagt, sollten Sie dieses Muster nach Möglichkeit vermeiden (auch wenn es ziemlich cool ist), weil es Ihre Module spröde und anfällig für Probleme bei der Ladereihenfolge macht. Stattdessen sollten Sie versuchen, Komposition zu nutzen, damit Ihre Modulerweiterungen das Modul, das sie erweitern, konsumieren. Anstatt das Modul selbst zu verändern, sollten Sie besser einen Wrapper exportieren.