Teil 4: Schneller entwickeln mit TypeScript

TypeScript lernen: So programmiert man TypeScript in der Praxis

Johannes Dienst

©Shutterstock/dotshock

In den vorangegangenen Artikeln dieser Serie wurde ausgiebig über die Möglichkeiten von TypeScript und die Integration in den eigenen Arbeitsauflauf gesprochen. Dieser Teil beschäftigt sich mit dem Praxisalltag. Dazu wurden vor allem Erfahrungen aus erster Hand herangezogen, die zwar nicht alles abdecken, dafür aber authentisch und unverfälscht sind.

In diesem Tutorial lernen Sie Schritt für Schritt die Programmierung mit TypeScript kennen. In fünf Teilen behandeln wir die sprachlichen Grundlagen und die Möglichkeiten der Typisierung kennen und wenden das Erlernte gleich in der Praxis an.

TypeScript lernen – So programmiert man in der Praxis

In den Anfangsjahren wurde über TypeScript entweder müde gelächelt oder sogar offen hergezogen. Inzwischen haben sich die Gemüter aber weitestgehend beruhigt. Nicht zuletzt durch die offene Politik und die Integration der Community in den Entwicklungsprozess herrscht im Allgemeinen ein positives Klima.

Selbst Google und Microsoft arbeiten intensiv zusammen, um die Bedürfnisse von Angular 2, das vollständig in TypeScript neu geschrieben wurde, besser zu befriedigen. Der Schritt des Angular-Teams auf eine Sprache des Konkurrenten zu wechseln, erscheint zuerst nicht nachvollziehbar zu sein. Deswegen werden wir uns in diesem Artikel auch damit beschäftigen, was das Angular-Team dazu bewogen hat, diesen Weg einzuschlagen. In unserem Unternehmen wird inzwischen hauptsächlich TypeScript zur Entwicklung unseres Hauptprodukts eingesetzt. Deswegen werden wir detailliert darauf eingehen, warum wir TypeScript gut finden und an welchen Stellen es in TypeScript hakt.

Google als Partner

Google hatte für Angular 2 ursprünglich geplant, ein eigenes Superset von JavaScript namens AtScript zu veröffentlichen, das Features wie ein Typsystem und Annotationen enthalten sollte. Aus pragmatischen Gründen haben sie sich jedoch dazu entschieden eine Partnerschaft mit Microsoft einzugehen, das die benötigten Erweiterungen in TypeScript integriert. Besonders wichtig dabei waren die Annotationen, die Dinge wie Dependency Injection möglich machen sollten. Diese wurden als sogenannte Decorators implementiert, die für die Annotierung oder die Modifizierung von Klassen und deren Properties verwendet werden können. Das Kernkonzept von Angular 2 ist die sogenannte Komponente, die mit dem Decorator @Component Gebrauch von diesem Feature macht.

Im Praxiseinsatz – Erfahrungen aus erster Hand

Laut Carola Lilienthal kann ein Entwickler nur einen kleinen Teil einer großen Codebasis vollständig detailliert abrufbereit haben. Über den Rest hat er ein grobes Verständnis und kann es bei Bedarf schnell auffrischen. Dieser Tatsache ist es geschuldet, dass die JavaScript-Entwicklung ohne Codevervollständigung und IDE-Unterstützung inzwischen an ihre Grenzen stößt. Als JavaScript noch eine reine Sprache für Webbrowser war, wurde die kritische Marke selten überschritten, an der Entwickler die Komplexität ihrer Codebasis nicht mehr vollständig überblicken konnten. Mit dem Aufkommen von Single-Page Applications, JavaScript Rich Clients sowie Node.js wird JavaScript zunehmend im großen Stil entwickelt. Erst hier zeigt sich die wahre Stärke von Sprachen mit einem ausgereiften Typsystem.

In unserem Praxisalltag kommt es häufig vor, dass Daten aus dem Backend als JSON geliefert werden. JSON ist hochkomplex mit verschachtelten Unterobjekten. Häufig musste man daher während der Programmierung in der Spezifikation nachsehen, welche Eigenschaften die Objekte besitzen. Noch dazu kam, dass es subtile Unterschiede zwischen ähnlich aufgebauten Objekten gab, die man nicht im Kopf behalten konnte. Zusätzlich bestand neben der Gefahr einer fehlerhaften Spezifikation auch der menschliche Fehlerfaktor. Zu leicht wurde etwas falsch interpretiert oder es schlichen sich Schreibfehler ein. Diese wurden erst zur Laufzeit bemerkt oder fielen ganz unter den Tisch. Inzwischen werden für Objekte aus dem Backend ausschließlich Interfaces definiert, die ein Abbild der Spezifikation darstellen. In der eigentlichen Businesslogik wird mit einem Objekt gearbeitet, dass dieses Interface als Typ hat. Es entfallen deshalb die Fehler, die durch manuelle Durchsicht der Spezifikation entstehen. Außerdem wird die Entwicklung effizienter, da eine gesuchte Property in einem Bruchteil der Zeit über die Autovervollständigung gefunden werden kann. Interessanterweise empfinden selbst erfahrene Webentwickler in unserem Team die Idee eines statischen Typsystems mit IntelliSense als angenehm.

Finde den Fehler mit Source Maps

Zur Einführung von TypeScript in unserem Unternehmen kam immer wieder die Frage auf, wie der TypeScript-Code debuggt werden kann. Ein Ansatz ist der, das erzeugte JavaScript selbst zu debuggen. Das ist insofern möglich, da der TypeScript-Compiler sauber formatiertes und für Menschen lesbares JavaScript erzeugt. Trotzdem wird die Fehlersuche dadurch erschwert, dass die gefundenen Codestellen auf die richtigen Stellen in TypeScript übertragen werden müssen. Das ist durch sogenannte Source Maps möglich. Sie sorgen dafür, dass direkt – z. B. im Browser – der TypeScript-Code im Debugger durchgegangen werden kann. Der Compiler ist in der Lage, mit einem Flag die passenden Source Maps zu generieren. Das beschleunigt die Fehlersuche allgemein.

Das Problem mit den Namespaces

Zu Beginn von TypeScript gab es eine Unterscheidung zwischen internen und externen Modulen. Leider war die Schreibweise identisch und führte dadurch zu Verwirrung. Mit dem Release 1.5 wurden die internen Module durch Namespaces ersetzt, die mit dem Schlüsselwort namespace beginnen. Die Idee dahinter war, TypeScript-interne Module zu schaffen, um den Code besser zu strukturieren. Die Idee wurde für unsere Codebasis damals als gut befunden, inzwischen sind aber nur noch echte ECMAScript2015-Module im Einsatz, denn es gibt durch Namespaces zwei Nachteile, die in den folgenden Absätzen gezeigt werden sollen. Listing 1 implementiert ein leicht abgespecktes und angepasstes Beispiel aus der Dokumentation. Es wird ein Namespace de.multamedio.Validation definiert, der eine Klasse LettersOnlyValidator exportiert.

namespace de.multamedio.Validation {
  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersOnlyValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }
}

Der Compiler erzeugt daraus das JavaScript in Listing 2. Hierbei handelt als sich um „Immediately invoked Function Expressions“. Diese sind eine logische Wahl, um Namespaces abzubilden, da sie praktisch verhindern, dass der globale Kontext mit den in ihnen enthaltenen Variablen verschmutzt wird. Nun kann man sich aber leicht vorstellen, dass der Zugriff auf die Klasse LettersOnlyValidator äußerst ineffizient ist, da man über mehrere Ebenen gehen muss.

var de;
(function (de) {
  var multamedio;
  (function (multamedio) {
    var Validation;
    (function (Validation) {
      var lettersRegexp = /^[A-Za-z]+$/;
      var numberRegexp = /^[0-9]+$/;
      var LettersOnlyValidator = (function () {
        function LettersOnlyValidator() {
        }
        LettersOnlyValidator.prototype.isAcceptable = function (s) {
          return lettersRegexp.test(s);
        };
        return LettersOnlyValidator;
      }());
      Validation.LettersOnlyValidator = LettersOnlyValidator;
    })(Validation = multamedio.Validation || (multamedio.Validation = {}));
  })(multamedio = de.multamedio || (de.multamedio = {}));
})(de || (de = {}));

Außerdem gibt es noch einen weiteren Nachteil: Die Klassen können nur über den voll qualifizierten Namespace angesprochen werden:

let validator = new de.multamedio.Validation.LettersOnlyValidator();

In der Dokumentation wird darauf hingewiesen, dass ein sogenannter Alias erstellt werden kann. Die Anwendung könnte z. B. folgendermaßen aussehen:

import validation = de.multamedio.Validation;
let validator = new validation.LettersOnlyValidator();

Das sieht zuerst einmal nicht verkehrt aus. Legt man aber in einer anderen Datei einen gleichnamigen Alias an, dann beschwert sich der Compiler:

// Error: Duplicate identifier 'validation'
import validation = de.multamedio.Validation;

Abhilfe schafft hier der Umstieg auf Module, den wir unbedingt empfehlen. Am Besten benutzt man ECMAScript2015-Module; damit ist dann die Zukunftsfähigkeit der Codebasis gesichert. Listing 3 portiert die Funktionalität des Namespaces in ein Modul.

export class LettersOnlyValidator {
  private lettersRegexp = /^[A-Za-z]+$/;

  isAcceptable(s: string) {
    return this.lettersRegexp.test(s);
  }
}

Dieses Modul kann in jeder beliebigen Datei importiert werden, sogar unter dem gleichen Alias:

import * as val from "./validator_module";
new val.LettersOnlyValidator();

Abwärtskompatibilität

Die Releases von TypeScript erscheinen in regelmäßigen Abständen. Normalerweise kann damit gerechnet werden, dass eine neue Version mindestens jedes Quartal zur Verfügung steht. Das Problem ist, dass es bei vielen Sprachen zu Breaking Changes kommt, die ein Upgrade unmöglich machen, da es schlicht und einfach zu teuer ist. Dieses Problem existiert bei TypeScript selten. So wurden zum Release 1.5 interne Module zu Namespaces umgewandelt. Ansonsten wurden bis jetzt mehrere Versionen ohne Probleme intern von uns migriert. Der größte Sprung in einem Projekt war von Version 1.4 nach Version 2.0 – alles, ohne auch nur eine Zeile Code ändern zu müssen (von Namespaces abgesehen). Das TypeScript-Team leistet hier aus unserer Sicht eine Menge, wenn man bedenkt, dass jedes Release nicht nur neue Features, sondern auch umfangreiche Compilerverbesserungen enthält.

Richtig in den Workflow integrieren

Wie bereits im letzten Artikel beschrieben, integriert sich TypeScript inzwischen fast vollständig in die gängigen Workflows von JavaScript. Diese basieren auf npm und Node.js und den entsprechenden Packages zur Build-Automatisierung. Das ebenfalls seit TypeScript 2.0 eingeführte, voll in npm integrierte Management von Definitionsdateien funktioniert zumindest bei uns reibungslos. Dadurch wird die Handhabung von Typdefinitionen von Fremdbibliotheken erheblich vereinfacht. Es ist praktisch kein großer Aufwand, TypeScript in seinen bisherigen JavaScript-Workflow zu integrieren.

React oder TypeScript?

Interessant ist, dass vor allem die React-Community TypeScript zu Beginn nicht uneingenommen gegenüberstand. Noch interessanter ist aber, dass TypeScript als echtes Superset von JavaScript implementiert ist. So wird nicht nur mit dem Angular-Team kooperiert. In Zusammenarbeit mit dem React-Team wurde sogar eine eigene Dateiendung .tsx implementiert, die es erlaubt, getypte .jsx-Dateien zu schreiben. Die Diskussion React vs. TypeScript ist damit eigentlich hinfällig und entbehrt jeder Grundlage. Zumal TypeScript als Obermenge von JavaScript keine statische Typisierung fordert, sondern sie nur optional zur Verfügung steht. Es bietet sich also wie so oft an, zu analysieren, welche Vorteile TypeScript bringen könnte und diese, wenn nötig, in seine React-Anwendung zu integrieren.

Fazit

TypeScript ist im Praxiseinsatz sehr unkompliziert und hat sich in unserem Unternehmen seit mehr als zwei Jahren bewährt. Die Entwicklungsgeschwindigkeit ist durch das Sicherheitsnetz der optionalen statischen Typisierung spürbar gestiegen. Interessant war zu sehen, wie selbst eingefleischte Webentwickler auf das Konzept von statischer Typisierung angesprungen sind und es als willkommene Unterstützung ihrer Arbeit durch IntelliSense und schneller Codenavigation angenommen haben. Keiner würde es inzwischen wieder hergeben wollen.

Es gibt aber einen Wermutstropfen, der sich leider auch in unserer Codebasis wiedergefunden hatte: TypeScript bringt ein eigenes Modulkonzept für interne Module mit sich: Namespaces. Diese führen zu unschönen Konstruktionen, da sie nicht wie normale Module importiert werden können. Für das Debuggen besteht seit Anfangszeiten die Möglichkeit, Source Maps zu generieren, mit denen direkt TypeScript-Code analysiert werden kann.

TypeScript ist in den letzten beiden Jahren näher an die JavaScript-Community herangerückt. Das Ziel ist die vollständige Integration in bestehende Workflows, vor allem in Hinblick auf die Paketverwaltung mit npm. Außerdem ist es Framework-agnostisch ausgelegt, und die Entwicklung orientiert sich neben dem (zukünftigen) ECMAScript-Standard ebenfalls an den Wünschen der Community. Es existiert daher nicht nur eine enge Zusammenarbeit mit dem Angular-Team, dessen Angular 2 in TypeScript geschrieben wurde. Auch React-Entwickler haben die Möglichkeit, in TSX-Dateien getypten React-Code zu schreiben und darin Vorteile statischer Typisierung zu nutzen.

Geschrieben von
Johannes Dienst
Johannes Dienst
Johannes Dienst ist Clean Coder aus Leidenschaft bei der DB Systel GmbH. Seine Tätigkeitsschwerpunkte sind die Wartung und Gestaltung von serverseitigen Java- und JavaScript-basierten Applikationen. Twitter: @JohannesDienst
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: