ManuScripts: Wenn jemand eine Reise tut… Funktionale Programmierung mit Elm – Teil 5 – Noch eine Dosis TypeScript

Eine Blog-Serie zum Vergleich von Elm, ReactJS und AngularJS anhand eines praktischen Beispiels.Hier geht es direkt zu [Teil 1, 23, 4, 6]

Es ist schon eine schräge Truppe, wie sie sich da präsentiert beim Ausstieg aus dem Bus vor den Elmer Sportbahnen. Allen voran die erwartungsvolle und leichtfüssige Elm und zuhinderst der schwer beladene Angular, leise vor sich hin fluchend, jedoch genau so tapfer und entschlossen, wie man das bei den anderen spürt.

Es ist ja auch nicht abzustreiten, dass Elm einen klaren Vorteil hat durch ihre angeborenen Fähigkeiten zur statischen Typisierung. Damit die anderen sich in den unwegigen Alpen genau so sicher fühlen können wie Elm, holen beide nun ihre Packungen mit der Aufschrift “TypeScript” hervor, welche sie glücklicherweise in der Apotheke in Glarus noch erstehen konnten.

Während Elm sich in Elm noch etwas umsieht, untersuchen die anderen die beiliegende Packungsbeilage lange und gründlich:

TypeScript für ReactJS und AngularJS

In diesem Teil liegt der Fokus bei TypeScript, welches wir im Zusammenhang mit den beiden Javascript-Werkzeugen Angular und React verwenden. Das Thema wurde bereits angeschnitten in Teil 3.

TypeScript ist Microsofts Antwort auf die von der Entwickler-Gemeinde geforderten Typsicherheit und lehnt im Syntax stark an C# an, da die Sprache offenbar vorwiegend für C#-Entwickler entworfen wurde, damit sich diese schnell damit vertraut fühlen können.

TypeScript wird bereits von vielen populären Editoren unterstützt, vornehmlich von Visual Studio 2015, aber auch viele Open-Source Editoren, wie Sublime Text, Atom oder Eclipse, bieten eine zum Teil recht breite Integration an.

Weil wir sowohl für Angular, als auch für React TypeScript einsetzen, sparen wir hier etwas Zeit, indem wir den Code für die Domäne (vom Englischen: problem domain) und sogar eventuelle Viewmodels teilen können.

Es sei hier nochmals erwähnt, dass aus dem Hause Facebook ein konkurrierender Typ-Checker namens Flow  stammt (https://flowtype.org/), der aus obigem Grund nicht weiter untersucht wird.

Damit wir etwas mehr von TypeScript sehen, werden wir nicht nur den Spielstein entwerfen, sondern gleich den ganzen Sack der möglichen Steine und diesen auf dem Bildschirm darstellen.

Im folgenden werden wir also Code schreiben, der sowohl von ang2-anagram
und react-anagram verwendet werden soll (siehe Teil 2, resp. Teil 3).

Für den Moment legen wir dazu einfach ein weiteres Verzeichnis namens ts-shared an (neben den obigen zwei), wo die in diesem Beitrag erarbeiteten Dateien liegen werden.

Sugar

Zuerst erstellen wir die Datei TileDef.ts:

class TileDef {
  constructor(public letter: string, public count: number, public value: number) { }
}

Der Klassen-Syntax von TypeScript lehnt sich stark an denjenigen von ES2016 an. So liest sich dies nun wie eine typische ES2016-Klasse, da es aber effektiv ein TypeScript-Konstrukt ist, konnte auch die Konstruktor-Parameter syntaktisch erweitert (syntactic sugar) werden, um die Bedienung zu vereinfachen (zu versüssen).

Ohne Zucker sähe die Klasse so aus:

class TileDef {
  letter: string;
  count: number;
  value: number;

  constructor( letter: string, count: number, value: number) { 
    this.letter = letter;
    this.count = count;
    this.value = value;
  }
}

Meiner Meinung nach ist die zweite Version sehr viel verständlicher als die erste, die Verwendung des Zuckers ist hier aber durchaus abzuwägen. Genau wie im privaten Alltag.

Der TypeScript-Compiler wird folgendermassen installiert und verwendet:

  • Installation: npm install -g typescript
  • Verwendung: tsc KompilierMich.ts (erzeugt KompilierMich.js und KompilierMich.map)
  • Konfiguration: tsconfig.json (steuert den tsc-Aufruf.)

Die Existenz von tsconfig.json im TypeScript-Projekt-Root-Verzeichnis führt dazu, dass die Kompilation aller darunterliegenden Dateien mit tsc gestartet werden kann und der Compiler sich gemäss den Angaben in diesem File verhält. Der momentane Inhalt wird gleich aufgeführt, für die Interpretation der Compiler-Optionen, sowie weitere Optionen sei auf die Dokumentation verwiesen.

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "sourceMap": true,
        "outDir" : "./built"
    },
    "exclude": [
        "node_modules"
    ]
}

(Mit der Angabe outDir können die während der Kompiliererung erzeugten Artifakte in ein eigenes Verzeichnis geschoben werden, und müssen nicht zwischen den .ts-Dateien zu liegen kommen (siehe auch Elm’s Empörung in Teil 2))

Also, tsconfig.json mit obigem Inhalt anlegen, dann tsc starten, und sobald alles kompiliert, geht die Reise weiter.

Rocks

Nebst der TileDef.ts brauchen wir auch noch die Klasse für den einzelnen Spielstein:

class Tile {
  blankoLetter: String;

  constructor(public tileDef : TileDef) {
    this.tileDef = tileDef;
    this.blankoLetter = null;
  }

  setBlankoLetter( letter: String ) : void{
    if( this.tileDef.letter != BLANK ) {
      throw new Error("not a blanko");
    }
    if( this.blankoLetter != null ) {
      throw new Error("blanko already set");
    }
    this.blankoLetter = letter;
  }
}

Für Blankos gibt es mit setBlankoLetter die Möglichkeit, zu einem späteren Zeitpunkt festzulegen, für welchen Buchstaben der Blanko-Stein stehen soll.

Wie schon erwähnt in Teil 3, enthält der Spielsack in der deutschen Variante des SCRABBLE-Spiels zu Beginn 102 Spielsteine. Diese Steine sollen nun erstellt werden und auf den Bildschirm ausgegeben werden.

Baggies

Dazu brauchen wir ein ähnliches Konstrukt wie für die Steine, nämlich eine Klasse namens BagDef für die Definition des Inhalts und eine Klasse namens Bag, welche dann die Spielsteine selbst beherbergt.

Da wir diese Klassen nun lieber in einem anderen File führen würden, müssen wir uns zu diesem Zeitpunkt mit Modul-Exporten und -Importen in TypeScript beschäftigen. (Beachte: „module“ : [„commonjs“](http://stackoverflow.com/questions/16521471/relation-between-commonjs-amd-and-requirejs) im tsconfig.json)

Das neue File BagDef.ts sollte eigentlich mit dieser Zeile beginnen, die auf Deutsch sagt, “importiere mir die Klassen TileDef und Tile aus dem relativ gelegenen File ./TileDef.ts”:

import { TileDef, Tile } from „./TileDef.ts“;

Damit das aber so kompiliert, müssen wir das File TileDef.ts noch entsprechend anpassen und die Exporte deklarieren (export vor die Klassen-Definitionen stellen):

export class TileDef { …
export class Tile { …

Es können auch andere Werte exportiert werden:

export const BLANK = “ „;

Nun können wir den Rest der Klassen-Gerüste entsprechend bereitstellen (BagDef.ts):

import { TileDef, Tile } from "./TileDef.ts"

export class BagDef {
  language: String;
  tileDefs: TileDef [];

  constructor( language: String, tileDefs: TileDef[] ) { 
    this.language = language;
    this.tileDefs = tileDefs;
  };

  createTiles() : Array<Tile> {
    return [];     
  }
}

export class Bag {
  bagDef: BagDef;
  tiles: Tile [];

  constructor( bagDef: BagDef ) { 
    this.bagDef = bagDef;
    this.tiles = bagDef.createTiles();
  }
}

export const GERMAN_BAG = 
  new BagDef("german"
            ,  [ new TileDef( BLANK, 2, 0 )
               , new TileDef( "E", 15, 1 )
               , new TileDef( "N", 9, 1 )
               , new TileDef( "S", 7, 1 )
               , new TileDef( "R", 6, 1 )
               , new TileDef( "U", 6, 1 )
               , new TileDef( "A", 5, 1 )
               , new TileDef( "D", 4, 1 )
               , new TileDef( "H", 4, 2 )
               , etc...
               ] 
            );

Es fehlt eigentlich nur noch die Implementation von createTiles. Dazu werden wir mit Arrays von TileDefs  und Tiles  umgehen müssen. Um Array-Operationen zu vereinfachen, so wie sie hier benötigt werden, weiche ich im Normalfall auf die exzellente lodash-Bibliothek aus. (An dieser Stelle muss ich auf das neuere und noch raffinierter ramda.js hinweisen.)

Dopings, ehmm, typings

Um bestehende Bibliotheken mit TypeScript verwenden zu können, gibt es den Typescript Definition Manager oder kurz typings. Wie der Name schon sagt, werden damit TypeScript-Typ-Definitionen verwaltet, die der TypeScript-Compiler verwendet, um die extrenen Typen zu überprüfen.

Wenn wir nun also lodash in unser Projekt bringen wollen, verwenden wir das Tool folgendermassen:

npm init -f --> (legt package.json an)
npm install lodash --save --> (speichert die benötigten Paket-Dateien im Ordner node_modules/ und speichert die Paket-Referenz in package.json) 
npm install -g typings (installiert typings global, benötigt sudo unter Linux)
typings install lodash --save (speichert die lodash-Typdefinitionen im Ordner typings/ und passt typings.json entsprechend an)

Danach kann die Abhängigkeit in BagDef.ts (oder wo auch immer) importiert werden mit:

import * as _ from ‚lodash‘;

(importiere alle Exporte aus dem Paket lodash unter dem Alias-Namen _)

Stoned

Die Funktion createTiles soll durch alle TileDefs gehen und so viele Steine davon erzeugen, wie die Definition es vorgibt. Mit lodash kann eine derartige map und concat-Operation mittels flatMap abgekürzt werden:

  createTiles() : Array<Tile> {
    return _.flatMap( this.tileDefs, td => td.createTiles() );  
  }

Da mir meine spontane Entscheidung der Delegation an TileDef gefällt, füge ich die Implementation der neuen Funktion dort hinzu (TileDef.ts):

  createTiles() : Array<Tile> {
    return _.times( this.count, _ => new Tile(this) );
  }

Anmerkung 1: Der verwendete Syntax nutzt die seit ES2015 verfügbaren Arrow-Funktionen. Also x => f(x) anstatt function(x) { return f(x); }, was viel Schreibarbeit erspart und die Leserlichkeit erhöht. (Arrow-Funktionen sind immer anonym und verändern nicht den Kontext von this, im Gegensatz zu herkömmlichen Funktionen.)

Anmerkung 2: Es ist eine allgemein akzeptierte Konvention, ignorierte Parameter mit _ zu bezeichnen. Damit ist klar, es wird zwar ein Parameter mitgegeben, aber nicht verwendet. In diesem Beispiel ist es der Zähler, der von der Funktion _.times an die anonyme Funktion übergeben wird.

Reacts Reaktionen

Die Verwendung im React.JS-Projekt (react-anagram), läuft so ab:

app.tsx (Applikation):

import * as React from "react";
import * as ReactDOM from "react-dom";

import { Bag, GERMAN_BAG } from "../../ts-shared/BagDef";
import { BagView } from "./Components.tsx"

var bagView = <BagView bag={new Bag(GERMAN_BAG)} />

ReactDOM.render(bagView, document.getElementById("main"));
</pre>

Components.tsx (Views):

import * as React from "react"
import { TileDef, Tile } from "../../ts-shared/TileDef";
import { Bag } from "../../ts-shared/BagDef";

interface TileProps {
  tile: Tile;
}

export class TileView extends React.Component<TileProps, {}> {
  render() {
    const {tile} = this.props;
    return <div>Tile: {tile.tileDef.letter}, V: {tile.tileDef.value}</div>;
  }
}

interface BagProps {
  bag: Bag;
}

export class BagView extends React.Component<BagProps, {}> {
  render() {
    return ( 
      <div>
        <div>Bag: {this.props.bag.bagDef.language}</div>
        <div>Letters: {this.props.bag.tiles.length}</div>
        <div>
          <ul>
            { this.props.bag.tiles.map((tile, idx) => 
                <TileView tile={tile} key={idx} />) }
          </ul>
        </div>
      </div> 
     );
  }
}

Die neue Klasse BagView erhält einen Bag als Zustand und stellt mit dem jsx-Syntax den Inhalt des Sacks dar. Mit dem Interface BagProps wird sichergestellt, dass die an die Komponenten übergebenen Daten einer gewissen Form folgen. Die Dokumentation für Interfaces geht auf das verwandte Konzept “Duck-Typing” ein

Fazit

TypeScript ist eine grosse Hilfe und für professionelle Entwicklung in meinen Meinung nach unumgänglig, falls mit javascript programmiert werden muss und das Projekt über 100 Zeilen lang wird. Beide, React und Angular 2, setzen auf TypeScript, da mit der Typsicherheit auch viel syntaktischer Sugar (z.B. Dekoratoren für Angular2-Komponenten) und besserer Tool-Support kommt.

Die Typdeklarationen für die meisten gängigen Bibliotheken wurden schon erstellt, und eigene Deklarationen sind relativ leicht nachzureichen.

Leider kommt mit Typescript auch wieder zusätzlicher Konfigurations- und Wartungsaufwand, der einfach nicht unterschätzt werden darf.

Die Integration mit ReactJS hat sich als relativ einfach erwiesen. Dasselbe für Angular2 zu tun, stellte sich als eindeutig schwieriger heraus, so dass ich die Entwicklung der Angular2-Komponenten lieber dem Leser als “Übung” überlasse. (Code-Kontributionen sind sehr willkommen!)

React ist bestens gelaunt und mit Elm ins Gespräch vertieft, so dass keiner bemerkt, dass Angular seit Beginn der Gondel-Fahrt zusehends bleicher wurde. Das leise Fluchen ist zu einem noch leiseren Jammern geworden, welches jeweils ab dem ersten Ruckeln über die Pfosten-Führung anschwillt und beim anschliessenden Schwenker seinen Höhepunkt mit abruptem Ende findet.

React scheint die Rezeptur besser zu bekommen als seinem Wanderpartner. Ganz im Gegenteil: in einem Moment der Stille zwischen zwei Pfosten, ertappt sich React dabei, wie er sich dank seinen neuen Fähigkeiten nun richtig auf die bevorstehende Wanderung freut. Sein Blick gleitet über das saftige Grün der Stalden-Alp. Er kann seine Gedanken aber nicht lange schweifen lassen, bevor ihn Angular’s Klagen wieder in die Realität zurückholt. Ob Angular die Reise wohl bis zum Ende durchstehen wird?

About the Author
Since earning his Swiss HTL degree at the University of Applied Sciences in Rapperswil (HSR) in 1999, Manuel Baumann has gathered extensive experience designing and realizing applications for the web and the desktop in a variety of settings both in Switzerland and the United States.