Fortgeschritten142025-01-15

TypeScript Generics - Wiederverwendbare Typen

Lerne Generics in TypeScript - das Werkzeug für flexible und wiederverwendbare Komponenten, Funktionen und Classes.

#typescript#generics#advanced#types

TypeScript Generics - Wiederverwendbare Typen

Generics erlauben flexible, wiederverwendbare Komponenten die mit verschiedenen Typen arbeiten.

Was sind Generics?

Ohne Generics müsstest du Code duplizieren:

// Für numbers
function identityNumber(value: number): number {
  return value
}

// Für strings
function identityString(value: string): string {
  return value
}

// Für booleans
function identityBoolean(value: boolean): boolean {
  return value
}

Mit Generics brauchst du nur eine Funktion:

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

const num = identity(42) // Type: number
const str = identity("Hello") // Type: string
const bool = identity(true) // Type: boolean

<T> ist ein Type Parameter - ein Platzhalter für einen Typ!

Generic Functions

Basic Syntax

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0]
}

const num = firstElement([1, 2, 3]) // Type: number | undefined
const str = firstElement(["a", "b"]) // Type: string | undefined

Mehrere Type Parameters

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second]
}

const p1 = pair("Max", 25) // Type: [string, number]
const p2 = pair(true, "Active") // Type: [boolean, string]

Generic Constraints

Type Parameters einschränken:

// T muss length Property haben
function logLength<T extends { length: number }>(item: T): void {
  console.log(item.length)
}

logLength("Hello") // ✅ OK: string has length
logLength([1, 2, 3]) // ✅ OK: array has length
logLength(42) // ❌ Error: number has no length

Häufige Constraints:

// Muss Object sein (nicht primitive)
function clone<T extends object>(obj: T): T {
  return { ...obj }
}

// Muss Array sein
function first<T extends any[]>(arr: T): T[0] {
  return arr[0]
}

// Muss Interface implementieren
interface Named {
  name: string
}

function greet<T extends Named>(obj: T): string {
  return `Hello ${obj.name}`
}

Generic Interfaces

interface Box<T> {
  value: T
  getValue(): T
  setValue(value: T): void
}

const numberBox: Box<number> = {
  value: 42,
  getValue() { return this.value },
  setValue(value) { this.value = value }
}

const stringBox: Box<string> = {
  value: "Hello",
  getValue() { return this.value },
  setValue(value) { this.value = value }
}

Generic Interface mit mehreren Types

interface Pair<K, V> {
  key: K
  value: V
}

const pair1: Pair<string, number> = {
  key: "age",
  value: 25
}

const pair2: Pair<number, string> = {
  key: 1,
  value: "First"
}

Generic Classes

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)
console.log(numberStack.pop()) // 2

const stringStack = new Stack<string>()
stringStack.push("Hello")
stringStack.push("World")

Generic Class mit Constraints

interface Comparable<T> {
  compareTo(other: T): number
}

class SortedList<T extends Comparable<T>> {
  private items: T[] = []

  add(item: T): void {
    this.items.push(item)
    this.items.sort((a, b) => a.compareTo(b))
  }

  get(index: number): T | undefined {
    return this.items[index]
  }
}

keyof Operator

Für Type-Safe Property Access:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const person = {
  name: "Max",
  age: 25,
  email: "max@test.com"
}

const name = getProperty(person, "name") // Type: string
const age = getProperty(person, "age") // Type: number
const invalid = getProperty(person, "address") // ❌ Error

Real-World Beispiel:

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@test.com",
  password: "secret"
}

const publicUser = pick(user, ["id", "name", "email"])
// Type: { id: number; name: string; email: string }

Generic Type Aliases

type Result<T> = { success: true; data: T } | { success: false; error: string }

function fetchUser(id: number): Result<User> {
  // ...
}

const result = fetchUser(1)
if (result.success) {
  console.log(result.data.name) // Type: User
} else {
  console.log(result.error) // Type: string
}

Promise<T>

async function fetchData(): Promise<string> {
  return "Data"
}

async function fetchUser(): Promise<{ name: string; age: number }> {
  return { name: "Max", age: 25 }
}

// TypeScript weiß: data ist string
const data = await fetchData()

// TypeScript weiß: user ist { name: string; age: number }
const user = await fetchUser()

Default Type Parameters

interface Response<T = string> {
  data: T
  status: number
}

// Mit Default: T = string
const response1: Response = {
  data: "Success",
  status: 200
}

// Mit explizitem Type
const response2: Response<{ id: number }> = {
  data: { id: 1 },
  status: 200
}

Generic Constraints mit typeof

function createInstance<T>(constructor: new () => T): T {
  return new constructor()
}

class Person {
  name: string = "Unknown"
}

const person = createInstance(Person)
console.log(person.name) // "Unknown"
Real-World Generic API Client
interface ApiResponse<T> {
  data: T
  status: number
  message?: string
}

class ApiClient {
  async get<T>(url: string): Promise<ApiResponse<T>> {
    const response = await fetch(url)
    return {
      data: await response.json(),
      status: response.status
    }
  }

  async post<T, U>(url: string, body: U): Promise<ApiResponse<T>> {
    const response = await fetch(url, {
      method: "POST",
      body: JSON.stringify(body)
    })
    return {
      data: await response.json(),
      status: response.status
    }
  }
}

// Usage
interface User {
  id: number
  name: string
}

const client = new ApiClient()
const response = await client.get<User[]>("/api/users")
// response.data ist Type: User[]

const newUser = await client.post<User, { name: string }>("/api/users", {
  name: "Max"
})
// newUser.data ist Type: User

📝 Quiz

Was macht der keyof Operator?

Tipps & Tricks

Generic Type Inference

TypeScript kann Generics oft automatisch erkennen:

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

// TypeScript erkennt automatisch:
// T = number, U = string
const result = map([1, 2, 3], n => n.toString())

Conditional Types mit Generics

type IsArray<T> = T extends any[] ? true : false

type A = IsArray<number[]> // true
type B = IsArray<string> // false

// Praktisches Beispiel
type Flatten<T> = T extends any[] ? T[number] : T

type Num = Flatten<number[]> // number
type Str = Flatten<string> // string

Mapped Types mit Generics

type Nullable<T> = {
  [K in keyof T]: T[K] | null
}

interface User {
  name: string
  age: number
}

type NullableUser = Nullable<User>
// { name: string | null; age: number | null }

Häufige Fehler

Fehler 1: Zu generische Generics

FALSCH:

function process<T>(data: T): T {
  return data.map(x => x * 2) // ❌ Error: T hat kein map
}

RICHTIG:

function process<T extends number[]>(data: T): T {
  return data.map(x => x * 2) as T
}

Fehler 2: any statt Generics

FALSCH:

function identity(value: any): any {
  return value
}

const result = identity(42) // Type: any (nicht number!)

RICHTIG:

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

const result = identity(42) // Type: number

Fehler 3: Generics bei Simple Cases

ÜBERTRIEBEN:

function add<T extends number>(a: T, b: T): number {
  return a + b
}

BESSER:

function add(a: number, b: number): number {
  return a + b
}

Regel: Nutze Generics nur wenn du sie brauchst!

🎯

Zusammenfassung

Du hast gelernt:

  • ✅ Generics für wiederverwendbare Komponenten
  • ✅ Type Parameters mit <T>
  • ✅ Generic Constraints mit extends
  • ✅ Generic Functions, Interfaces, Classes
  • ✅ keyof für Type-Safe Property Access
  • ✅ Default Type Parameters
  • ✅ Promise<T> und async/await

Key Takeaways:

  • Generics = Platzhalter für Typen
  • Constraints mit extends
  • keyof für Property-Namen
  • Type Inference nutzen wo möglich
  • Nicht übertreiben - nur bei Bedarf nutzen

Nächste Schritte

Als Nächstes lernst du:

  • Union und Intersection Types
  • Type Guards und Narrowing
  • Utility Types (Pick, Omit, Partial, etc.)
  • Advanced Types

Viel Erfolg! 🚀

TypeScriptLektion 6 von 15
40% abgeschlossen
Lektion abgeschlossen!

Gut gemacht! 🎉

Du hast "TypeScript Generics - Wiederverwendbare Typen" abgeschlossen

Artikel bewerten

0.0 (0 Bewertungen)

Bitte einloggen um zu bewerten