TypeScript Generics gehoeren zu den maechtigsten Funktionen des Typsystems. Sie ermoeglichen es, flexiblen, wiederverwendbaren und typsicheren Code zu schreiben, indem Typen parametrisiert werden. Anstatt Funktionen oder Klassen fuer jeden Datentyp zu duplizieren, koennen Sie mit Generics einmal schreiben und mit jedem Typ verwenden. Dieser Leitfaden behandelt alles von den Grundlagen bis zu fortgeschrittenen Mustern.
Ihre Erste Generische Funktion
Der klassische Ausgangspunkt ist die Identitaetsfunktion — eine Funktion, die genau das zurueckgibt, was sie empfaengt. Ohne Generics wuerden Sie entweder Typinformationen durch any verlieren oder separate Funktionen fuer jeden Typ schreiben muessen.
Mit Generics definieren Sie einen Typparameter (konventionell T genannt), der als Platzhalter dient. Beim Aufruf der Funktion leitet TypeScript den tatsaechlichen Typ automatisch ab.
// Without generics — loses type info
function identityAny(value: any): any {
return value;
}
const result1 = identityAny("hello"); // type: any (not string!)
// With generics — preserves type info
function identity<T>(value: T): T {
return value;
}
const result2 = identity("hello"); // type: string ✓
const result3 = identity(42); // type: number ✓
const result4 = identity(true); // type: boolean ✓
// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: stringGenerische Funktionen
Generische Funktionen koennen mehrere Typparameter akzeptieren und sogar Standardtypen haben. Dies macht sie unglaublich vielseitig.
// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("hello", 42); // type: [string, number]
// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]
// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strings = createArray(3, "x"); // T inferred as string
const numbers = createArray(3, 0); // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to stringStandard-Typparameter funktionieren wie Standard-Funktionsparameter — wenn der Aufrufer keinen Typ angibt, wird der Standard verwendet.
Generische Interfaces und Typen
Generics sind nicht auf Funktionen beschraenkt. Sie koennen Interfaces und Type-Aliases parametrisieren, um flexible Datenstrukturen zu erstellen.
// Generic interface
interface Box<T> {
value: T;
label: string;
}
const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };
// Generic interface with multiple params
interface KeyValuePair<K, V> {
key: K;
value: V;
}
// Generic type alias
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };
// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;
// Generic type for nullable values
type Nullable<T> = T | null | undefined;Generische Interfaces sind besonders nuetzlich fuer die Definition von API-Antworten, Sammlungen und Datencontainern.
Generische Klassen
Klassen koennen ebenfalls generisch sein. Dies ist nuetzlich fuer Datenstrukturen wie Stacks, Queues, verkettete Listen oder beliebige Container.
// Generic Stack class
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined
// Generic class with multiple type params
class HashMap<K, V> {
private map = new Map<K, V>();
set(key: K, value: V): void {
this.map.set(key, value);
}
get(key: K): V | undefined {
return this.map.get(key);
}
has(key: K): boolean {
return this.map.has(key);
}
}Der Typparameter ist in der gesamten Klasse verfuegbar — in Eigenschaften, Methodenparametern und Rueckgabetypen.
Generische Einschraenkungen mit extends
Uneingeschraenkte Generics akzeptieren jeden Typ, aber manchmal muessen Sie die erlaubten Typen einschraenken. Das extends-Schluesselwort ermoeglicht Einschraenkungen.
Einschraenkung auf Objektschluessel mit keyof
Ein haeufiges Muster ist die Einschraenkung eines Typparameters auf gueltige Schluessel eines anderen Typs.
// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name"); // type: string ✓
getProperty(user, "age"); // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"
// Pick specific keys from an object
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => { result[key] = obj[key]; });
return result;
}
const subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }Einschraenkung auf bestimmte Formen
Sie koennen verlangen, dass ein Typparameter bestimmte Eigenschaften hat, indem Sie ein Interface oder einen Objekttyp erweitern.
// Constraint to objects with a length property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): T {
console.log(`Length: ${value.length}`);
return value;
}
logLength("hello"); // OK — string has length
logLength([1, 2, 3]); // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42); // Error — number has no length property
// Constraint to objects with an id
interface HasId {
id: number | string;
}
function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
return items.find(item => item.id === id);
}
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const found = findById(users, 1);
// type: { id: number; name: string } | undefinedBedingte Typen
Bedingte Typen ermoeglichen die Wahl zwischen zwei Typen basierend auf einer Bedingung mit der Syntax T extends U ? X : Y.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true
// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type D = Flatten<string[]>; // string
type E = Flatten<number[][]>; // number[]
type F = Flatten<string>; // string (not an array, returned as-is)
// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type G = MyReturnType<() => string>; // string
type H = MyReturnType<(x: number) => boolean>; // boolean
// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>; // numberBedingte Typen werden besonders maechtig in Kombination mit infer, womit Typen aus anderen Typen extrahiert werden koennen.
Mapped Types
Mapped Types transformieren die Eigenschaften eines bestehenden Typs, um einen neuen Typ zu erstellen. Sie iterieren ueber Schluessel mit der [K in keyof T]-Syntax.
// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties required (like Required<T>)
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Transform property types
type Stringify<T> = {
[K in keyof T]: string;
};
interface User {
name: string;
age: number;
active: boolean;
}
type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }
// Mapped type with conditional
type OptionalNullable<T> = {
[K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};Sie koennen Modifikatoren wie readonly und ? in Mapped Types hinzufuegen oder entfernen.
Eingebaute Utility Types
TypeScript liefert viele mit Generics gebaute Utility Types. Hier sind die am haeufigsten verwendeten:
| Utility Type | Beschreibung | Beispiel |
|---|---|---|
Partial<T> | Macht alle Eigenschaften optional | Partial<User> |
Required<T> | Macht alle Eigenschaften erforderlich | Required<Partial<User>> |
Pick<T, K> | Waehlt angegebene Eigenschaften | Pick<User, "name" | "email"> |
Omit<T, K> | Schliesst angegebene Eigenschaften aus | Omit<User, "password"> |
Record<K, V> | Erstellt ein Schluessel-Wert-Typ-Mapping | Record<string, number> |
Readonly<T> | Macht alle Eigenschaften schreibgeschuetzt | Readonly<Config> |
ReturnType<T> | Extrahiert den Rueckgabetyp einer Funktion | ReturnType<typeof fn> |
Parameters<T> | Extrahiert das Tupel der Parametertypen | Parameters<typeof fn> |
// Utility types in action
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// For update forms — all fields optional
type UpdateUser = Partial<User>;
// Public user — omit password
type PublicUser = Omit<User, "password">;
// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;
// Lookup table
type UserLookup = Record<number, User>;
// Immutable user from API
type FrozenUser = Readonly<User>;
// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>
type FetchParams = Parameters<typeof fetchUser>; // [number]Praxismuster
Generischer API-Antwort-Wrapper
Eine der haeufigsten Verwendungen von Generics ist das Wrappen von API-Antworten in einer konsistenten Struktur.
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
};
}
// Usage with specific types
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}
// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓
const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓Generische React-Komponenten-Props
Generics ermoeglicht typsichere React-Komponenten, die mit verschiedenen Datentypen arbeiten.
// Generic list component props
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
onItemClick?: (item: T) => void;
emptyMessage?: string;
}
function GenericList<T>({
items,
renderItem,
keyExtractor,
onItemClick,
emptyMessage = "No items",
}: ListProps<T>) {
if (items.length === 0) return <p>{emptyMessage}</p>;
return (
<ul>
{items.map((item, index) => (
<li
key={keyExtractor(item)}
onClick={() => onItemClick?.(item)}
>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage — TypeScript infers T from items
<GenericList
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
onItemClick={(user) => console.log(user.email)}
/>Generisches Repository-Muster
Das Repository-Muster abstrahiert den Datenzugriff hinter einem generischen Interface.
// Generic repository interface
interface Entity {
id: number | string;
}
interface Repository<T extends Entity> {
findAll(): Promise<T[]>;
findById(id: T["id"]): Promise<T | null>;
create(data: Omit<T, "id">): Promise<T>;
update(id: T["id"], data: Partial<T>): Promise<T>;
delete(id: T["id"]): Promise<boolean>;
}
// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
constructor(private baseUrl: string) {}
async findAll(): Promise<T[]> {
const res = await fetch(this.baseUrl);
return res.json();
}
async findById(id: T["id"]): Promise<T | null> {
const res = await fetch(`${this.baseUrl}/${id}`);
if (!res.ok) return null;
return res.json();
}
async create(data: Omit<T, "id">): Promise<T> {
const res = await fetch(this.baseUrl, {
method: "POST",
body: JSON.stringify(data),
});
return res.json();
}
async update(id: T["id"], data: Partial<T>): Promise<T> {
const res = await fetch(`${this.baseUrl}/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
return res.json();
}
async delete(id: T["id"]): Promise<boolean> {
const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
return res.ok;
}
}
// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]Generischer Event-Emitter
Ein typsicherer Event-Emitter stellt sicher, dass Eventnamen und Payloads korrekt typisiert sind.
// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;
class TypedEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: EventCallback<Events[K]>[];
} = {};
on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
const cbs = this.listeners[event];
if (cbs) {
this.listeners[event] = cbs.filter(cb => cb !== callback);
}
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const cbs = this.listeners[event];
if (cbs) {
cbs.forEach(cb => cb(payload));
}
}
}
// Define your event types
interface AppEvents {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"item:added": { itemId: string; quantity: number };
"error": { message: string; code: number };
}
const emitter = new TypedEmitter<AppEvents>();
// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
console.log(payload.userId); // type: string ✓
console.log(payload.timestamp); // type: number ✓
});
// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});Haeufige Fehler
Hier sind die haeufigsten Fehler mit Generics und deren Korrekturen:
| Fehler | Problem | Korrektur |
|---|---|---|
| any statt Generics verwenden | Verlust aller Typinformationen | Typparameter T statt any verwenden |
| Typparameter zu stark einschraenken | Macht den Generic fuer gueltige Typen unbrauchbar | Nur einschraenken, was wirklich benoetigt wird |
| Unnoetige Typparameter | Ein nur einmal verwendetes T erhoeht die Komplexitaet | Wenn T nur im Rueckgabetyp vorkommt, konkreten Typ verwenden |
| Keine Einschraenkungen beim Eigenschaftszugriff | TypeScript-Fehler: Eigenschaft existiert nicht auf Typ T | extends-Einschraenkung hinzufuegen |
| Generische Standardwerte und Einschraenkungen verwechseln | Der Standardtyp beschraenkt nicht — Aufrufer koennen andere Typen uebergeben | extends zum Einschraenken, = fuer Standard |
| Nicht inferieren, wenn moeglich | Explizite Typangabe, wenn TS inferieren kann | TypeScript aus Argumenten inferieren lassen |
| T[] statt readonly T[] verwenden | Erlaubt Mutation von Arrays, die schreibgeschuetzt sein sollten | readonly T[] fuer unveraenderliche API-Daten verwenden |
// ❌ Mistake: Using any
function badParse(json: string): any {
return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!
// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
try {
const parsed = JSON.parse(json);
return validator(parsed) ? parsed : null;
} catch {
return null;
}
}
// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
return arr.length; // T is never meaningfully used
}
// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
return arr.length;
}
// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
return obj.name; // Error: Property 'name' does not exist on type 'T'
}
// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
return obj.name; // OK ✓
}Haeufig Gestellte Fragen
Was ist der Unterschied zwischen Generics und dem Typ any?
Generics bewahren Typinformationen, waehrend any sie verwirft. Mit Generics weiss TypeScript, dass der Rueckgabetyp dem Eingabetyp entspricht.
Wann sollte man Generics vs. Funktionsueberladungen verwenden?
Verwenden Sie Generics, wenn die Logik fuer alle Typen gleich ist. Verwenden Sie Ueberladungen, wenn sich die Implementierung je nach Eingabetyp unterscheidet.
Kann man Generics mit Pfeilfunktionen in TSX-Dateien verwenden?
Ja, aber fuegen Sie ein nachstehendes Komma hinzu: const fn = <T,>(arg: T): T => arg.
Wie viele Typparameter sollte ein Generic haben?
So wenige wie moeglich. Ein oder zwei sind ueblich. Bei mehr als drei sollten Sie ein Refactoring in Betracht ziehen.
Beeinflussen Generics die Laufzeit-Performance?
Nein. Generics sind rein ein Compile-Time-Feature und werden vollstaendig entfernt.
Wie lautet die Namenskonvention fuer Typparameter?
Einzelne Grossbuchstaben sind ueblich: T fuer Typ, K fuer Key, V fuer Value, E fuer Element, R fuer Rueckgabetyp.