TypeScript

Die Programmiersprache TypeScript ist eine Erweiterung der dynamisch typisierten Programmiersprache JavaScript um ein statisches Typsystem. Dynamisch typisierte Programmiersprachen sind flexibler, aber dafür auch fehleranfälliger als statisch typisierte Programmiersprachen.

Kann man bei dynamisch typisierten Programmiersprachen den Code direkt durch einen Interpreter ausführen lassen, kommt bei statisch typisierten Programmiersprachen noch ein Kompilationsschritt hinzu. Dank Typinformationen kann der Compiler einige potenzielle Fehler bereits erkennen, bevor das Programm ausgeführt wird. Zudem helfen Typinformationen beim Verständnis des Programmcodes.

Bei der objektorientierten Programmierung werden neue Datentypen auf Basis bestehender Datentypen erstellt. Dank der statischen Typisierung sieht man auf den ersten Blick, aus welchen Datentypen ein neuer Datentyp zusammengesetzt ist.

TypeScript ist eine Obermenge von JavaScript, d.h. jedes JavaScript-Programm ist auch ein TypeScript-Programm, aber nicht umgekehrt.

Variablen und Konstanten

In modernem JavaScript werden Variablen mit dem Schlüsselwort let deklariert:

let fullName = "Jane Done";
let age = 31;
let married = false;

Dabei kann sich der Datentyp jeder Variable dynamisch an den jeweils zugewiesenen Wert anpassen:

let value = "ten";
value = 10;

Dies macht es aber schwer im Programmcode zu erkennen, welche Operationen auf einen Wert angewendet werden können, und welchen Effekt das hat:

console.log(value + value); // 20 or "1010"?

Typannotationen

Darum führt TypeScript sogenannte Typannotationen ein, die nach dem Variablennamen und einem Doppelpunkt aufgeführt werden:

let fullName: string = "Jane Done";
let age: number = 31;
let married: boolean = false;

Dadurch ist es nicht möglich, einer Variablen unpassende Werte zuzuweisen:

age = "still quite young"; // ERROR: Type 'string' is not assignable to type 'number'.

Der Datentyp muss dabei nicht zwingend angegeben werden, denn der Compiler kann diesen anhand des zugewiesenen Wertes ermitteln (Typinferenz).

Konstanten

Eine Variable kann ihren Wert im Programmverlauf verändern:

let fullName: string = "Jane Done";
let age: number = 31;
let married: boolean = false;

fullName = "Jane Doe";
age = 32;
married = true;

Oftmals ist diese Veränderbarkeit nicht erwünscht, da sich bestimmte Werte im Programmverlauf nicht ändern dürfen. In diesem Falle deklariert man Konstanten mit dem const-Schlüsselwort:

const planet: string = "Earth";
const gravity: number = 9.81;
const hasMoon: boolean = true;

Eine erneute Zuweisung würde zu einem Fehler führen:

gravity = 9.99; // ERROR: Cannot assign to 'gravity' because it is a constant.

In JavaScript und TypeScript sollen wann immer möglich Konstanten statt Variablen verwendet werden. So kann man direkt erkennen, welche Werte sich ändern werden und welche nicht.

Grundlegende Datentypen

Die bisher betrachteten Datentypen lauten:

  1. string: eine Zeichenkette
  2. number: eine Zahl (mit oder ohne Nachkommastellen)
  3. boolean: ein Wahrheitswert (true oder false)

Zeichenketten: Verkettung und Interpolation

Zeichenketten oder Strings können als Literalen (z.B. "hello") direkt hingeschrieben oder mithilfe von Programmlogik basierend auf bestehenden Zeichenketten aufgebaut werden.

Zum Verketten von Strings dient der Operator +:

let article: string = "some";
let adjective: string = "faulty";
let noun: string = "program";
let phrase: string = article + " " + adjective + " " + noun;
console.log(phrase); // some faulty program

Diese Syntax kann aber etwas umständlich werden, wenn man wie beim obigen Beispiel nur Leerzeichen zwischen einzelne Wörter einfügen möchte.

Mithilfe von String-Interpolation kann ein mit Backtics umfasster String als Template (Schablone) definiert werden. Werte innerhalb von ${} werden dann als Ausdrücke interpretiert. Das obige Beispiel kann folgendermassen vereinfacht werden:

let phrase: string = `${article} ${adjective} ${noun}`;

Die Leerzeichen stehen nun in der String-Literalen.

Zahlen: Konvertierung von und zu Zeichenketten

Oftmals werden Zahlen als Zeichenketten angegeben (z.B. bei Benutzereingaben) oder müssen als Zeichenketten ausgegeben werden. Dadurch wird eine Konvertierung zwischen den beide Datentypen string und number nötig. Dabei unterscheidet man zwischen zwei Vorgängen:

  1. parsen: Eine Zeichenkette wird in eine Zahl umgewandelt, was scheitern kann.
  2. rendern: Eine Zahl wird in eine Zeichenkette umgewandelt, was immer gelingt.

Eine Zeichenkette kann als Ganz- oder als Fliesskommazahl geparsed werden:

let a = Number.parseInt("1.23"); // 1
let b = Number.parseFloat("1.23"); // 1.23

Kann die Zeichenkette nicht in eine Zahl umgewandelt werden, erhält das Ergebnis den Wert NaN: not a number:

let nan = Number.parseFloat("hello"); // NaN

Die Umwandlung von einer Zahl zu einer Zeichenkette gelingt immer und kann bei Bedarf mit einer Rundung erfolgen:

const priceExact: number = 10 / 3;
const priceRounded: string = priceExact.toFixed(2); // "3.33"
const priceWholeUnits: string = priceExact.toFixed(0); // "3"

Wahrheitswerte und logische Operatoren

Wahrheitswerte oder Booleans können nur true oder false sein. Der Datentyp boolean unterstützt folgende logische Operatoren:

  • !: Negation (logisches Nicht)
  • &&: Konjunktion (logisches Und)
  • ||: Disjunktion (logisches Oder)
const rainy: boolean = false;
const cold: boolean = true;

const sunny = !rainy; // true
const badWeather = rainy || cold; // true
const reallyBadWeather = rainy && cold; // false

Mehr vom Gleichen: Arrays

Arrays erlauben das Abspeichern mehrerer Werte des gleichen Datentyps:

const singlePlanet: string = "Earth";
const solarSystem: string[] = [
  "Sun",
  "Mercury",
  "Venus",
  "Earth",
  "Jupiter",
  "Saturn",
  "Uranus",
  "Neptun",
  "Pluto",
];

Man beachte, dass eine skalare Variable mit dem Typ string annotiert wird, während ein Array von Strings mit der Annotation string[] versehen ist. Die Werte des Arrays werden innerhalb von eckigen Klammern als Liste aufgeführt.

Eine alternative Notation für Arrays sieht folgendermassen aus:

const bigJupiterMoons: Array<string> = ["Io", "Europa", "Ganymede", "Callisto"];

Statt string[] (“string array”) kommt die generische Notation Array<string> (“array of strings”) zum Einsatz. Diese wird uns später im Modul im Zusammenhang mit Typparametern noch begegnen.

Funktionen

In TypeScript werden Funktionsparameter mit einer Typannotation versehen. Der Aufrufer muss dann Argumente der entsprechenden Datentypen übergeben:

function repeat(text: string, n: number): string {
  let result = "";
  for (let i = 0; i < n; i++) {
    result += text;
  }
  return result;
}

Die Funktion repeat erwartet zwei Parameter vom Typ string bzw. number und gibt einen Wert vom Typ string zurück. Entsprechend muss sich der Aufrufer an diese Datentypen halten:

let message = repeat("Ente", 3); // "EnteEnteEnte"

// ERROR: Argument of type 'string' is not assignable to parameter of type 'number'.
message = repeat("x", "infinity"); 

Beachte, dass message nicht mit einer Typannotation versehen worden ist. Dank des deklarierten Rückgabetyps von repeat kann der Compiler dies selber ermitteln.

Funktionsliteralen

JavaScript und TypeScript unterstützen eine weitere Form der Funktionsdeklaration:

const repeat = (text: string, n: number): string => {
  let result = "";
  for (let i = 0; i < n; i++) {
    result += text;
  }
  return result;
};

Hierbei handelt es sich um eine sogenannte Funktionsliterale, die als eine Konstante abgespeichert wird. Vergleichen wir die Syntax der Deklaration:

function repeat(text: string, n: number): string {
  // TODO
}

const repeat = (text: string, n: number): string => {
  // TODO
};

Die obere Notation verlangt, dass jede Funktion einen eigenen Namen hat. Die untere Notation ermöglicht auch anonyme Funktionen, d.h. Funktionen ohne Namen, die dann beispielsweise als Funktionsparameter übergeben werden können.

Verzweigungen

Verzweigungen können in JavaScript und TypeScript mit den Schlüsselwörtern if und else sowie mit deren Kombination else if ausgedrückt werden:

function fizzBuzz(x: number): string {
  if (x % 3 == 0 && x % 5 == 0) {
    return "FizzBuzz";
  } else if (x % 5 == 0) {
    return "Buzz";
  } else if (x % 3 == 0) {
    return "Fizz";
  } else {
    return x.toFixed(0);
  }
}

Werden mehrere Bedingungen auf dem gleichen Ausdruck geprüft, kann dies mit switch/case kompakter formuliert werden:

function convert(amountCHF: number, toCurrency: string): number {
  switch (toCurrency) {
    case "USD":
      return 1.24 * amountCHF;
    case "EUR":
      return 1.06 * amountCHF;
    case "GBP":
      return 0.91 * amountCHF;
    default:
      throw new Error(`no conversion rate CHF to ${toCurrency}`);
  }
}

Dabei ist zu beachten, dass die case-Blöcke explizit terminiert werden müssen: entweder mit return wie oben oder mit break wie im folgenden Beispiel:

function classify(grade: string): void {
  switch (grade) {
    case "A":
      console.log("great");
    case "B":
      console.log("good");
    case "C":
    case "D":
      console.log("passed");
      break;
    default:
      console.log("failed");
  }
}

Da die Funktion keinen Wert zurückgibt, ist sie mit dem Rückgabetyp void annotiert.

Schleifen

JavaScript und TypeScript kennen zwei grundlegende Arten von Schleifen: while und for.

while und do/while

Die while-Schleife kann kopf- oder fussgesteuert verwendet werden. Bei der kopfgesteuerten Variante wird die Bedingung oben geprüft:

let text = "hello";
while (text.length < 10) {
  text = " " + text;
}
console.log(text); // "     hello"

Bei der fussgesteuerten Schleife wird die Bedingung unten geprüft:

const goal: number = 100;
const rate: number = 0.05;
let saved: number = 90;
do {
  saved *= 1 + rate;
} while (saved < goal);
console.log(saved); // 104.18625000000002

Dadurch wird die fussgesteuerte Schleife immer mindestens einmal durchgeführt.

for, for/of, for/in

Betrachten wir folgende while-Schleife:

const rent: number = 1500;
let capital: number = 5000;
let months: number = 0;
while (capital > 0) {
  months++;
  capital -= rent;
}
console.log(`You have ${months} months left for rent.`);

Diese verfügt über eine Initialisierung (capital = 5000), eine Bedingung (capital > 0) und eine Modifikation (capital -= rent). Als Seiteneffekt soll die Anzahl der Monate hochgezählt werden (months++).

Mit der for-Schleife kann die obige kopfgesteuerte Schleife folgendermassen vereinfacht werden:

let months = 0;
for (let capital: number = 5000; capital > 0; capital -= rent) {
  months++;
}
console.log(`You have ${months} months left for rent.`);

Dabei ist zu beachten, dass die Variable capital nur innerhalb der Schleife existiert. Die Variable months, die ausserhalb der Schleife deklariert worden ist, existiert auch nach der Schleife noch.

Die Schleifenvariante for/of kommt bei Arrays zum Einsatz. Ein Array lässt sich grundsätzlich auch mit einer herkömmlichen for-Schleife durchiterieren:

const dudes: string[] = ["Vinnie", "Mickey", "Johnny", "Phil"];
for (let i = 0; i < dudes.length; i++) {
  console.log(`${i + 1}. ${dudes[i]}`);
}

Ausgabe:

1. Vinnie
2. Mickey
3. Johnny
4. Phil

Bequemer ist aber die Variante mit for/of, welche der Schleifenbariable direkt den Wert zuweist. Der Zugriff auf den Index ist dabei nicht mehr möglich:

const dudes: string[] = ["Vinnie", "Mickey", "Johnny", "Phil"];
for (let dude of dudes) {
  console.log(dude);
}

Ausgabe:

Vinnie
Mickey
Johnny
Phil

Mithilfe von for/in kann man auf die Indizes zugreifen. Aber Vorsicht:

for (let i in dudes) {
  console.log(`${i + 1}. ${dudes[i]}`);
}

Ausgabe:

01. Vinnie
11. Mickey
21. Johnny
31. Phil

Die Variable i ist kein Index, sondern ein String! Der Arrayzugriff funktioniert damit trotzdem.