Geschichte
Die Ideen stammt aus der Theorie der Typensysteme und Polymorphie. Sie wurden in den 1970er und 1980er Jahren von den Informatikern Robin Milner und John Reynolds entwickelt. Ihre Arbeiten legten den Grundstein für die späteren Implementierungen von Generics in Programmiersprachen. Die Programmiersprache Ada wurde in der 1980er entwickelt. Sie war die erste Programmiersprache, die generische Datentypen implementierte, um wiederverwendbare Datenstrukturen und Algorithmen zu unterstützen.
Vorteile von Generics
- Typsicherheit: Generics ermöglichen es, Typen zu definieren, die zur Compile-Zeit überprüft werden, wodurch viele Arten von Laufzeitfehlern vermieden werden.
- Wiederverwendbarkeit: Generics erlauben die Erstellung wiederverwendbarer Komponenten, die mit verschiedenen Datentypen arbeiten können.
- Flexibilität: Mit Generics kann man flexibel mit verschiedenen Datentypen arbeiten, ohne auf die Typsicherheit verzichten zu müssen.
Generische Funktionen
Generische Funktionen erlauben es, den Typ der Parameter und Rückgabewerte zu parametrisieren.
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("Hello World");
let output2 = identity<number>(42);
//Hier wird der Typ automatisch erkannt.
let output3 = identity("Hello World");
let output4 = identity(42);
console.log(output1+" - "+output2);
// Result: Hello World - 42
console.log(output3+" - "+output4);
// Result: Hello World - 42
Hier wird T
als Platzhalter für den Typ verwendet, der zur Laufzeit bestimmt wird. In dem Beispiel von output3
und output4
wird gezeigt, dass der Typ nicht explizit, sondern durch TypeScript automatisch erkannt wird. Um eine generische Funktion schneller zu erkennen, wäre das Angeben der Typen ratsam.
Generische Klassen
Generische Klassen erlauben es, den Typ von Klassenmitgliedern zu parametrisieren.
class DataHolder<T> {
private data: T;
constructor(data: T) {
this.data = data;
}
getData(): T {
return this.data;
}
setData(data: T): void {
this.data = data;
}
}
const dataHolderString = new DataHolder<string>("Hello, World!");
console.log(dataHolderString.getData());
//RESULT: Hello, World!
const dataHolderNumber = new DataHolder<number>(42);
console.log(dataHolderNumber.getData());
//Result: 42
Das Beispiel zeigt eine Klasse mit dem generischen Typ T
. Instanziieren wir ein neues Objekt, wird der Platzhalter T
mit einem spezifischen Typ belegt.
Generische Klasse als API Response
class ApiResponse<T> {
constructor(public data: T, public error: string) {}
}
const userData = { name: "Dimi", age: 30 };
const errorMsg = "Fehlermeldung:";
let userResponse = new ApiResponse<typeof userData>(userData, errorMsg);
console.log(userResponse.data.name+", "+userResponse.data.age);
//Result: Dimi, 30
console.log(userResponse.error);
//Result: Fehlermeldung:
In dieser Klasse erstellen wir ein neues Objekt und übergeben unsere Konstante userData
mit den Properties (Eigenschaften) und errorMsg
. Das typeof
für das neue Objekt wird verwendet, um den Typ einer Variablen oder eines Objekts dynamisch zu bestimmen, insbesondere wenn der Typ nicht direkt bekannt ist oder von einem Ausdruck abgeleitet werden muss. Das typeof
ist nützlich, um Typen dynamisch zu bestimmen. Es erhöht die Wiederverwendbarkeit und Typensicherheit.
Generische Interfaces
Einfaches generisches Interface
Generische Interfaces erlauben es, den Typ von Interface-Eigenschaften und -Methoden zu parametrisieren.
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
In diesem Beispiel holen wir uns das return arg
aus der Funktion identity
. Allerdings nicht ohne vorher mit dem Interface GenericIdentityFn
den Typ number
festzulegen.
Einfaches generisches Interface für eine Funktion
interface Result<T> {
data: T;
error: string | null;
}
function handleResult<T>(result: Result<T>): void {
if (result.error) {
console.error(result.error);
} else {
console.log(result.data);
}
}
const stringResult: Result<string> = { data: "Erfolg", error: null };
handleResult(stringResult);
In dem weiteren Beispiel vergeben wir über das Interface den Typ string
und die Properties data
mit dem generisch vergebenen Typ (also string) und error
vom Typ string
oder null
, wenn error
ohne Wert ist. Eine Zeile tiefer rufen wir die Funktion handleResult
inkl. die Konstante stringResult
als Parameter. In der Funktion selbst wird das Interface Result
, um die Properties bearbeiten zu können.
Generische Constraints (Zwänge)
Als Abschluss zum Thema generischen Funktionen möchte ich mit Dir die Generischen Constraints durchgehen. Dieses erreichen wir mit dem Schlüsselwort extends
. Extends
erlauben es, die generischen Typen auf bestimmte Strukturen oder Eigenschaften zu beschränken. Dies erhöht die Typensicherheit, indem sichergestellt wird, dass nur Typen verwendet werden, die die erforderlichen Eigenschaften besitzen.
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
const myArray = [1, 2, 3, 4, 5];
logLength(myArray); // Ausgabe: 5
const myString = "Hello, world!";
logLength(myString); // Ausgabe: 13
Der Fokus liegt hier bei der Funktion logLength
. Durch extends und Property length
mit der Typ definition number
sind wir im Zwang numerische Werte auszugeben.
Das item
der Funktion kann myArray
oder myString
sein. Mit console.log
und der Property length
geben wir die Länge aus.
Beachte, dass sich bei allen generischen Funktionen um die Typensicherheit und der Flexibilität des Typensystems geht. Es soll helfen, den Fehler zur Compile-Zeit zu erkennen, anstatt zur Laufzeit.
Zusammenfassung
Generics in TypeScript ermöglichen es, wiederverwendbare und flexible Komponenten zu erstellen, die mit einer Vielzahl von Datentypen arbeiten können, ohne die Typsicherheit zu verlieren. Sie sind ein mächtiges Werkzeug, um robusten und flexiblen Code zu schreiben. Sie haben sich im Laufe der Jahre weiterentwickelt und sind zu einem unverzichtbaren Werkzeug in der modernen Softwareentwicklung geworden. Entwicklern ist es so möglich, flexible und dennoch typensichere Codekomponenten zu erstellen, was die Wartbarkeit und Wiederverwendbarkeit des Codes erheblich verbessert.