Wie man any vermeidet, ohne Code zu duplizieren
Wer in TypeScript any verwendet, gewinnt kurzfristig Tempo und verliert dauerhaft Typsicherheit. Wer stattdessen für jeden Typ eine eigene Funktion schreibt, produziert Duplizierung und Wartungsaufwand. Generics lösen dieses Dilemma: eine einzige Funktion, ein einziges Interface, eine einzige Klasse bleibt für beliebige Typen wiederverwendbar und behält dabei die volle Typprüfung des Compilers.
Inhaltsverzeichnis
- 1. Das Problem: any oder Code-Duplizierung
- 2. Generische Funktionen: Syntax und Typinferenz
- 3. Mehrere Typparameter: K, V und Constraints
- 4. Generische Interfaces und Type Aliases
- 5. Generische Klassen: State-Container und typisierte Collections
- 6. Praxisbeispiel Teil 1: Typen für den API-Fetch-Wrapper
- 7. Praxisbeispiel Teil 2: Die generische Fetch-Funktion
- 8. Namenskonventionen: T, K, V oder beschreibende Namen
- 9. Generics im Vergleich: wann sie sich lohnen und wann sie Overkill sind
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem: any oder Code-Duplizierung
any ist der schnellste Weg, TypeScript zum Schweigen zu bringen und gleichzeitig sämtliche Vorteile des Typsystems zu verschenken. Sobald eine Variable oder ein Funktionsparameter als any deklariert ist, schaltet der Compiler die Typprüfung für alles komplett ab, was von diesem Wert abhängt. Autovervollständigung verschwindet, Tippfehler in Property-Namen fallen erst zur Laufzeit auf, und Refactoring-Tools können nicht mehr zuverlässig erkennen, welche Stellen im Code von einer Änderung betroffen sind. any funktioniert wie ein Typ, verhält sich aber wie das komplette Fehlen eines Typs.
Die naheliegende Alternative, für jeden konkreten Typ eine eigene Funktion zu schreiben, etwa getFirstString und getFirstNumber statt einer gemeinsamen Funktion, löst das Typsicherheitsproblem, erzeugt aber ein neues: Code-Duplizierung. Jede neue unterstützte Struktur bedeutet eine weitere, fast identische Funktion, ein weiterer Testfall, eine weitere Stelle, die bei einem Bugfix synchron gehalten werden muss. In größeren Codebasen wächst diese Duplizierung schnell auf Dutzende Varianten derselben Logik an.
Generics lösen genau dieses Dilemma. Statt einen konkreten Typ fest einzucodieren, wird der Typ selbst zu einem Parameter der Funktion, des Interfaces oder der Klasse. Der Compiler kennt beim Aufruf den tatsächlichen Typ, prüft ihn vollständig und leitet daraus Rückgabetypen und erlaubte Operationen ab, ohne dass eine einzige Zeile Logik dupliziert werden muss.
2. Generische Funktionen: Syntax und Typinferenz
Die Grundsyntax einer generischen Funktion fügt vor der Parameterliste eine spitze Klammer mit einem Platzhalter für den Typ ein, etwa function identity<T>(value: T): T. T ist dabei kein konkreter Typ, sondern eine Variable auf Typenebene, die der Compiler bei jedem Aufruf neu bindet. Ruft man identity(42) auf, wird T automatisch zu number, bei identity('text') wird T automatisch zu string. Der Rückgabetyp bleibt dabei exakt an den Eingabetyp gekoppelt, was mit any niemals möglich wäre.
Diese automatische Ableitung heißt Typinferenz und funktioniert in den meisten Fällen ohne explizite Angabe des Typarguments. Nur wenn der Compiler den Typ nicht eindeutig aus den Argumenten ableiten kann, etwa bei einem leeren Array, gibt man das Typargument explizit an: createArray<string>(). Diese explizite Form dient auch der Dokumentation, wenn die Absicht im Code sonst unklar wäre.
// Generic identity function preserves the exact input type
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // inferred as number
const str = identity("hello"); // inferred as string
// Generic helper: wrap a value in a fixed-length array
function createArray<T>(item: T, length: number): T[] {
return Array.from({ length }, () => item);
}
const zeros = createArray<number>(0, 3); // number[]
const flags = createArray(false, 2); // inferred as boolean[]
// Without generics, this would need one function per type
// or would fall back to "any" and lose all type information
3. Mehrere Typparameter: K, V und Constraints
Generische Funktionen sind nicht auf einen einzigen Typparameter beschränkt. Für Schlüssel-Wert-Strukturen sind zwei Parameter üblich, meist K für den Key-Typ und V für den Value-Typ, etwa in function toMap<K, V>(entries: [K, V][]): Map<K, V>. Jeder Parameter wird unabhängig aus den Argumenten abgeleitet, sodass ein Aufruf mit string-Keys und number-Values automatisch Map<string, number> ergibt.
Besonders nützlich wird es, wenn ein Typparameter vom anderen abhängt. Mit K extends keyof T lässt sich erzwingen, dass K ausschließlich ein tatsächlich existierender Property-Name von T sein darf. Eine Funktion getProperty<T, K extends keyof T>(obj: T, key: K): T[K] gibt dann exakt den Typ zurück, den die jeweilige Property tatsächlich hat, und der Compiler lehnt Aufrufe mit nicht existierenden Keys bereits beim Kompilieren ab, statt erst zur Laufzeit mit undefined zu überraschen.
// Multiple type parameters with a constraint between them
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
interface Product {
id: number;
name: string;
price: number;
}
const product: Product = { id: 1, name: "Keyboard", price: 79 };
const name = getProperty(product, "name"); // inferred as string
const price = getProperty(product, "price"); // inferred as number
// getProperty(product, "sku"); // compile error: "sku" is not a key of Product
// Two independent type parameters for key/value pairs
function toMap<K, V>(entries: Array<[K, V]>): Map<K, V> {
return new Map(entries);
}
const stock = toMap([["sku-1", 12], ["sku-2", 4]]); // Map<string, number>
4. Generische Interfaces und Type Aliases
Nicht nur Funktionen, auch Interfaces und Type Aliases lassen sich generisch definieren. Ein Interface wie interface Container<T> { value: T } beschreibt eine Struktur, deren konkreter Inhalt erst bei der Verwendung festgelegt wird: Container<string> für Textinhalte, Container<Product> für Produktdaten, ohne dass für jeden Fall ein eigenes Interface nötig wäre. Das ist besonders wertvoll für wiederkehrende Wrapper-Strukturen wie API-Antworten, die immer dasselbe Umschlagsformat haben, aber unterschiedliche Nutzdaten transportieren.
Type Aliases mit generischen Parametern eignen sich für kompaktere Konstrukte, etwa type Nullable<T> = T | null oder type Pair<A, B> = [A, B]. Der Unterschied zu Interfaces liegt vor allem darin, dass Type Aliases auch Union-Typen und Tupel generisch beschreiben können, während Interfaces auf Objektformen beschränkt bleiben. In der Praxis kombiniert man beide je nach Struktur: Interfaces für Objekte mit klaren Property-Namen, Type Aliases für alles, was über eine reine Objektform hinausgeht.
// Generic interface: reusable envelope for any payload type
interface ApiResponse<T> {
data: T;
status: number;
timestamp: string;
}
// Generic type alias: nullable value
type Nullable<T> = T | null;
// Generic type alias: tuple pair
type Pair<A, B> = [A, B];
interface User {
id: number;
email: string;
}
const userResponse: ApiResponse<User> = {
data: { id: 1, email: "dev@mironsoft.de" },
status: 200,
timestamp: "2026-07-12T09:00:00Z",
};
const maybeUser: Nullable<User> = null;
const entry: Pair<string, number> = ["sku-1", 12];
5. Generische Klassen: State-Container und typisierte Collections
Klassen können denselben Typparameter tragen wie Funktionen und Interfaces, was sie ideal für wiederverwendbare Datenstrukturen macht. Eine class Stack<T> mit den Methoden push(item: T): void und pop(): T | undefined bleibt für jeden konkreten Typ nutzbar: Stack<string> für eine Undo-Historie aus Textbefehlen, Stack<Product> für zuletzt angesehene Produkte, ohne dass die Klasse selbst dafür angepasst werden müsste. Der Typparameter wird bei der Instanziierung festgelegt, etwa new Stack<Product>(), und bleibt danach für die gesamte Lebensdauer der Instanz fixiert.
Dasselbe Prinzip trägt State-Container in Frontend-Anwendungen: ein generischer Store<T> kapselt Zustand beliebigen Typs zusammen mit Getter, Setter und Subscription-Mechanismus, ohne dass für jeden Datentyp ein eigener Store geschrieben werden muss. Wichtig ist, den Typparameter nicht zu breit zu fassen: Ein Stack<unknown> verhindert zwar Fehler durch any, zwingt aber bei jedem pop() zu einer manuellen Typprüfung, bevor der Wert verwendet werden darf, weil unknown im Gegensatz zu einem konkreten Generic-Parameter keine Operationen ohne vorherige Prüfung erlaubt.
6. Praxisbeispiel Teil 1: Typen für den API-Fetch-Wrapper
Der native fetch der Browser- und Node-APIs liefert mit response.json() grundsätzlich Promise<any> zurück, weil zur Kompilierzeit unmöglich vorhersehbar ist, welche Struktur ein beliebiger Endpunkt tatsächlich zurückgibt. Genau hier zeigt sich der praktische Nutzen von Generics: Ein Wrapper um fetch kann den erwarteten Antworttyp als Typparameter entgegennehmen und die Typprüfung an jeder Aufrufstelle wiederherstellen, ohne für jeden Endpunkt eine eigene Fetch-Funktion zu benötigen.
Bevor die Funktion selbst entsteht, lohnt es sich, die begleitenden Typen zu definieren: ein generisches Ergebnis-Envelope, ein einheitlicher Fehlertyp und optionale Request-Parameter. Diese Typen bilden das Fundament, auf dem die eigentliche generische Fetch-Funktion im nächsten Abschnitt aufbaut, und lassen sich unabhängig vom konkreten Endpunkt wiederverwenden.
// Shared error shape for every API call in the application
interface ApiError {
code: string;
message: string;
}
// Generic result type: either a typed success or a typed error
type FetchResult<T> =
| { ok: true; data: T }
| { ok: false; error: ApiError };
// Optional request configuration, independent of the response type
interface FetchOptions {
method?: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>;
body?: unknown;
}
7. Praxisbeispiel Teil 2: Die generische Fetch-Funktion
Mit den Typen aus dem vorherigen Abschnitt lässt sich die generische Fetch-Funktion selbst kompakt formulieren. Der Typparameter T beschreibt ausschließlich die erwartete Nutzdatenstruktur der Erfolgsantwort, während Fehlerfälle über den festen ApiError-Typ abgebildet werden. Innerhalb der Funktion bleibt eine einzige, an dieser Stelle bewusste Typ-Assertion nötig, weil der Compiler nicht wissen kann, dass ein beliebiges JSON tatsächlich die Form T hat. Diese Annahme wird an genau einer Stelle getroffen, statt sich unkontrolliert durch die gesamte Codebasis zu verteilen.
An der Aufrufstelle zahlt sich der Aufwand aus: typedFetch<Product>('/api/products/1') liefert ein vollständig typisiertes FetchResult<Product>, inklusive Autovervollständigung für result.data.price und einer vom Compiler erzwungenen Prüfung von result.ok, bevor auf data zugegriffen werden darf. Ein zweiter Aufruf mit einem anderen Typparameter, etwa typedFetch<User[]>('/api/users'), nutzt exakt dieselbe Implementierung, ohne eine einzige Zeile zu duplizieren.
// Generic fetch wrapper: T is only the success payload shape
async function typedFetch<T>(
url: string,
options: FetchOptions = {}
): Promise<FetchResult<T>> {
try {
const response = await fetch(url, {
method: options.method ?? "GET",
headers: options.headers,
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
return { ok: false, error: { code: String(response.status), message: response.statusText } };
}
// Single, deliberate assertion: the JSON shape is trusted here
const data = (await response.json()) as T;
return { ok: true, data };
} catch (err) {
return { ok: false, error: { code: "NETWORK_ERROR", message: String(err) } };
}
}
// Call sites reuse the same function for completely different shapes
const productResult = await typedFetch<Product>("/api/products/1");
if (productResult.ok) {
console.log(productResult.data.price); // fully typed, autocompletes
}
const usersResult = await typedFetch<User[]>("/api/users");
8. Namenskonventionen: T, K, V oder beschreibende Namen
Die Konvention, Typparameter mit einzelnen Großbuchstaben wie T, K, V zu benennen, stammt aus generischen Programmiersprachen wie C++ und Java und hat sich in TypeScript für einfache Fälle bewährt. T steht traditionell für Type, K für Key, V für Value, E für Element. Bei Funktionen mit ein oder zwei Typparametern und offensichtlichem Kontext, etwa identity<T> oder Map<K, V>, ist diese Kurzform lesbarer als ein ausgeschriebener Name, weil sie sofort als Typparameter statt als konkreter Typ erkennbar ist.
Sobald eine Funktion drei oder mehr Typparameter hat oder deren Bedeutung nicht aus dem unmittelbaren Kontext hervorgeht, verbessern beschreibende Namen wie TItem, TResponse oder TFormValues die Lesbarkeit deutlich. Ein führendes T als Präfix signalisiert dabei weiterhin, dass es sich um einen Typparameter und nicht um einen konkreten Typnamen handelt, ohne mit Interfaces oder Klassen im selben Namensraum zu kollidieren. Die Faustregel: Kurzformen für generische, algorithmische Utilities, sprechende Namen für domänenspezifische Strukturen mit mehreren gleichzeitig aktiven Typparametern.
9. Generics im Vergleich: wann sie sich lohnen und wann sie Overkill sind
Nicht jede Funktion profitiert von einem Typparameter. Die folgende Übersicht zeigt typische Situationen, in denen der Umstieg von any oder dupliziertem Code auf Generics einen klaren Unterschied macht, und wo ein einfacher konkreter Typ ausreicht.
| Szenario | Ohne Generics | Mit Generics | Vorteil |
|---|---|---|---|
| Funktion für einen Typ | firstString(arr: string[]): string |
first<T>(arr: T[]): T |
Eine Implementierung für alle Arraytypen |
| Duplizierte Utility-Funktionen | getFirstString, getFirstNumber, … |
first<T>(arr: T[]): T |
Keine Duplizierung, ein Testfall |
| API-Antwort verarbeiten | fetch(url).then(r => r.json()) als any |
typedFetch<Product>(url) |
Typsicherheit bis zur Aufrufstelle |
| Objekt-Property lesen | obj[key] mit key: string |
getProperty<T, K extends keyof T> |
Compiler verhindert nicht existierende Keys |
| Container für einen festen Typ | class ProductStack (Duplikat je Typ) |
class Stack<T> |
Eine Klasse für Stack<Product>, Stack<string> usw. |
Generics lohnen sich, sobald derselbe Codepfad für mehr als einen Typ wiederverwendet werden soll, etwa in Utility-Funktionen, generischen Containern oder eben einem Fetch-Wrapper. Overkill sind sie dagegen, wenn eine Funktion nachweislich nur mit einem einzigen konkreten Typ aufgerufen wird und das auch dauerhaft so bleibt: Ein Typparameter, der überall nur mit demselben Argument instanziiert wird, fügt Komplexität hinzu, ohne einen einzigen zusätzlichen Anwendungsfall abzudecken. Im Zweifel gilt: erst den konkreten Typ schreiben, erst bei der zweiten tatsächlichen Wiederverwendung zu einem Generic verallgemeinern.
Mironsoft
TypeScript-Tooling, typsichere Frontend-Architektur und Magento/Hyvä-Integrationen
Typsichere TypeScript-Architektur für euer Projekt?
Wir analysieren bestehenden TypeScript-Code, ersetzen any-Stellen und Duplizierung durch saubere Generics und bauen typsichere API-Layer für Magento- und Hyvä-Projekte, die auch bei wachsender Komplexität wartbar bleiben.
Code-Review
Analyse bestehender TypeScript-Typen auf any-Stellen und vermeidbare Duplizierung
Generic-Refactoring
Wiederverwendbare, typsichere Funktionen, Interfaces und Klassen einführen
API-Layer
Typisierte Fetch-Wrapper und Service-Schichten für Magento- und Headless-Projekte
10. Zusammenfassung
Die Generics-Grundlagen in TypeScript lösen ein wiederkehrendes Dilemma: any verschenkt Typsicherheit, dupliziertes Code je Typ erzeugt Wartungsaufwand. Generische Funktionen, Interfaces und Klassen ersetzen beide Ansätze durch eine einzige, typparametrisierte Implementierung, die der Compiler bei jedem Aufruf mit dem konkreten Typ prüft. Mehrere Typparameter mit Constraints wie K extends keyof T erlauben Beziehungen zwischen Typen, die mit any oder festen Typen unmöglich wären. Der typisierte API-Fetch-Wrapper zeigt, wie sich dieses Prinzip auf ein alltägliches Praxisproblem anwenden lässt: eine Funktion, beliebig viele Antworttypen, volle Typprüfung an jeder Aufrufstelle.
Die Namenskonvention T, K, V bleibt für einfache, algorithmische Utilities die lesbarste Wahl, während beschreibende Namen wie TItem oder TResponse bei komplexeren, domänenspezifischen Strukturen mit mehreren aktiven Typparametern die Übersicht verbessern. Generics sind kein Selbstzweck: Sie lohnen sich, sobald Code für mehr als einen Typ wiederverwendet wird, und sind Overkill, wenn ein konkreter Typ dauerhaft ausreicht. Wer diese Grenze im Blick behält, schreibt TypeScript-Code, der sowohl typsicher als auch tatsächlich lesbar bleibt.
Generics-Grundlagen - Das Wichtigste auf einen Blick
any vs. Generics
any schaltet die Typprüfung komplett ab. Generics behalten volle Typsicherheit bei gleichzeitiger Wiederverwendbarkeit für beliebige Typen.
Mehrere Typparameter
<K, V> für Key-Value-Strukturen, K extends keyof T für Constraints zwischen Typparametern.
Interfaces & Klassen
Generische Interfaces für wiederkehrende Wrapper-Strukturen, generische Klassen für typisierte Container und Stores.
Namenskonventionen
T/K/V für einfache Utilities, beschreibende Namen wie TItem für komplexe, domänenspezifische Strukturen.