Typsicher und komfortabel mit TypeScript

Seite 3: Hilfreich: Utility-Typen

Inhaltsverzeichnis

TypeScript bringt einige generische Typen mit, die aus einem Typ, der ein Objekt beschreibt, einen anderen Typ erzeugen. Der neue kann beispielsweise dieselben Properties wie der Ausgangstyp haben, die aber beispielsweise alle als optional oder readonly gekennzeichnet sind. Folgender Code zeigt zwei Utility-Typen:

// wie oben gesehen
type Person = {
  firstname: string;
  lastname: string;
  age?: number;
}; 

function patch(person: Readonly<Partial<Person>>) {
  person.firstname = "Klaus"; 
  // Fehler: Cannot assign to 'firstname'
  // because it is a read-only property.

  // Alle Eigenschaften aus Person sind hier optional,
  // deswegen fuehrt folgender Aufruf zu einem Fehler,
  // obwohl lastname im Person-Type nicht als optional
  // gekennzeichnet ist
  person.lastname.toUpperCase(); 
  // Fehler: Object is possibly 'undefined'

  // weitere Implementierung ausgelassen
}

patch({
  firstname: "Klaus", // OK
  age: 32 // OK
}) // OK, auch ohne lastname, weil alle 
// Eigenschaften optional gemacht wurden

patch({
  // Alle Eigenschaften im Objekt sind optional,
  // aber ihre urspruenglichen Typen bleiben
  // ansonsten erhalten
  lastname: null 
  // Fehler: Type 'null' is not assignable
  // to type 'string | undefined'
})

Die Funktion patch soll ein Objekt vom Typ Person an einen Server schicken. Der Aufrufer soll allerdings in der Lage sein, nur die Teile des Person-Objekts zu übergeben, die er auf dem Server speichert. Der Typ des Parameters person muss somit eine Untermenge von Person sein. Damit Entwicklerinnen und Entwickler den neuen Typ nicht manuell definieren müssen, können sie mit Partial automatisch einen Typ erzeugen, der alle Properties des ursprünglichen Typs enthält, die aber alle als optional deklariert sind. Der Utility-Typ ReadOnly sorgt dafür, dass die patch-Funktion das übergebene Objekt darüber hinaus nicht verändern kann. Er deklariert alle Properties als readonly. Damit führt der schreibende Zugriff auf die Felder zu einem Compile-Fehler.

Autovervollständigung für Ausprägungen eines mit keyof erzeugten Union Type (Abb. 3)

Generics kennt TypeScript ebenfalls. Die im folgenden Listing gezeigte addListener-Funktion soll einem beliebigen Objekt einen Listener für eine ebenso beliebige Property hinzufügen. Dazu erwartet sie einerseits einen Parameter mit dem Objekt, der in generischer Form als Typ Argument in spitzen Klammern beschrieben ist. Der zweite Parameter erwartet den Namen einer Property aus diesem Objekt. Dazu dient der keyof-Operator von TypeScript, der einen Union Type (siehe Abbildung 3) zurückgibt, dessen Ausprägungen aus den Keys eines Objekts bestehen. Ein Beispiel dafür ist ebenfalls im Listing zu sehen: Der Typ PersonKeys ist ein Union Type aus den Strings firstname, lastname und age, sodass eine Variable des Typs nur einen dieser drei Werte annehmen kann.

// Beispiel 1: keyof-Operator
//   PersonKeys kann nur ein Key-Name aus dem Person-Objekt sein
type PersonKeys = keyof Person;
let lastname: PersonKeys = "lastname"; // OK
let city: PersonKeys = "city"; // Fehler: Type '"city"' 
                               // is not assignable to
                               // type 'keyof Person'

// Beispiel 2: generische Funktion mit keyof-Operator
function addListener<O extends object>(o: O, 
                                       propertyName: keyof O) {
  // Implementierung ausgelassen
}

const susi2 = {
  firstname: "Susi",
  lastname: "Meier",
};

addListener(susi2, "firstname"); // OK

addListener(susi2, "age"); 
// Fehler: Argument of type '"age"' is not assignable
// to parameter of type '"firstname" | "lastname"'

Die addListener-Funktion verwendet keyof, um sicherzustellen, dass der zweite Parameter ein String ist, dessen Wert einem der Keys entspricht, die in dem übergebenen Objekt enthalten sind. Wenn der Aufrufer ein Objekt übergibt und einen String, der nicht einem der Key-Namen entspricht, gibt es einen Compile-Fehler, und eine IDE kann Autovervollständigung für Aufrufe bieten.

Mit Generics und Utility-Typen lässt sich eine Vielzahl typischer JavaScript-Programmiermuster abbilden und typsicher beschreiben. Sofern die Utility-Typen nicht ausreichend sind, existiert eine Art Meta-Sprache auf Ebene des Typsystems. Mit ihr lassen sich individuelle abgeleitete Typen erstellen, die in TypeScript Mapped Types heißen.

Folgender Code zeigt eine validate-Funktion, die ein beliebiges Objekt validieren soll. Sie soll ein Objekt zurückliefern, das genauso aussieht wie das übergebene Objekt, mit dem Unterschied, dass alle Felder zwar denselben Namen, aber den Typ boolean haben. Dessen Wert drückt aus, ob das Feld erfolgreich validiert werden konnte. Der ValidatedObject-Type beschreibt ein solches Objekt. Er iteriert über alle Properties in einem bestehenden Objekt, das als Typargument übergeben wird, und setzt in dem neuen Objekt den Typ aller Felder des Original-Objektes auf den Typ boolean.

type ValidatedObject<O> = {
  [Key in keyof O]: boolean
}

function validate<O extends object>(object: O): 
  ValidatedObject<O> {
  // Implementierung ausgelassen
}


let validatedObject = validate({
  firstname: "Susi",
  lastname: "Meier",
});

let vF: boolean = validatedObject.firstname; 
// OK: firstname ist jetzt boolean
let vL: string = validatedObject.lastname; 
// Fehler: Type 'boolean' is not assignable to type 'string'
let vC: boolean = validatedObject.city; 

Wer eine Anwendung mit TypeScript baut, muss potenziell Libraries verwenden, die in JavaScript implementiert sind. In solchen Fällen kommt einem eine wesentliche Designentscheidung von TypeScript zugute: Die Programmiersprache soll nicht nur mit TypeScript-, sondern auch mit JavaScript-Code zusammenarbeiten. Das bezieht sich ebenso auf eigenen JavaScript-Bestandscode wie auf JavaScript-Libraries.

TypeScript ausprobieren

Der TypeScript Playground vermittelt einen ersten Eindruck der Eigenschaften von TypeScript.

Um TypeScript ohne Installation auszuprobieren, bietet sich der TypeScript Playground an. Dabei handelt es sich um einen Online-TypeScript-Editor, der über das typische Tooling verfügt, insbesondere Autovervollständigung und die Ausgabe von Compile-Fehlern. Außerdem kann der Editor anzeigen, wie der Code aussehen würde, wenn man ihn nach JavaScript übersetzt.

Um die Bibliotheken typsicher mit TypeScript zu verwenden, ohne sie auf TypeScript umzustellen, lassen sich Typdeklarationen erstellen: TypeScript-Beschreibungen der API einer Bibliothek. Dazu gehören in erster Linie ihre exportierten Funktionen, Objekte und Klassen. Wenn eine Bibliothek die Deklarationen nicht bereitstellt, lassen sie sich extern pflegen und in einem eigenen Repository zur Verfügung stellen. Das Projekt Definitely Typed bietet eine umfangreiche Auswahl von Typdeklarationen für gängige und weniger verbreitete JavaScript-Bibliotheken an. Sogar die offiziellen Typdeklarationen für React finden sich dort. Die Entwicklung und Pflege hat nicht das React-Team, sondern eine Community übernommen.

Da weder Browser noch Node.js TypeScript-Code ausführen können, gilt es vor dem Start, den Code zu kompilieren. Der Compiler überprüft zunächst die korrekte Verwendung der Typen und erzeugt danach ausführbaren JavaScript-Code. Dabei entfernt er im Wesentlichen die Typannotationen, die nicht dem JavaScript-Sprachstandard entsprechen. Daher gibt es zur Laufzeit keinen Weg, in ähnlicher Weise auf die Typinformationen zuzugreifen. Etwas Ähnliches wie  die Reflection-API von Java ist somit nicht verfügbar.