Inhaltsverzeichnis


Nichts verpassen?

angularjs ÔÇó typescript | Sascha Brink ÔÇó | 10 Minuten

Ich m├Âchte euch in diesem Artikel zeigen, wie man schon heute mit AngularJS 1.X Anwendungen mit TypeScript entwickeln kann. TypeScript ist eine Sprache die von Microsoft entwickelt wurde und auf ES2015 (fr├╝her auch mal ES6 oder Harmony genannt) aufsetzt und um Typen erweitert.

Typen in JavaScript?

Dabei wird TypeScript nach JavaScript transformiert. Was im Browser ausgef├╝hrt wird ist also ganz normales JavaScript(ohne Typen). Das sch├Âne ist, dass in TypeScript die Typen optional sind. So k├Ânnen wir zun├Ąchst normales JavaScript schreiben und nur die Typen erg├Ąnzen.

Um TypeScript zu JavaScript zu kompilieren muss zun├Ąchst TypeScript installiert werden

$ npm install -g typescript

und eine Konfiguration f├╝r das Projekt angelegt werden. Die Konfiguration ist in die Datei tsconfig.json zu schreiben.

{
  "compilerOptions": {
    "sourceMap": true
  },
  "exclude": [
    "node_modules"
  ]
}

Schauen wir uns das in einem Beispiel an:

var coolFramework = 'AngularJS'
var thingsILearned = [coolFramework, 'TypeScript', 'How to survive']

Das sieht aus wie normales JavaScript, ist aber eben auch valides TypeScript. Nun f├╝gen wir einmal Typen an. (Zum Ausprobieren)

var coolFramework: string = 'AngularJS'
var thingsILearned: string[] = [coolFramework, 'TypeScript', 'How to survive']

Damit legen wir fest, dass coolFramework immer vom Typ string sein muss. Doch Achtung, coolFramework darf auch die Werte undefined und null einnehmen. In thingsILearned definieren wir ein Array aus Strings. Alternativ h├Ątten wir auch Array<string> schreiben k├Ânnen.

TypeScript erlaubt uns au├čerdem Klassen und Interfaces zu definieren.

interface IHuman {
  name: string
  size: number
  saySomething(sentence: string): void
}

class Human implements IHuman {
  constructor(
    public name: string,
    public size: number
  ) {}

  saySomething(sentence: string) {
    console.log(sentence)
  }
}

Wenn wir aber normale JavaScript-Frameworks einbinden wollen, sind nat├╝rlich hierf├╝r keine Typen f├╝r dieses Framework gesetzt. Daf├╝r gibt es so genannte TypeDefinitions mit der Dateiendung .d.ts.. Damit wir nicht jedes mal die Definitionen von Hand neu schreiben m├╝ssen, gibt es daf├╝r eine gro├če Registry, DefinitelyTyped, in der wir auch die TypeDefinition von AngularJS und vielen anderen Frameworks finden.

AngularJS mit TypeScript

Wir starten mit unserem AngularJS-TypeScript-Seed auf GitHub. Hier sind alle n├Âtigen Schritte, wie das konfigurieren des Compilers und das laden der TypeDefinitions bereits erledigt und wir k├Ânnen uns auf das wesentliche konzentrieren.

Schauen wir uns die index.html an, sehen wir eine kleine Besonderheit. In diesem Seed setzten wir SystemJS als Module Loader ein um unsere Datein direkt laden zu k├Ânnen und im Browser vom TypeScript zu JavaScript zu ├╝bersetzten. So k├Ânnen wir hier auf einen Build-Prozess verzichten. Wir importieren als Startpunkt also die Datei, die in app/app.ts liegt. Die Dateiendung deutet auf TypeScript hin und wird von SystemJS automatisch angef├╝gt. Schauen wir uns also die Datei mal genauer an.

import * as angular from 'angular'
import 'angular-route'

import routes from './routes'

import navigation from './components/navigation/navigation'
import booksIndex from './components/books-index/books-index'
import booksShow from './components/books-show/books-show'

angular.module('myApp', [
  'ngRoute',
  routes,
  navigation,
  booksIndex,
  booksShow
])

Diese Datei k├Ânnte genau so gut ein ES2015-Modul sein. Hier versteckt sich kein spezifischer TypeScript-Code

Eine Component in TypeScript

Schauen wir uns also mal die entscheiden Dateien in diesem Projekt genauer an. In der Datei app/components/books-index/books-index.ts finden wir zun├Ąchst eine class mit den Namen BooksIndex. Das ist unser Controller f├╝r die gleichnamige Komponente die wir mit component(ÔÇŽ) definiert haben.

import * as angular from 'angular'

import booksApiModule, {IBook, IBooksApi} from '../../services/books-api/books-api'

class BooksIndex {
  books: IBook[]

  constructor (booksApi: IBooksApi) {
  booksApi.all()
    .then(books => this.books = books)
  }
}

const moduleName = 'myApp.books-index'
export default moduleName

angular.module(moduleName, [booksApiModule])
  .component('booksIndex', {
    templateUrl: 'app/components/books-index/books-index.html',
    controller: BooksIndex,
    controllerAs: 'booksIndex'
  })

In dem Controller steckt nun einiges an TypeScript. Schauen wir uns das mal Zeile f├╝r Zeile an.

class BooksIndex {
  books: IBook[]

Wir definieren eine Klasse mit dem Namen BooksIndex, in der es ein Attribut mit dem Namen books gibt. Standardm├Ą├čig ist dieser Parameter public, also darf man darauf auch bei einer Instanz zugreifen. books ist vom Typ IBook[] - das IBook importieren wir uns aus dem book-api (unserem Service mit unserem Backend zu kommunizieren). Die []-Klammern zeigen an, dass es sich hier um ein Array handelt.

constructor (booksApi: IBooksApi) {

Als n├Ąchstes folgt der constructor(), der automatisch aufgerufen wird wenn die Klasse instanziert wird. In den Parametern finden wir unseren Service wieder. Wir nutzen hier nat├╝rlich auch hier die Dependency Injection von AngularJS. Da TypeScript den Typen aber nicht ableiten kann und booksApi standardm├Ą├čig vom Typ any w├Ąre geben wir hier die Klasse als den Typ IBooksApi an. Generell werden Typen immer mit einem Nachgestellten Doppelpunkt angegeben. Im constructor() laden wir alle B├╝cher und speichern sie im eben definierten Attribute books.

booksApi.all()
  .then(books => this.books = books)

Innerhalb des .then() nutzen wir eine weitere ES2015-Technik: Die Arrow-Function. Sie verk├╝rzt zum einen die Schreibweise, zum anderen bindet sie das this des Eltern-Scope in die neue Funktion. Ihr seid bestimmt mit der controllerAs Syntax schon einige male auf dieses Verhalten gesto├čen. Eine alternative ohne Fat-Arrow k├Ânnte so aussehen:

booksApi.all()
  .then(function(books) { return this.books = books }.bind(this))

Wir exportieren unsere Modulnamen als mit export default um die Abh├Ąngigkeiten einfacher Verwalten zu k├Ânnen. Au├čerdem registrieren wir eine neue component mit Hilfe von .component(). Da wir die Type Definitions nutzen, ist die Methode component() bekannt und wir bekommen je nach Editor oder IDE sogar eine Code Completion.

Code Completion mit Code

Services

Beim Umgang mit Services spielt TypeScript seine gro├čen St├Ąrken aus. In Services implementieren wir unsere Gesch├Ąftslogik, wir laden Daten und ver├Ąndern diese. Schauen wir uns unseren booksApi-Service in app/services/books-api/books-api.ts mal genauer an.

import * as angular from 'angular'

export interface IBook {
  title: string
  subtitle: string
  publisher: {
    name: string
    url: string
  }
  // ...
}

Wir definieren hier das eben im Controller schon genutzte IBook, das Interface der Daten die uns der Server schickt.

export interface IBooksApi {
  all(): angular.IPromise<IBook[]>
  getByIsbn(isbn: string): angular.IPromise<IBook>
}

Au├čerdem schaffen wir uns ein Interface f├╝r die Klasse die wir implementieren werden. So k├Ânnen wir die Implementierung auswechseln k├Ânnen aber sicher sein, dass die API sich nicht ver├Ąndern wird. Interessant sind hier die R├╝ckgabewerte der Funktionen. Bei der Methode all() erwarten wir, dass ein Promise zur├╝ckgegeben wird der im Erfolgsfall ein Array aus B├╝chern zur├╝ckgibt. Das Interface angular.IPromise<T> nennt man Generic.

Erst dann folgt unser eigentlicher Service, den wir ebenfalls als Klasse definieren.

class HttpBooksApi implements IBooksApi {
  private baseUrl: string = 'http://bookmonkey-api.angular.de/books'

  constructor(
    private $http: angular.IHttpService
  ) {}

Das Attribute baseUrl definieren wir als private. Dies sch├╝tzt uns davor, dass die baseUrl von au├čen ge├Ąndert werden kann. Dann folgt unser constructor(), der wieder Services mittels DI injectet. Das Schl├╝sselwort private sorgt daf├╝r, dass wir innerhalb der Klasse mit this.$http auf den Service zugreifen k├Ânnen. Gleiches w├╝rde auch mit public funktionieren, dann w├Ąre der Wert auch von Au├čen zug├Ąnglich. Auch hier ben├Âtigen wir wieder einen Typ. Die Type Definition f├╝r Angular folgt dem Schema angular.I<Name des Service ohne $>Service.

public all() {
  return this.$http.get<IBook[]>(this.baseUrl)
    .then(booksResponse => booksResponse.data)
}

Anschlie├čend implementieren wir unsere Methoden die vom Interface gefordert werden. Methoden sind(wie Attribute auch) standardm├Ą├čig public, eine explizite Notation ist aber zu empfehlen, so wird auch f├╝r TypeScript-Neulinge deutlich.

Wir senden unseren $http-Request an unsere API ab. Der Request liefert ein Array aus B├╝chern zur├╝ck, das k├Ânnen wir direkt im Request mit beschreiben. Zuletzt m├Âchten wir noch die Daten ├╝bergeben. Die Arrow Function hat ein implizites return wenn nach dem Pfeil kein neuer Block folgt. Dadurch ist die Schreibweise sehr kurz.

angular.module(moduleName, [])
  .service('booksApi', HttpBooksApi)

Nat├╝rlich m├╝ssen wir unseren Service noch bei Angular anmelden. Daf├╝r nutzen wir den service-Service der auf die factory aufsetzt und unsere Klasse instanziiert.

Kann ich TypeScript schon heute nutzen?

Die Frage, ob man TypeScript mit Angular im 1er-Zweig nutzen kann, ist ganz klar mit JA! zu beantworten. Der Core von Angular wird in TypeScript geschrieben sein, so dass viele Projekte in Zukunft sicherlich auch TypeScript nutzen werden. TypeScript hilft euch, viele Fehler zu vermeiden und unterst├╝tzt dabei noch durch eine Code Completion. Die Unaufdringlichkeit von TypeScript (Typen sind nicht verpflichtend) hilft besonders Einsteigern, welche bereits Programmiersprachen wie Java oder C# kennen.