Fortgeschrittene Typisierung: Generics

Dezember 30, 2024Web-Frameworks

Generics sind ein mächtiges Feature von TypeScript, das es ermöglicht, Typen flexibler und wiederverwendbarer zu gestalten. Sie erlauben es, Typen erst zur Laufzeit festzulegen, ohne die Typsicherheit zu verlieren. In diesem Artikel werden wir uns mit der Einführung von Generics, ihrer Verwendung in Funktionen und Klassen sowie den Einschränkungen (Constraints) befassen.

Einführung in Generics

Generics bieten eine Möglichkeit, Typen flexibel und unabhängig von einem bestimmten Datentyp zu definieren. Sie helfen dabei, wiederholbaren Code zu vermeiden und gleichzeitig sicherzustellen, dass der Code typisiert bleibt.

Ein einfaches Beispiel für eine Funktion, die ohne Generics funktioniert, wäre:

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

Diese Funktion funktioniert nur mit `number`. Was aber, wenn wir möchten, dass die Funktion mit beliebigen Typen arbeitet? Hier kommen Generics ins Spiel. Anstatt den Typ im Voraus festzulegen, verwenden wir einen Generics-Parameter:

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

In diesem Fall steht `T` für einen generischen Typ, der erst beim Aufruf der Funktion festgelegt wird. Dadurch kann die Funktion für unterschiedliche Typen verwendet werden, behält aber gleichzeitig die Typensicherheit:

let output1 = identity<number>(42);   // T wird zu number
let output2 = identity<string>("Hello!"); // T wird zu string

Der Vorteil von Generics ist, dass sie uns erlauben, Funktionen oder Klassen zu schreiben, die mit unterschiedlichen Typen arbeiten können, ohne dass wir für jeden Typ eine neue Implementierung schreiben müssen.

Verwendung von Generics in Funktionen und Klassen

Generics in Funktionen sind besonders nützlich, wenn der Typ eines Arguments und der Rückgabewert voneinander abhängen. Hier ein Beispiel einer Funktion, die mit Arrays arbeitet:

function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

let numberArray = [1, 2, 3];
let stringArray = ["a", "b", "c"];

console.log(getFirstElement(numberArray)); // Ausgabe: 1
console.log(getFirstElement(stringArray)); // Ausgabe: "a"

In diesem Beispiel nimmt die Funktion `getFirstElement` ein Array vom Typ `T[]` als Argument und gibt das erste Element vom Typ `T` zurück. Da `T` generisch ist, kann die Funktion mit Arrays beliebiger Typen verwendet werden.

Generics in Klassen ermöglichen es, eine Klasse für verschiedene Typen wiederzuverwenden, ohne den Typ im Voraus festlegen zu müssen. Ein häufiges Beispiel hierfür ist eine Datenstruktur wie eine Liste oder ein Stack:

class Box<T> {
  content: T;
  constructor(content: T) {
    this.content = content;
  }
  getContent(): T {
    return this.content;
  }
}

let numberBox = new Box<number>(123);
let stringBox = new Box<string>("TypeScript");

console.log(numberBox.getContent()); // Ausgabe: 123
console.log(stringBox.getContent()); // Ausgabe: "TypeScript"

In diesem Beispiel haben wir eine generische Klasse `Box`, die mit unterschiedlichen Typen von Inhalten arbeiten kann. Ob es sich um Zahlen, Strings oder andere Typen handelt, die Klasse bleibt flexibel und typensicher.

Einschränkungen von Generics (Constraints)

Generics sind extrem flexibel, aber es kann vorkommen, dass man sicherstellen möchte, dass der generische Typ bestimmte Eigenschaften besitzt. In solchen Fällen können Constraints verwendet werden, um den generischen Typ einzuschränken.

Zum Beispiel: Angenommen, wir wollen eine Funktion schreiben, die auf ein Attribut `length` eines Objekts zugreift. Nicht alle Typen in TypeScript haben die Eigenschaft `length`. Um sicherzustellen, dass unser generischer Typ nur Typen akzeptiert, die diese Eigenschaft haben, können wir Constraints setzen:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): void {
  console.log(arg.length);
}

logLength("Hello!"); // Ausgabe: 6
logLength([1, 2, 3, 4]); // Ausgabe: 4
logLength({ length: 10 }); // Ausgabe: 10

Hier schränkt der Ausdruck `T extends HasLength` den generischen Typ `T` so ein, dass er nur Typen akzeptiert, die über eine `length`-Eigenschaft verfügen. Das können Strings, Arrays oder benutzerdefinierte Objekte sein, die die Schnittstelle `HasLength` implementieren.

Mehrfache Constraints sind ebenfalls möglich. Angenommen, wir möchten, dass der generische Typ sowohl `HasLength` als auch ein bestimmtes andere Interface implementiert:

interface Identifiable {
  id: number;
}

function logIdAndLength<T extends HasLength & Identifiable>(arg: T): void {
  console.log(`ID: ${arg.id}, Length: ${arg.length}`);
}

const obj = { id: 1, length: 5 };
logIdAndLength(obj); // Ausgabe: ID: 1, Length: 5

Hier verwenden wir den `&`-Operator, um sicherzustellen, dass `T` sowohl das Interface `HasLength` als auch `Identifiable` erfüllt.

Fazit

Generics sind eine leistungsstarke Funktion in TypeScript, die es ermöglicht, wiederverwendbaren und flexiblen Code zu schreiben, ohne die Sicherheit der Typüberprüfung zu verlieren. Sie ermöglichen es, Funktionen und Klassen zu erstellen, die mit unterschiedlichen Typen arbeiten können, während sie gleichzeitig sicherstellen, dass der richtige Typ verwendet wird. Einschränkungen (Constraints) bieten eine zusätzliche Kontrolle, indem sie sicherstellen, dass generische Typen bestimmte Eigenschaften erfüllen. Durch den Einsatz von Generics kann TypeScript Entwicklern helfen, stabileren, wartbaren und wiederverwendbaren Code zu schreiben.

Let’s talk business

Kontaktieren Sie uns und lassen Sie sich bei einem kostenlosen Erstgespräch von uns beraten. Wir freuen uns darauf, Sie kennenzulernen.

“Gemeinsam gestalten wir ihre digitale Identität”

Philipp Zimmermann

Philipp Zimmermann

Ihr Ansprechpartner

14 + 12 =