Zum Inhalt springen
TypeScript Fortgeschritten 35 min

Generics Grundlagen

Lerne die Grundlagen von Generics in TypeScript für wiederverwendbare und typsichere Komponenten.

Aktualisiert:

Generics Grundlagen

Generics ermöglichen es dir, wiederverwendbare Komponenten zu erstellen, die mit verschiedenen Typen funktionieren, ohne die Typsicherheit zu verlieren. Sie sind eines der mächtigsten Features in TypeScript.

Das Problem ohne Generics

// Option 1: Nur für einen Typ
function getFirst(arr: number[]): number {
    return arr[0];
}
// Funktioniert nur mit number[]

// Option 2: Mit any (verliert Typsicherheit)
function getFirstAny(arr: any[]): any {
    return arr[0];
}
const first = getFirstAny(["a", "b"]);  // Typ: any (schlecht!)

Generics einführen

Mit Generics behältst du die Typsicherheit:

// T ist ein Type Parameter (Platzhalter)
function getFirst<T>(arr: T[]): T {
    return arr[0];
}

// TypeScript inferiert den Typ
const num = getFirst([1, 2, 3]);        // Typ: number
const str = getFirst(["a", "b", "c"]);  // Typ: string

// Oder explizit angeben
const explicit = getFirst<string>(["x", "y"]);  // Typ: string

Generic Syntax

// Funktion
function identity<T>(value: T): T {
    return value;
}

// Arrow Function
const identity2 = <T>(value: T): T => value;

// Mit mehreren Type Parameters
function pair<K, V>(key: K, value: V): [K, V] {
    return [key, value];
}

const p1 = pair("name", "Max");     // [string, string]
const p2 = pair(1, { x: 10 });      // [number, { x: number }]

Generic Interfaces

// Generic Interface
interface Container<T> {
    value: T;
    getValue(): T;
}

// Verwendung
const numContainer: Container<number> = {
    value: 42,
    getValue() { return this.value; }
};

const strContainer: Container<string> = {
    value: "hello",
    getValue() { return this.value; }
};

Generic Type Aliases

// Generic Type Alias
type Result<T> = {
    success: boolean;
    data?: T;
    error?: string;
};

// Verschiedene Instanziierungen
type UserResult = Result<User>;
type NumberResult = Result<number>;

// Verwendung
function fetchUser(): Result<User> {
    return {
        success: true,
        data: { id: 1, name: "Max" }
    };
}

Generic Constraints

Beschränke, welche Typen akzeptiert werden:

// T muss ein Objekt mit 'length' Property sein
function logLength<T extends { length: number }>(item: T): void {
    console.log(item.length);
}

logLength("hello");       // OK: string hat length
logLength([1, 2, 3]);     // OK: array hat length
logLength({ length: 10 }); // OK: hat length Property
logLength(123);           // Fehler: number hat kein length

Constraints mit Interfaces

interface Identifiable {
    id: number;
}

function findById<T extends Identifiable>(items: T[], id: number): T | undefined {
    return items.find(item => item.id === id);
}

const users = [
    { id: 1, name: "Max" },
    { id: 2, name: "Anna" }
];

const user = findById(users, 1);  // Typ: { id: number; name: string } | undefined

keyof und Generics

// K muss ein Key von T sein
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { name: "Max", age: 25, email: "max@example.com" };

const name = getProperty(user, "name");  // Typ: string
const age = getProperty(user, "age");    // Typ: number
getProperty(user, "invalid");            // Fehler: "invalid" ist kein Key

Default Type Parameters

// T hat einen Standardtyp
interface Response<T = unknown> {
    data: T;
    status: number;
}

// Ohne Type Parameter
const response1: Response = { data: "anything", status: 200 };

// Mit Type Parameter
const response2: Response<User> = { data: { id: 1, name: "Max" }, status: 200 };

Generics in Klassen

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];
    }

    isEmpty(): boolean {
        return this.items.length === 0;
    }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const num = numberStack.pop();  // Typ: number | undefined

const stringStack = new Stack<string>();
stringStack.push("hello");

Praktische Beispiele

Generic API Response Handler

type ApiResponse<T> = {
    data: T;
    meta: {
        page: number;
        total: number;
    };
};

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
    const response = await fetch(url);
    return response.json();
}

// Typsichere API-Aufrufe
type User = { id: number; name: string };
type Post = { id: number; title: string; content: string };

const users = await fetchApi<User[]>("/api/users");
// users.data ist User[]

const posts = await fetchApi<Post[]>("/api/posts");
// posts.data ist Post[]

Generic State Container

class Store<State> {
    private state: State;
    private listeners: ((state: State) => void)[] = [];

    constructor(initialState: State) {
        this.state = initialState;
    }

    getState(): State {
        return this.state;
    }

    setState(newState: Partial<State>): void {
        this.state = { ...this.state, ...newState };
        this.listeners.forEach(listener => listener(this.state));
    }

    subscribe(listener: (state: State) => void): () => void {
        this.listeners.push(listener);
        return () => {
            this.listeners = this.listeners.filter(l => l !== listener);
        };
    }
}

// Verwendung
type AppState = {
    user: User | null;
    theme: "light" | "dark";
    count: number;
};

const store = new Store<AppState>({
    user: null,
    theme: "light",
    count: 0
});

store.subscribe((state) => {
    console.log("State changed:", state.count);
});

store.setState({ count: store.getState().count + 1 });

Generic Event Emitter

type EventMap = Record<string, unknown>;

class EventEmitter<Events extends EventMap> {
    private handlers = new Map<keyof Events, Function[]>();

    on<K extends keyof Events>(
        event: K,
        handler: (data: Events[K]) => void
    ): void {
        const existing = this.handlers.get(event) || [];
        this.handlers.set(event, [...existing, handler]);
    }

    emit<K extends keyof Events>(event: K, data: Events[K]): void {
        const handlers = this.handlers.get(event) || [];
        handlers.forEach(handler => handler(data));
    }
}

// Typsichere Events
type AppEvents = {
    userLogin: { userId: number; timestamp: Date };
    userLogout: { userId: number };
    pageView: { path: string };
};

const emitter = new EventEmitter<AppEvents>();

emitter.on("userLogin", (data) => {
    // data ist { userId: number; timestamp: Date }
    console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

emitter.emit("userLogin", { userId: 1, timestamp: new Date() });
// emitter.emit("userLogin", { wrong: true });  // Fehler!

Utility-Funktionen

// Array-Operationen
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
    return arr.map(fn);
}

function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
    return arr.filter(predicate);
}

// Objekt-Manipulation
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 user = { id: 1, name: "Max", email: "max@example.com", password: "secret" };
const publicUser = pick(user, ["id", "name", "email"]);
// Typ: { id: number; name: string; email: string }

Zusammenfassung

  • Type Parameter (<T>) - Platzhalter für Typen
  • Mehrere Parameter (<T, U>) - Für komplexere Fälle
  • Constraints (extends) - Typ einschränken
  • keyof - Keys eines Objekttyps
  • Default Types - Standardwert für Type Parameter

Best Practices:

  • Verwende aussagekräftige Namen (nicht nur T, U, V)
  • Setze Constraints wenn möglich
  • Nutze Inference statt expliziter Type Arguments
  • Vermeide übermäßig komplexe Generics

Im nächsten Modul lernst du Interfaces und Types im Detail!

Zurück zum TypeScript Kurs