Fallstricke bei TypeScript

Typescript ist toll….vor allem wenn man von Java, C#, C++ oder einer anderen streng typisierten Sprache kommt. Es hält aber auch ein paar mehr oder weniger böse Fallstricke bereit. Diese Seite zeigt eine kleine Liste von bekannten Effekten, die man kennen sollte.

Webpack und Watch

Webpack bietet die Möglichkeit, mit dem Parameter --watch oder -w seine Quellcodedateien überwachen zu lassen. Sobald sich eine Änderung ergibt, wird das Projekt (inkrementell) aktualisiert. Das ist besonders praktisch, wenn man Fehler im Code sucht und immer nur kleine Änderungen machen möchte. Dabei behält Webpack auch die ganzen Abhängigkeiten im Blick und aktualisiert das Projekt auch bei einer veränderten Abhängigkeit. Alle Abhängigkeiten? NEEEIN.

Von der Abhängigkeitsbetrachtung werden *.d.ts-Dateien ausgenommen. Diese enthalten Definitionen, wie normales JavaScript aussieht und bietet die Möglichkeit, auch die Schnittstelle zum JavaScript einer statischen Codeanalyse zu unterziehen. Wenn man jedoch diese Dateien ändert, muss man den Webpack-Prozess neu starten!

Merke: Die Webpack-Option --watch oder -w schließt *.d.ts-Dateien nicht mit ein, sondern nur, was über import eingebunden wurde!

Import, ReferenceTypes oder ReferencePath

TypeScript bietet mehrere Möglichkeiten, Abhängigkeiten zu definieren. Diese sind (am Beispiel der Datei snapsvg):

// 1: Lokale Deklaration
/// <reference path='../node_modules/@types/snapsvg/index.d.ts' />

// 2: Benannter Typ
/// <reference types="snapsvg" />

// 3. ts Datei (eigentlich)
import * as Snap from 'snapsvg';

Die ersten beiden sind für den Import von *.d.ts-Dateien gedacht. Die Deklarationen müssen am Anfang einer .ts-Datei stehen (vor Code) und wirken sich auf das gesamte Projekt aus! Es ist also vollkommen egal, wo sie stehen, sie gelten immer. Das kann zu Verwirrungen führen. Fall 1 sollte man normalerweise für lokal liegende Definitionsdateien verwenden, wo man einen relativen Pfad kennt, Fall 2 hingegen für Sachen aus DefinitelyTyped die unter node_modules/@types/…. liegen. Übrigens: Auch wenn es reference types heißt, kann man immer nur einen Typen importieren. Bei mehreren Typen muss man halt mehrere Zeilen schreiben.

In der tsconfig.json sollte sich folgender Eintrag finden, der angibt, wo reference types gesucht werden:

{
    "compilerOptions": {
        // [...]
        "typeRoots": [
            // add path to @types
            "node_modules/@types"
        ], 
        // [...]
   }
}

Die letzte Deklaration ist schließlich ein tatsächlicher Import von Code, der typischerweise komplett in TypeScript geschrieben ist. Sofern man auf die Idee kommt, diesen Import auch für *.d.ts-Dateien zu verwenden, endet dies in komischen Fehlermeldunge, sobald man Dinge überladen möchte oder an anderer Stelle Declaration Merging verwenden will.

Merke:

  1. reference types nur für @types
  2. reference path nur für lokale .d.ts Dateien
  3. import nur für richtiges TypeScript (.ts)
  4. reference types und reference path wirkt sich auf alle .ts Dateien aus!

Declaration Merging

Declaration Merging ist cool. Speziell dann, wenn man Bibliotheken durch das Hinzufügen von Funktionen erweitert und diese Funktionen später noch im TypeScript nutzen möchte. In diesem Fall muss man nämlich nicht die ursprüngliche *.d.ts-Datei erweitern sondern ergänzt sie sozusagen. Dazu müssen jedoch ein paar Vorraussetzungen in den Definitionsdateien erfüllt sein.

  1. Namespaces und Funktionen mit gleichem Namen führen zu Problemen.
  2. Klassen und Namespaces mit gleichem Namen klappen. (warum auch immer)
  3. Es werden nur Definitionsdateien zusammengeführt. Eine .d.ts-Datei mit einer *.ts-Datei zu erweitern geht nicht.

Hier ein Beispiel, wo Snapsvg um eine zusätzliche Methode erweitert wird. Die Methode ist in JavaScript als Plugin für Snap geschrieben, aber irgendwie muss man das ja nach TypeScript bekommen. @types/snapsvg/index.d.ts (Auszug):

declare module "snapsvg" {
    export = Snap;
}

declare const Snap: SnapStatic;
interface SnapStatic {
    filter:Filter;   
    path:Path;

    (width:number|string,height:number|string):Snap.Paper;
    (query:string):Snap.Paper;
    (DOM:SVGElement):Snap.Paper;
    // [...]
}

declare namespace Snap{
    interface Paper extends Snap.Element {
        // [...]
    }
}

./snap-extensions.d.ts:

namespace Snap {
    export interface Paper{
        translate(dx:Number, dy:Number):Snap.Paper;
    }
}

Zuletzt noch die Zusammenführung der Module:

/// <reference types="snapsvg" />
/// <reference path="./snap-extensions.d.ts" />

class SvgGraph{
    constructor(container: JQuery)
    {
        let paper: Snap.Paper = Snap(container);
        paper.circle(10,20,4);
        paper.translate(4,2);
    }
    // [...]
}

Merke:

  1. Declaration Merging funktioniert nur in .d.ts-Dateien.
  2. Declaration Merging funktioniert nur, wenn es keine Namespace-Funktion-Kollision gibt.
  3. Declaration Merging funktioniert nur, wenn die .d.ts-Dateien per reference importiert wurden.