Im ersten Teil über Generics haben wir bereits Identitätsfunktionen erstellt, die auf verschiedene Typen angewendet werden können. In diesem Artikel werden wir uns mit der Art der Funktionen selbst und der Erstellung von generischen Schnittstellen befassen.
Die Typisierung von generischen Funktionen erfolgt analog zu nicht-generischen Funktionen, wobei die Typ-Parameter zuerst aufgeführt werden, ähnlich wie bei Funktionsdeklarationen.
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
Es wäre ebenso möglich gewesen, in der Typdefinition einen anderen Namen für den generischen Typ-Parameter zu verwenden, solange die Anzahl der Typvariablen und deren Verwendung übereinstimmen.
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Input>(arg: Input) => Input = identity;
Es ist auch möglich, den generischen Typ als Aufruf-Signatur in einem Objektliteral-Typ zu definieren:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
Dies führt uns zur Erstellung unserer ersten generischen Schnittstelle. Wir können das Objektliteral aus dem vorherigen Beispiel als Vorlage nutzen und es in eine Schnittstelle überführen:
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
In ähnlichen Fällen können wir den generischen Parameter als Teil der gesamten Schnittstelle definieren. Dadurch wird sichtbar, welche Typen für die Generizität verwendet werden (z. B. Dictionary<string>
anstelle von nur Dictionary
). Der Typ-Parameter ist somit für alle anderen Mitglieder der Schnittstelle sichtbar.
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
In unserem Beispiel hat sich etwas geändert. Statt eine generische Funktion zu beschreiben, haben wir jetzt eine nicht-generische Funktionssignatur, die Teil eines generischen Typs ist. Wenn wir GenericIdentityFn
verwenden, müssen wir auch das entsprechende Typargument (hier: number
) angeben, um festzulegen, welche Signatur verwendet wird. Es ist wichtig zu verstehen, wann der Typparameter direkt auf die Aufrufsignatur und wann auf die Schnittstelle selbst angewendet werden sollte, um zu beschreiben, welche Aspekte eines Typs generisch sind.
Zusätzlich zu generischen Schnittstellen können wir auch generische Klassen erstellen. Beachte, dass es nicht möglich ist, generische Enums
und Namespaces
zu erstellen.
Generische Klassen
Generische Klassen haben ähnlich wie generische Schnittstellen eine Struktur mit einer Liste von generischen Typ-Parametern in spitzen Klammern (<>
) nach dem Klassennamen.
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
Dies ist eine sehr wörtliche Verwendung der Klasse GenericNumber
, aber man könnte bemerkt haben, dass sie nicht darauf beschränkt ist, nur den Typ number
zu verwenden. Wir könnten stattdessen auch string
oder sogar komplexere Objekte verwenden.
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
Wie es bei Schnittstellen der Fall ist, ermöglicht es uns auch bei Klassen, den Typ-Parameter direkt auf der Klasse zu setzen, um sicherzustellen, dass alle Eigenschaften der Klasse mit dem gleichen Typ arbeiten.
Eine Klasse hat zwei Typseiten: die statische Seite und die Instanzseite. Generische Klassen sind nur generisch über ihre Instanzseite, nicht über ihre statische Seite, sodass bei der Arbeit mit Klassen die statischen Elemente nicht den Typ-Parameter der Klasse nutzen können.
Generische Einschränkungen
Wenn man sich an ein früheres Beispiel erinnert, möchte man möglicherweise manchmal eine generische Funktion schreiben, die auf einer Menge von Typen arbeitet, bei denen Du bestimmte Kenntnisse darüber hast, welche Fähigkeiten diese Typen besitzen werden. In unserem Beispiel der Funktion loggingIdentity
wollten wir auf die .length
-Eigenschaft von arg
zugreifen, aber der Compiler konnte nicht garantieren, dass jeder Typ eine .length
-Eigenschaft hat. Aus diesem Grund warnte uns der Compiler, dass wir diese Annahme nicht treffen können.
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
Property 'length' does not exist on type 'Type'.
return arg;
}
Anstatt mit allen möglichen Typen zu arbeiten, möchten wir diese Funktion auf Typen beschränken, die auch die .length
-Eigenschaft haben. Solange der Typ diese Eigenschaft besitzt, erlauben wir ihn, aber es ist erforderlich, dass er mindestens diese Eigenschaft hat. Dazu müssen wir unsere Anforderung als Einschränkung für den Typ angeben.
Dafür erstellen wir eine Schnittstelle, die unsere Anforderung beschreibt. Wir erstellen eine Schnittstelle mit einer einzelnen .length
-Eigenschaft und verwenden das Schlüsselwort extends
, um unsere Einschränkung auszudrücken.
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
Aufgrund der Einschränkung der generischen Funktion wird sie nicht mehr für alle Typen funktionieren:
loggingIdentity(3);
// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
Stattdessen müssen wir Werte übergeben, deren Typ alle erforderlichen Eigenschaften besitzt:
loggingIdentity({ length: 10, value: 3 });
Verwendung von Typ-Parametern in generischen Einschränkungen
Wenn Du einen Typ-Parameter deklarierst, kannst Du ihn durch einen anderen Typ-Parameter einschränken. Angenommen, Du möchtest einer Funktion den Namen einer Eigenschaft eines Objekts übergeben und die dazugehörige Eigenschaft abrufen. Damit Du sicherstellen kannst, dass die Eigenschaft auch tatsächlich auf dem Objekt existiert, setzt Du eine Einschränkung zwischen den beiden Typen.
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
Verwendung von Klassen-Typen in Generics
Wenn Sie in TypeScript Generics verwenden, um Factories
zu erstellen, müssen Sie Klassentypen anhand ihrer Konstruktionsfunktionen referenzieren. Ein Beispiel dafür ist:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
Ein Beispiel mit höherem Schwierigkeitsgrad verwendet die Prototyp-Eigenschaft, um Beziehungen zwischen der Konstruktionsfunktion und der Instanzseite von Klassentypen zu erschließen und zu begrenzen.
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
Herzlichen Glückwunsch, Du hast es geschafft! Nachdem Du zwei unserer Blog-Artikel zum Thema Generics in TypeScript gelesen hast, bist Du nun in der Lage, die vielfältigen Möglichkeiten der Generics-Programmierung zu nutzen. In den Artikeln haben wir uns intensiv mit der Verwendung von Typ-Parametern, generischen Schnittstellen und der Einschränkung von Typen befasst. Wir haben auch gelernt, wie wir Klassentypen in Generics einbeziehen und Beziehungen zwischen der Konstruktionsfunktion und der Instanzseite von Klassentypen ableiten und einschränken können.
All diese Konzepte werden in der Welt der TypeScript-Programmierung häufig eingesetzt, um wiederverwendbaren, flexiblen und robusten Code zu erstellen. Sie helfen uns, die Wartbarkeit von Code zu verbessern und die Entwicklungszeit zu verkürzen. Wir hoffen, dass Du unsere Artikel genossen hast und dass sie dir einen umfassenden Überblick über das Thema Generics in TypeScript gegeben haben.
Jetzt bist Du bereit, diese Konzepte in deiner eigenen Arbeit anzuwenden und eine neue Welt der flexiblen und wiederverwendbaren Programmierung zu entdecken. Vielen Dank fürs Lesen und viel Erfolg bei deinen zukünftigen Projekten!