Generics

Generics in TypeScript (Teil 1)

Der Schwerpunkt bei der Softwareentwicklung liegt darauf, Komponenten zu erschaffen, die nicht nur über gut definierte und einheitliche Schnittstellen verfügen, sondern auch wiederverwendbar sind. Komponenten, die in der Lage sind, sowohl mit aktuellen als auch zukünftigen Daten zu arbeiten, bieten die größte Flexibilität beim Aufbau großer Software-Systeme.

In Programmiersprachen wie C# und Java ist eine der wichtigsten Techniken für die Erstellung wiederverwendbarer Komponenten die Verwendung von Generics. Das bedeutet, dass man eine Komponente erschaffen kann, die mit verschiedenen Typen arbeiten kann, anstatt nur mit einem bestimmten Typ. Dies ermöglicht es den Anwendern, diese Komponenten zu nutzen und ihre eigenen Typen zu verwenden.

Der Einstieg in Generics

Beginnen wir mit dem Grundbeispiel der Generics: der Identitätsfunktion. Die Identitätsfunktion ist eine Funktion, die das zurückgibt, was ihr übergeben wird. Du kannst dir das vorstellen wie den Befehl echo.

Ohne die Verwendung von Generics müssten wir der Identitätsfunktion entweder einen spezifischen Typ zuweisen:

function identity(arg: number): number {
  return arg;
}

Alternativ könnten wir die Identitätsfunktion mit dem Typ any beschreiben:

function identity(arg: any): any {
  return arg;
}

Wir haben gesehen, dass die Verwendung von any zwar generisch ist, aber bei der Rückgabe verlieren wir Informationen über den tatsächlichen Typ. Stattdessen benötigen wir eine Methode, um den Typ des Arguments zu erfassen und zu definieren, was zurückgegeben wird. Hier kommt die Typvariable ins Spiel, eine besondere Art von Variablen, die sich auf Typen und nicht auf Werte konzentriert.

function identity<Type>(arg: Type): Type {
  return arg;
}

Wir haben eine Typvariable mit dem Namen Type zur Identitätsfunktion hinzugefügt. Diese Art hilft uns, den von dem Benutzer bereitgestellten Typ zu erfassen, wie zum Beispiel eine Zahl. Dadurch können wir später auf diese Information zurückgreifen. In diesem Fall wird die Type erneut als Rückgabetyp verwendet. Wenn man genauer hinsieht, kann man erkennen, dass der gleiche Typ für das Argument und den Rückgabetyp verwendet wird. Dies ermöglicht es uns, diese Typ-Information auf einer Seite der Funktion einzuführen und auf der anderen Seite wieder herauszuführen.

Wir bezeichnen diese Version der Identitätsfunktion als generisch, da sie über eine Vielzahl von Typen hinweg funktioniert. Im Gegensatz zur Verwendung von any, bei der manche Informationen verloren gehen können, ist diese generische Version genau so präzise wie die erste Identitätsfunktion, die Zahlen für das Argument und den Rückgabetyp verwendet hat.

Nachdem wir die generische Identitätsfunktion geschrieben haben, kann sie auf zwei Arten aufgerufen werden. Der erste Weg besteht darin, alle Argumente, einschließlich des Typ-Arguments, an die Funktion zu übergeben.

let output = identity<string>("myString");

Hier definieren wir den Type explizit als String, indem wir eines der Argumente an die Funktion übergeben und es mit den <>-Zeichen um die Argumente kennzeichnen, anstatt mit den üblichen ()-Zeichen.

Eine weitere Möglichkeit ist die Verwendung der Typenargument-Inferenz. Hierbei überlassen wir dem Compiler, den Wert von Type automatisch anhand des Typs des übergebenen Arguments zu setzen.

let output = identity("myString");

Bitte beachte, dass es nicht erforderlich war, den Typ explizit in den Winkelklammern anzugeben. Der Compiler hat einfach den Wert myString betrachtet und den Typ auf die entsprechende Art festgelegt. Trotz dessen, dass die Typargument-Inferenz ein nützliches Mittel sein kann, um den Code kürzer und verständlicher zu gestalten, kann es in manchen Fällen notwendig sein, die Typargumente explizit anzugeben, wie dies im vorherigen Beispiel gezeigt wurde. Dies kann bei komplexeren Anwendungsfällen der Fall sein, wenn der Compiler den Typ nicht automatisch bestimmen kann.

Das Arbeiten mit Generics

Wenn du anfängst, mit Generics zu arbeiten, wirst du feststellen, dass der Compiler bei der Erstellung von Funktionen wie identity dafür sorgt, dass du die generisch typisierten Parameter innerhalb des Funktionsrumpfes korrekt verwenden. Dies bedeutet, dass du die Parameter so behandeln müssen, als könnten sie jeder und jeder Typ sein. Der Compiler stellt sicher, dass du die generischen Typen korrekt in Ihrem Code verwenden.

Lass uns die Identitätsfunktion, die wir bereits kennengelernt haben, näher betrachten:

function identity<Type>(arg: Type): Type {
  return arg;
}

Was passiert, wenn wir bei jedem Aufruf der Funktion auch die Länge des Arguments arg in der Konsole protokollieren möchten? Es könnte verlockend sein, folgendes zu schreiben:

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length); // Property 'length' does not exist on type 'Type'.
  return arg;
}

Wenn wir den oberen Code-Block ausführen, wird der Compiler uns einen Fehler melden, weil wir das .length-Mitglied von arg verwenden, ohne dass wir gesagt haben, dass arg dieses Mitglied hat. Wir sollten uns daran erinnern, dass wir früher erwähnt haben, dass diese Typparameter für jeden möglichen Type stehen und jemand, der diese Funktion nutzt, stattdessen auch eine Zahl übergeben könnte, die kein .length-Mitglied hat.

Angenommen, wir haben eigentlich beabsichtigt, dass diese Funktion mit Arrays des Type arbeitet und nicht direkt mit dem Type. Da wir mit Arrays arbeiten, sollte das .length-Mitglied verfügbar sein. Wir können dies so beschreiben, wie wir Arrays anderer Typen erstellen:

function loggingIdentity<Type>(arg: Type[]): Type[] {
  console.log(arg.length);
  return arg;
}

Der Typ von der Funktion loggingIdentity kann als „eine generische Funktion, die den Typparameter Type und ein Argument arg, welches ein Array von Types ist, akzeptiert und ein Array von Types zurückgibt“ gelesen werden. Wenn wir ein Array mit Zahlen übergeben, würde das Array mit Zahlen zurückgegeben, da Type an die Zahl gebunden ist. Dies gibt uns die Möglichkeit, den generischen Typparameter Type als Teil der Types, mit denen wir arbeiten, anstelle des gesamten Typs, zu verwenden und erhöht dadurch unsere Flexibilität.

Alternativ können wir das Beispiel auch wie folgt schreiben:

function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
  console.log(arg.length); // Array has a .length, so no more error
  return arg;
}

Du kennst möglicherweise diesen Typenstil bereits aus anderen Programmiersprachen. Im zweiten Teil dieser Reihe werden wir zeigen, wie du eigene generische Typen wie Array<Type> erstellen kannst.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Shopping Cart
WordPress Cookie Notice by Real Cookie Banner