ECMAScript 6 und TypeScript – Auf in die neue JavaScript-Generation

Seit einiger Zeit ist es möglich, seinen Code in ECMAScript 6 oder TypeScript zu schreiben und diesen ohne Kompatibilitätsverluste überall laufen zu lassen. Ich möchte hier ein paar Entdeckungen teilen, die ich bei der Erkundung des Neulandes gemacht habe.

Dabei werde ich nicht groß auf Sprachdetails eingehen, sondern vor allem eine Begriffsklärung und die Verwendung der Transpiler in den Vordergrund stellen.

Zu diesem Beitrag gehört ein Git-Repository, in dem alle hier erwähnten Dateien vorhanden sind. Es kann mit dem folgenden Befehl ausgecheckt werden:

git clone https://github.com/svi3c/es6-and-typescript-blog-sources.git

Was ist ES6?

ECMAScript 6 (Auch bekannt als „Harmony“ oder auch „ES2015“) ist der aktuelle JavaScript-Standard und der Nachfolger von ECMAScript 5. Letzterer hat mittlerweile weite Verbreitung erlangt. Der neue Standard wird von den Browserherstellern derzeit implementiert und Teile sind bereits umgesetzt.

Wir müssen aber glücklicher Weise nicht darauf warten, dass die Browserhersteller ES6 implementiert haben. Es steht uns eine Hand voll Transpiler zur Verfügung, die ES6-Code zu ES5-Code übersetzen.

Der Vorreiter unter diesen Transpilern ist derzeit Babel (ehemals „6to5“).

Verwendung von Babel als ES6 nach ES5 Transpiler

Babel und das es2015 preset kann man über npm installieren (Seit Babel 6 gibt es mehrere plugins und entsprechende presets):

npm install -g babel-cli
npm install babel-preset-es2015

Betrachten wir die folgende ES6-Datei:

class Foo {
  bar(x) {
    return `foo bar ${x}`;
  }
}
const foo = new Foo();
console.log(foo.bar("baz"));

Wenn wir uns ansehen wollen, was das Ergebnis der Übersetzung von ES6 nach ES5 ist, können wir dies folgendermaßen auf der Kommandozeile tun:

babel --presets=es2015 foo.es6 -o foo.js

Das Ergebnis sieht dann so aus:

"use strict";

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Foo = (function () {
  function Foo() {
    _classCallCheck(this, Foo);
  }

  _createClass(Foo, [{
    key: "bar",
    value: function bar(x) {
      return "foo bar " + x;
    }
  }]);

  return Foo;
})();

var foo = new Foo();
console.log(foo.bar("baz"));

Ausführen können wir den Code dann beispielsweise mit node (Serverseite) oder mit phantomjs (Clientseite):

babel --presets=es2015 foo.es6 -o foo.js
node foo.js
> foo bar baz
phantomjs foo.js
> foo bar baz

Und was ist TypeScript?

TypeScript wird von Microsoft entwickelt und ist dabei, ES6 zu implementieren. Zusätzlich ist TypeScript statisch typisiert und beinhaltet u.a. Decorators (ES7-Proposal und im moment noch experimentell). Die Entwicklung der Sprache AtScript wurde vom Angular 2 Team gestartet, dann aber verworfen und die Annotationen (daher der Name der Sprache), wurden als Decorators mit in TypeScript aufgenommen. Den Unterschied zwischen Annotationen und Decorators erläutert Pascal Precht in diesem Blogpost.

Das folgende Venn-Diagramm zeigt die Sprach-Hierarchie auf. Mit der Version 2.0 von TypeScript plant Microsoft ES6 komplett zu implementieren.

JavaScript

 

Verwendung von tsc

Der TypeScript Transpiler kann ebenfalls über npm installiert werden:

npm install -g typescript

Da ES6 zum großen Teil valides TypeScript ist, können wir die o.g. Datei foo.es6 kompilieren. Wir müssen sie vorher jedoch umbenennen, weil tsc sonst meldet, dass die Dateiendung nich unterstützt wird. foo.ts ist eine Kopie von foo.es6.

tsc foo.ts

Das Übersetzungsergebnis finden wir in der datei foo.js:

var Foo = (function () {
    function Foo() {
    }
    Foo.prototype.bar = function (x) {
        return "foo bar " + x;
    };
    return Foo;
})();
var foo = new Foo();
console.log(foo.bar("baz"));

Führen wir die Datei z.B. in Node aus, erhalten wir das gleiche Ergebnis wie mit der Übersetzung mittels babel:

node foo.js
> foo bar baz

Statisches Typsystem

Wie wir sehen, ist die Typisierung in TypeScript nicht verpflichtend (ansonsten hätte der TypeScript Compiler bereits einen Fehler gemeldet). Modifizieren wir das Programm ein wenig, sodass der Parameter von Foo.prototype.bar() typisiert ist (foo-broken.ts):

class Foo {
  bar(x: Number) {
    return `foo bar ${x}`;
  }
}
const foo = new Foo();
console.log(foo.bar("baz"));

Wir erhalten die folgende Meldung bereits zur Compilezeit:

tsc foo-broken.ts 
> foo-broken.ts(7,21): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Number'.
  Property 'toFixed' is missing in type 'String'.

Typen für öffentliche Bibliotheken

Vor allem, wenn man externe Bibliotheken nutzen möchte, sind Typen hilfreich, um möglichst schnell ein Feedback zu bekommen, ob man die Schnittstellen korrekt anspricht. Zu diesem Zweck gibt es eine Sammlung von Typ-Deklarationen für eine Vielzahl an öffentlichen Bibliotheken namens DefinitelyTyped. Mit dem Tool tsd kann man diese relativ einfach in sein Projekt einbinden.

Browserify

Das Tool browserify ermöglicht es, CommonJS-Modulabhängigkeiten aufzulösen und die Dateien zu einem Bundle zusammenzufügen. Dies ermöglicht u.A. node-Module auch auf dem Client wiederzuverwenden und nicht verwendete Module aus dem Bundle auszuschließen.

Installieren können wir browserify über npm:

npm install -g browserify

Desweiteren gibt es die Möglichkeit, Plugins zu verwenden. Die Plugins, die uns im Kontext dieses Artikels interessieren, sind babelify und tsify. Die Plugins funktionieren analog zueinander, also betrachten wir hier nur tsify.

npm install tsify

Sehen wir uns nun die folgenden beiden Dateien an.

export default class Foo {
  bar(x) {
    return `foo bar ${x}`
  }
}
import Foo from "./fooModule"

const foo = new Foo();
console.log(foo.bar("baz"));

Wir können mit browserify und tsify in einem Schritt ein Bundle generieren (und beispielsweise in node ausführen):

browserify app/scripts/main.ts -p tsify | node
> foo bar baz

Warum funktioniert das? tsc kann ES6-Module zu unterschiedlichen Modultypen kompilieren. Darunter befindet sich auch CommonJS. Browserify muss also lediglich die TypeScript-Dateien mit der entsprechenden Konfiguration durch tsc übersetzen lassen und dann kann es die Abhängigkeiten auslesen und diesen Vorgang rekursiv wiederholen.

Workflow und Kollaboration

Zu einem größeren Frontend-Projekt gehört ein Task-Runner. Der Hauptvorteil ist die Automatisierung des Build-Prozesses. Wenn man eine Hand voll Transpiler (less, sass, typescript, babel, …) verwendet, ist es sinnvoll, dass die Entwickler so weit wie möglich von der manuellen Ausführung der Transpiler befreit werden. Dies spart Zeit und vermeidet potenzielle Fehler (vor allem wichtig für die Releases).

Hier verwenden wir exemplarisch Grunt. Empfehlen kann ich das Plugin grunt-browserify. Hier ist eine entsprechende Beispiel-Konfiguration für TypeScript sourcen:

module.exports = (grunt) ->

  grunt.initConfig

    browserify:
      all:
        src: "app/scripts/main.ts"
        dest: "dev/scripts/main.js"
        options:
          browserifyOptions:
            debug: true
            plugin: ["tsify"]

    exorcise:
      dist:
        files:
          "dist/scripts/main.min.js.map": "dev/scripts/main.js"
        options:
          bundleDest: "dist/scripts/main-with-maps.min.js"

    uglify:
      withMaps:
        files:
          "dist/scripts/main-with-maps.min.js": "dist/scripts/main-with-maps.min.js"
        options:
          sourceMap: true
          sourceMapIn: "dist/scripts/main.min.js.map"
          sourceMapName: "dist/scripts/main.min.js.map"
      noMaps:
        files:
          "dist/scripts/main.min.js": "dev/scripts/main.js"

  grunt.loadNpmTasks "grunt-browserify"
  grunt.loadNpmTasks "grunt-exorcise"
  grunt.loadNpmTasks "grunt-contrib-uglify"

  grunt.registerTask "dev", ["browserify"]
  grunt.registerTask "dist", ["browserify", "uglify:noMaps"]
  grunt.registerTask "dist-with-maps", ["browserify", "exorcise", "uglify:withMaps"]

Zu beachten ist hierbei, dass die Source-Maps direkt mit erzeugt werden und alle Sourcen inline in der Zieldatei landen. Dies ist aufgrund der erhöhten Größe natürlich nur für Entwicklungs-Builds zu empfehlen.

Für andere Builds können wir per grunt-exorcise die Source-Maps extrahieren. Wenn das Ergebnis hinterher noch per grunt-contrib-uglify komprimiert wird, kann man mit der Option sourceMapIn die SourceMap weiterverarbeiten (falls man diese im Staging- oder Produktionssystem mit ausliefern möchte).

Mit den folgenden Befehlen können die Builds gestartet werden:

npm install -g grunt-cli
npm install grunt grunt-browserify tsify grunt-exorcise grunt-contrib-uglify
grunt dev dist dist-with-maps

Um ein Ergebnis zu sehen, kann man in der Datei index.html das zu testende Script einkommentieren und in einem Browser, der Source-Maps versteht (z.B. Chrome oder Firefox), öffnen:

<!doctype html>
<html>
  <head>
    <!-- <script type="text/javascript" src="dev/scripts/main.js"></script> -->
    <!-- <script type="text/javascript" src="dist/scripts/main.min.js"></script> -->
    <!-- <script type="text/javascript" src="dist/scripts/main-with-maps.min.js"></script> -->
  </head>
</html>

Fazit

Sowohl mit dem babel-, als auch mit dem TypeScript-Transpiler lässt sich bereits heute schon ein großer Teil der ES6-Features nutzen (die Roadmap findet man hier). Und dies ohne Kompatibilitätseinbußen zu ES5 (TypeScript bietet sogar die Zielversion von ES3 an).

Ich empfehle daher, diese Technologien so bald wie möglich zu verwenden, um ein gutes Know-How auf diesem Gebiet aufzubauen. Denn früher oder später wird dieses Know-How für die moderne Webentwicklung unverzichtbar sein.

Das Statische Typsystem von TypeScript ist vor allem für das Schreiben wiederverwendbarer Komponenten ein großer Schritt in Richtung Robustheit moderner Webapps.

Teilen Sie diesen Beitrag

Das könnte dich auch interessieren …

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert