DevToolBoxKOSTENLOS
Blog

TypeScript Generics erklärt: Praktischer Leitfaden mit Beispielen

14 Min. Lesezeitvon DevToolBox

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: string

Generische 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 string

Standard-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 } | undefined

Bedingte 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>;          // number

Bedingte 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 TypeBeschreibungBeispiel
Partial<T>Macht alle Eigenschaften optionalPartial<User>
Required<T>Macht alle Eigenschaften erforderlichRequired<Partial<User>>
Pick<T, K>Waehlt angegebene EigenschaftenPick<User, "name" | "email">
Omit<T, K>Schliesst angegebene Eigenschaften ausOmit<User, "password">
Record<K, V>Erstellt ein Schluessel-Wert-Typ-MappingRecord<string, number>
Readonly<T>Macht alle Eigenschaften schreibgeschuetztReadonly<Config>
ReturnType<T>Extrahiert den Rueckgabetyp einer FunktionReturnType<typeof fn>
Parameters<T>Extrahiert das Tupel der ParametertypenParameters<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:

FehlerProblemKorrektur
any statt Generics verwendenVerlust aller TypinformationenTypparameter T statt any verwenden
Typparameter zu stark einschraenkenMacht den Generic fuer gueltige Typen unbrauchbarNur einschraenken, was wirklich benoetigt wird
Unnoetige TypparameterEin nur einmal verwendetes T erhoeht die KomplexitaetWenn T nur im Rueckgabetyp vorkommt, konkreten Typ verwenden
Keine Einschraenkungen beim EigenschaftszugriffTypeScript-Fehler: Eigenschaft existiert nicht auf Typ Textends-Einschraenkung hinzufuegen
Generische Standardwerte und Einschraenkungen verwechselnDer Standardtyp beschraenkt nicht — Aufrufer koennen andere Typen uebergebenextends zum Einschraenken, = fuer Standard
Nicht inferieren, wenn moeglichExplizite Typangabe, wenn TS inferieren kannTypeScript aus Argumenten inferieren lassen
T[] statt readonly T[] verwendenErlaubt Mutation von Arrays, die schreibgeschuetzt sein solltenreadonly 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.

𝕏 Twitterin LinkedIn
War das hilfreich?

Bleiben Sie informiert

Wöchentliche Dev-Tipps und neue Tools.

Kein Spam. Jederzeit abbestellbar.

Verwandte Tools ausprobieren

TSJSON to TypeScriptJSTypeScript to JavaScriptGTGraphQL to TypeScript

Verwandte Artikel

JSON zu TypeScript: Vollständiger Leitfaden mit Beispielen

Erfahren Sie, wie Sie JSON-Daten automatisch in TypeScript-Interfaces konvertieren. Verschachtelte Objekte, Arrays, optionale Felder und Best Practices.

TypeScript vs JavaScript: Wann und wie man konvertiert

Praktischer Leitfaden, wann man TypeScript in JavaScript konvertiert und umgekehrt. Migrationsstrategien, Tooling, Bundle-Größe und Teamüberlegungen.

JSON zu Zod Schema: Typsichere Laufzeitvalidierung in TypeScript

Erfahren Sie, wie Sie JSON in Zod-Schemas konvertieren fur typsichere Laufzeitvalidierung in TypeScript.

GraphQL zu TypeScript: Code-Generierung Guide

Generieren Sie TypeScript-Typen aus GraphQL-Schemas.