TypeScript Generics vastehn
Generics san des Feature, über des Leit stolpern, grod nachdem's mit den einfachen TypeScript-Typen vertraut worn san - und dann, sobois's klickt, zu oam der meistgenutzten Werkzeuge in da Kistn wird. Die Kernidee is klan: Statt a Funktion oda an Typ für oan bestimmten Typ zum schreibn, schreibst ihn für "wöchan Typ a immer übergebn wird", und TypeScript verfolgt den Typ für dich durch den Rest vo da Signatur.
Des Problem, des Generics lösn
Sogn ma, du wüst a Funktion, de s'erste Element von an Array zruckgibt.
function firstOfNumbers(arr: number[]): number {
return arr[0];
}
function firstOfStrings(arr: string[]): string {
return arr[0];
}
Zwoa Funktionen, identische Logik, verschiedene Typen. Du kunntst's mit any typisieren:
function first(arr: any[]): any {
return arr[0];
}
Des funktioniert, aber du host olle Informationen weggeschmissn. first([1, 2, 3]) hot jetzt Typ any statt number, drum fängt nix weiter unten an Fehler wia first([1, 2, 3]).toUpperCase() ab.
Generics erhoitn die Verbindung zwischen Input- und Output-Typ:
function first<T>(arr: T[]): T {
return arr[0];
}
const n = first([1, 2, 3]); // T inferred as number, n: number
const s = first(['a', 'b', 'c']); // T inferred as string, s: string
T is a Typparameter - a Platzhalter, den TypeScript auf Basis dessen befüllt, wos du tatsächlich übergibst, genauso wia a Funktionsparameter mit am Wert befüllt wird. Du hast ned first<number>([1, 2, 3]) schreibn miassn; TypeScript leitet T aus dem Argument ab.
Einschränkn, wos T sei kann
A uneingschränktes T kann olles sei, des heißt, du kannst koane bestimmte Form annehmen. Des kompiliert ned:
function getLength<T>(item: T): number {
return item.length; // Error: Property 'length' does not exist on type 'T'
}
TypeScript beschwert si zu Recht - T kunnt number sei, und der hot ka .length. Schränk's mit extends ein:
function getLength<T extends { length: number }>(item: T): number {
return item.length; // OK — T is guaranteed to have a `length` property
}
getLength('hello'); // OK, strings have .length
getLength([1, 2, 3]); // OK, arrays have .length
getLength(42); // Error: number doesn't satisfy the constraint
extends meint do ned Klassenvererbung - es heißt "T muass zu dera Form zuweisbar sei". Jeder Typ mit ana .length-Zahleneigenschaft erfüllt des.
Generische Interfaces und Klassen
Generics sand ned nur für Funktionen da - sie parametrisieren Typen söba. A Box<T>, de jeden einzelnen Wert hoitn kann:
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: 'hello' };
A realistischeres Beispiel - a typisierter API-Response-Wrapper, der in ana ganzen App genutzt wird:
interface ApiResponse<T> {
data: T;
status: number;
error: string | null;
}
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return { data, status: res.status, error: null };
}
Jeder Aufrufer von fetchUser kriagt response.data ois User typisiert, ned ois any - des Generic trägt den spezifischen Payload-Typ durch de gesamte Response-Form, ohne dass du für jede Entität a eigenes UserApiResponse, PostApiResponse und so weiter schreibn muasst.
Mehrere Typparameter
Generics kinnan mehr ois oan Parameter nehmen, was bei Funktionen üblich is, de zwoa unabhängige Dinge kombinieren - a Map-Funktion is des Paradebeispiel:
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const lengths = mapArray(['a', 'bb', 'ccc'], (s) => s.length);
// T inferred as string, U inferred as number
// lengths: number[]
T is da Typ vom Eingabeelement, U is, wos da Callback zruckgibt - de miassn ned übereinstimmen, und TypeScript leitet beide unabhängig voneinander davon ab, wia du's aufrufst.
Standard-Typparameter
Wia Default-Funktionsparameter kinnan Generics Defaults hom, nützlich, wenn a Typparameter meistens des Söbe is, aber überschreibbar bleiben soi:
interface PaginatedResponse<T = unknown> {
items: T[];
page: number;
totalPages: number;
}
function emptyPage(): PaginatedResponse {
return { items: [], page: 1, totalPages: 1 }; // T defaults to unknown
}
function usersPage(): PaginatedResponse<User> {
return { items: [], page: 1, totalPages: 1 }; // T explicitly User
}
A häufiger Fehler: Zu früh zu Generics greifn
Ned jede Funktion mit an Parameter muass generisch sei. Wenn a Funktion immer nur auf string operiert, fügt function process<T extends string>(input: T) an Typparameter dazu, der nie variiert und seine Komplexität nie rechtfertigt. Generics lohnen si, wenn die Beziehung zwischen Input- und Output-Typ über an Aufruf hinweg tatsächlich erhaltn bleibn muass - a first<T>, des den söbn Elementtyp zruckgibt, den's kriagt hot, a Box<T>, de beliebige Werte einpackt, a mapArray<T, U>, des an Typ in an ondern transformiert. Wenn si da Typ über Verwendungen hinweg nie ändert, is a konkreter Typ klarer ois a Generic, das niemand anders instanziiert. Merk da des, du Hirnederl.
Fazit
Generics gibt's, um a Beziehung zwischen Typen zu erhoitn, de any löschn würd und de N fast identische Overloads duplizieren würdn. Des mentale Modell, des's klicken lasst: a Typparameter is a Parameter, genauso wia a Funktionsargument, nur dass des, wos du übergibst, a Typ is, und TypeScript verfolgt ihn durch den Rest vo da Signatur wia an Wert.