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

  1. Typsicherheit: Generics ermöglichen es, Typen zu definieren, die zur Compile-Zeit überprüft werden, wodurch viele Arten von Laufzeitfehlern vermieden werden.
  2. Wiederverwendbarkeit: Generics erlauben die Erstellung wiederverwendbarer Komponenten, die mit verschiedenen Datentypen arbeiten können.
  3. 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 lengthmit 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.

Diese Seite verwendet Cookies, um die Nutzerfreundlichkeit zu verbessern. Mit der weiteren Verwendung stimmst du dem zu.

Datenschutzerklärung