ANHANG D

Rezepte für das Schreiben von Deklarationsdateien für JavaScript-Module von Drittanbietern

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 }

Verschiedene Arten von Exporten

Wie Sie Ihre Deklarationsdateien schreiben, hängt stark davon ab, ob Ihr Modul den globalen Namensraum, ES2015 oder CommonJS für die Exporte verwendet.

Globale Objekte

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.

ES2015-Exporte

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

}

// Klassen-Export

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

}

CommonJS-Exporte

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:

declare namespace MyExports {

export let someExport: SomeType

export type SomeType = number

}

declare function MyExports(a: number): string

export = MyExports

UMD-Exporte

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

Ein Modul erweitern

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?

Globale Namensräume

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.

Module

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' image

declare module 'react' { image

interface Component<P, S> { image

reducer(action: object, state: S): S image

}

}

  1. image Wir importieren 'react' und schalten unsere Erweiterungsdatei dadurch in den Skriptmodus, um ein React-Modul einbinden zu können. Wir hätten den Skriptmodus hier auch auf andere Weise aktivieren können, beispielsweise indem wir etwas anderes im- oder exportieren oder durch den Export eines leeren Objekts (export {}). Wir hätten 'react' hier nicht unbedingt importieren müssen.
  2. image Wir deklarieren das 'react'-Modul und zeigen TypeScript dadurch, dass wir Typen für genau diesen import-Pfad deklarieren wollen. Da @types/react bereits installiert ist (das einen Export für genau diesen 'react'-Pfad definiert), kombiniert TypeScript diese Moduldeklaration mit der in @types/react bereitgestellten.
  3. imageWir erweitern das von React bereitgestellte Component-Interface, indem wir ein eigenes Interface gleichen Namens deklarieren. Gemäß den Regeln des »Interface Merging« (siehe »Deklarationen verschmelzen« auf Seite 94) müssen wir exakt die gleiche Signatur wie in @types/react verwenden.
  4. image Schließlich deklarieren wir unsere reducer-Methode.

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.