Getty Images/iStockphoto

Wie Generika in TypeScript funktionieren

Programmiersprachen wie Java, C# und Swift verwenden Generika, um Code zu erstellen, der wiederverwendbar ist. Hier erfahren Sie, wie Generika in TypeScript funktionieren.

Generika (Generics) sind eine leistungsstarke Funktion der meisten Programmiersprachen. Sie unterstützen Entwickler bei der Erstellung von Funktionen, Klassen und Schnittstellen, in denen später im Programmierzyklus bestimmte Typen deklariert werden.

Stark typisierte Programmiersprachen wie Java, C# und Swift unterstützen Generika. In der Programmiersprache C++ werden sie Templates genannt, in Objective-C Lightweight Generics, also leichtgewichtige Generika. JavaScript, das dynamisch typisiert ist, unterstützt keine Generika. TypeScript wurde zum Teil als eine stark typisierte Option für JavaScript-Programmierer entwickelt.

Dieser Artikel erklärt die Grundlagen der Generika in TypeScript. Wir besprechen die Syntax für die Verwendung von Generika und zeigen, wie Sie Generika verwenden können, um Typsicherheit zur Kompilierungszeit zu erzwingen. Außerdem sehen Sie ein Beispiel für die Verwendung von Generika zur Implementierung von bedingter Logik in einer Funktion entsprechend dem Typ, der bei der Verwendung der Generika deklariert wurde. Es wird davon ausgegangen, dass der Leser über Grundkenntnisse in der TypeScript-Programmierung und eine gewisse Vertrautheit mit der objektorientierten Programmierung verfügt, aber gerade erst anfängt, mit Generika zu arbeiten.

Die Syntax von Generika

Der Schlüssel zur Arbeit mit der Syntax von Generika ist das Verständnis des Konzepts einer generischen Variable. Eine generische Variable ist ein einzelnes Zeichensymbol, das einen Datentyp angibt, der später deklariert wird. Stellen Sie sich eine generische Variable als einen Typ-Platzhalter vor.

Als Symbol für eine generische Variable wird üblicherweise eines der Großbuchstabenzeichen verwendet: T, V, K, S oder E. (Sie können jedes beliebige alphabetische Zeichen verwenden, aber ein Großbuchstabe macht Ihren Code leichter verständlich). Diese Zeichen werden zwischen öffnende und schließende spitze Klammern, <wie hier> gesetzt. Um die generische Variable zu verwenden, gibt der Programmierer dann den tatsächlichen Typnamen für jede deklarierte generische Variable an.

Wie bereits erwähnt, können Entwickler Generika mit Funktionen, Schnittstellen und Klassen verwenden. In den folgenden Abschnitten werden die Syntax und die Verwendung der einzelnen Generika beschrieben.

Syntax einer Funktion mit einem Parameter

Beispiel 1 zeigt eine Funktion namens myFunctionOne<T>(), die eine generische Variable T verwendet, um den Typ des Parameters mit dem Namen param1 anzugeben.

function myFunctionOne<T>(param1: T): void {
    console.log(`The type of the parameter value ${param1} is: ${typeof param1}`)
}

Beispiel 1: Eine Funktion, die eine einzelne generische Variable, T, deklariert.

Beachten Sie, dass die generische Variable T in der Funktionssignatur zwischen spitzen Klammern steht, etwa so: myFunctionOne<T>.

Der Grund dafür, die <T>-Deklaration mit dem Funktionsnamen zu verbinden, ist, dass sie die generische Variable definiert, die in der Funktion verwendet wird, wie in diesem Fall mit der Parameterdeklaration.

Der folgende Codeausschnitt zeigt die Funktion myFunction<T>(), die für den Wert von T den Typ string deklariert. Der Typ des in der Funktion übergebenen Parameterwerts ist dann ein String.

myFunctionOne<string>("Hi There!");

Dies ist die Ausgabe:

The type of the parameter value Hi There! is: string

Syntax einer Funktion mit zwei Parametern

Das folgende Beispiel 2 zeigt eine Funktion namens myFunctionTwo<T, V>(), die zwei generische Variablen T und V verwendet, um die Typen anzugeben, die die Funktion verwenden wird. In diesem Fall definieren die generischen Variablen T und V die Typen für param1 beziehungsweise param2:

function myFunctionTwo<T, V>(param1: T, param2: V): string {

    return JSON.stringify({ param1, param2 })

}

Beispiel 2: Eine Funktion, die zwei generische Variablen, T und V, deklariert.

Hier ist ein spezifischerer Anwendungsfall:

const str = myFunctionTwo<number, string>(100, "People");

console.log(str);

Und die Ausgabe sollte wie folgt aussehen:

{ "param1": 100, "param2": "People" }

Interface-Syntax

Beispiel 3 zeigt eine TypeScript-Schnittstelle, die eine generische Variable namens T deklariert, die zur Definition des Typs für das Datenelement mit dem Namen value verwendet wird.

interface IGenericInterface<T> {

   Wert: T;

}

Beispiel 3: Eine TypeScript-Schnittstelle, die eine generische Variable, T, deklariert.

Der folgende Code zeigt, wie eine TypeScript-Klasse die Schnittstelle IGenericInterface implementiert. Beachten Sie, dass die Verwendung von IGenericInterface den Wert der impliziten generischen Variablen T auf einen String setzt, wie im Ausdruck IGenericInterface<string> angegeben. Somit wird auch der Typ des Werts des Klassenmitglieds implizit auf den Typ String gesetzt.

class MyClass implements IGenericInterface<string>{

    value: string = "Hi There";

}

const message = new MyClass();

console.log(message.value)

Und hier ist die einfache Ausgabe:

Hi There

Syntax der Klasse

Beispiel 4 zeigt eine Klasse namens GenericClass<T, V>. Die Klasse deklariert zwei generische Variablen T und V, die innerhalb der Klasse verwendet werden können. Beachten Sie, dass die generische Variable T den Typ des Klassenmitglieds namens make definiert. Die generische Variable V definiert den Typ des Klassenmitglieds mit dem Namen model.

Beachten Sie auch, dass der Konstruktor der Klasse zwei Parameter namens make und model entgegennimmt. Die Werte dieser Parameter werden den Klassenmitgliedern zugewiesen. Denken Sie daran, dass die generischen Variablen T und V den Parametern des Konstruktors zur Verfügung stehen, weil sie in der Klassendeklaration wie folgt definiert sind: GenericClass<T, V>.

Class GenericClass<T, V> {

    make: T;

    model: V

 

    constructor(make: T, model: V) {

        this.make = make;

        this.model = model;

    }

}

Beispiel 4: Eine TypeScript-Klasse, die zwei generische Variablen deklariert, T und V, die die Klasse verwendet.

Hier sehen Sie zwei verschiedene Möglichkeiten, die oben beschriebene GenericClass<T, V> anzuwenden. Die erste definiert den String-Typ für die beiden impliziten generischen Variablen T und V:

const car1 = new GenericClass<string, string>("Kia", "Soul");

console.log(`${car1.make} ${car1.model}`)

Und hier ist die Ausgabe:

Kia Soul

Das zweite Beispiel definiert den Typ string für die generische Variable T und den Typ number für die generische Variable V:

const car2 = new GenericClass<string, number>("Chrysler", 300);

console.log(`${car2.make} ${car2.model}`

Und hier ist die Ausgabe:

Chrysler 300

Erzwingen von Typsicherheit

Einer der Vorteile der Verwendung von Generika ist, dass sie die Typsicherheit in wiederverwendbarem Code erzwingen. Ein Entwickler kann Code erstellen, dessen Verhalten auf einer generischen Variable basiert, und ein anderer Entwickler kann dann den genauen Typ für die generische Variable definieren. Sobald dieser Typ definiert ist, gibt der Code zur Kompilierzeit einen Fehler aus, wenn er einen anderen als den definierten Typ verwendet.

Betrachten wir zur Veranschaulichung das TypeScript Array Objekt, das implizit generische Variablen unterstützt. Diejenigen, die TypeScript bereits verwenden, sind wahrscheinlich schon auf die folgende Deklaration für ein Array gestoßen:

const arr = new Array<string>()

Diese Anweisung besagt: Erstelle ein Array, in das nur Daten vom Typ string eingefügt werden können. Daher wird die folgende Anweisung funktionieren:

arr.push("Hi There")

Während diese Anweisung einen Kompilierzeitfehler wie folgt auslöst:

arr.push(1)

Warum? Das Array wurde so deklariert, dass es nur das Hinzufügen von Daten vom Typ string erlaubt. Der zweite Codeschnipsel verwendet eine Ganzzahl, daher der Fehler.

Unter der Haube lautet die allgemeine Definition dieses Arrays wie folgt:

Array<T>

Dieses T ist eine generische Variable, die einen Typ repräsentiert, der später deklariert wird.

Das Schöne an der Unterstützung des Array-Objekts für generische Deklarationen ist, dass Fehler bereits bei der Kompilierung ausgegeben werden. Außerdem melden viele Entwicklungs-Tools wie Visual Studio Code und WebStorm den Fehler der Nichtübereinstimmung des Typs bereits beim Schreiben des Codes, wie im folgenden Screenshot zu sehen ist.

TypeScript Generics Fehler
Abbildung 1: Ein Fehler, der in einer IDE aufgrund einer Nichtübereinstimmung des Typs auftritt.

Durch die Verwendung von Generika wird der Code selbst korrigiert. Das ist zwar schön, wenn es um Programmiermagie geht, aber die eigentliche Frage ist: Warum sollten wir die Typsicherheit in einem Array gewährleisten wollen?

Stellen Sie sich ein Programm vor, das ein Array benötigt, das nur Strings enthält – zum Beispiel die 12-Wort-Authentifizierungsphrase, die von einem Kryptowährungs-Wallet wie MetaMask veröffentlicht wird. Unter keinen Umständen wollen wir auch nur die Möglichkeit haben, dem Array etwas anderes als ein String hinzuzufügen, nicht einmal zur Programmierzeit. Daher deklarieren wir das Array wie folgt:

const seedPhrase = new Array<string>()

Dadurch wird sichergestellt, dass der Programmierer, der mit dem Code arbeitet, dem Array nie etwas anderes als ein String hinzufügen kann. Der generische Code erzwingt die Typsicherheit.

Bedingtes Verhalten basierend auf dem Typ

Ein weiterer Vorteil von Generika ist, dass Entwickler bedingtes Verhalten auf der Grundlage des Typs programmieren können, der einer generischen Variablen zugewiesen ist.

Beispiel 5 unten zeigt eine Klasse namens SmartPrinter<T>, die eine Methode namens print(data: T) hat. Die generische Variable T ist in der Klassendeklaration in Zeile 1 definiert, wird aber in der Methode print in Zeile 2 verwendet.

export class (SmartPrinter<T> { 
   print(data: T) { 
       if (typeof data === "string") { 
           console.log(`I am going to print: ${data.toLocaleUpperCase()}`); 
       } else { 
           console.log(`I am going to print a ${typeof data}`); 
       } 
   } 
}

Beispiel 5: Eine Klasse, die eine generische Variable verwendet und eine bedingte Logik auf der Grundlage des Typs der generischen Variable ausführt.

Beachten Sie, dass die Methode print(data: T) ein Verhalten aufweist, das den tatsächlichen Typ des Datenparameters in den Zeilen 3 - 7 prüft. Die Prüfung erfolgt zur Laufzeit und der Code verhält sich entsprechend. Im Folgenden werden verschiedene Möglichkeiten der Verwendung der Methode SmartPrinter.print(data: T) mit den entsprechenden Ergebnissen gezeigt.

Hier ein Beispiel, wie Sie dies codieren können:


const smartPrinter = new SmartPrinter()

smartPrinter.print(1);

smartPrinter.print("This is very cool");

smartPrinter.print({ firstName: "Joe", lastName: "Jones" });

Die Ausgabe s ieht wie folgt aus:


I am going to print a number

I am going to print: THIS IS VERY COOL

I am going to print a object

Wie Sie sehen, führt die Möglichkeit, bedingtes Verhalten auf der Grundlage bestimmter Typen hinzuzufügen, eine neue Dimension in die generische Programmierung ein. Zugegeben, das ist ein etwas fortgeschrittenes Thema, aber es ist gut zu wissen, dass man mit Generika bedingte Programmierung betreiben kann.

TypeScript Generika sind ein umfangreiches Thema, und dies ist nur der Anfang. Dieser Bereich kann komplex werden, wenn Sie tiefer in die Typprogrammierung eintauchen. Nichtsdestotrotz ist dieser Artikel ein guter Einstieg in die Arbeit mit Generika und bietet einen Einblick in die Arbeit der Entwickler, die sie entwickelt haben. Wenn Sie mehr lernen, werden Sie in der Lage sein, generischen Code zu erstellen, der wiederum anderen Programmierern helfen wird. Es ist ein Gewinn für alle Beteiligten.

Erfahren Sie mehr über Softwareentwicklung