React & TypeScript: Bewährte Patterns für saubereren Code

Beim Entwickeln von React-Anwendungen mit TypeScript begegnen Sie immer wieder denselben Herausforderungen: Wie typisiert man Props korrekt, behandelt Events sicher und strukturiert Komponenten für maximale Wiederverwendbarkeit? Diese Patterns dienen nicht nur dazu, den Compiler zufriedenzustellen – sie helfen dabei, Code zu schreiben, der einfacher zu verstehen, zu refactoren und zu warten ist.
Dieser Artikel behandelt die praktischsten React TypeScript-Patterns, die Sie täglich verwenden werden. Sie lernen, wie Sie Komponenten effektiv typisieren, optionale Props handhaben, mit Events und Refs arbeiten, Utility-Types nutzen und Discriminated Unions in Reducern verwenden. Jedes Pattern enthält fokussierte Beispiele, die genau zeigen, warum es Ihren Code verbessert.
Wichtige Erkenntnisse
- Props und State explizit typisieren mit Interfaces oder Types für bessere IntelliSense und Fehlererkennung
- Optionale und Standard-Props handhaben mit TypeScripts optionalen Eigenschaften und Standardparametern
- Utility-Types verwenden wie
Pick
undComponentProps
, um Wiederholungen zu reduzieren und Konsistenz zu gewährleisten - Events und Refs korrekt typisieren für typsichere DOM-Interaktionen
- Discriminated Unions nutzen in Reducern für erschöpfende Action-Behandlung
- Komponenten mit forwardRef komponieren, um Typsicherheit beim Weiterreichen von Refs zu gewährleisten
Typisierung von Props und State
Das Fundament der React TypeScript-Patterns beginnt mit der korrekten Typisierung Ihrer Komponenten-Props. Dies fängt Fehler zur Compile-Zeit ab und bietet bessere Autovervollständigung in Ihrem Editor.
Grundlegendes Props-Pattern
interface ButtonProps {
label: string
onClick: () => void
disabled?: boolean
}
function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
)
}
Dieses Pattern stellt sicher, dass Ihre Komponenten die korrekten Datentypen erhalten. Das Interface dokumentiert klar, welche Props die Komponente erwartet, wodurch sie selbstdokumentierend wird.
State mit useState typisieren
TypeScript leitet State-Typen normalerweise korrekt ab, aber komplexer State profitiert von expliziter Typisierung:
import { useState } from 'react'
interface User {
id: string
name: string
email: string
}
function UserProfile() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(false) // Typ wird als boolean abgeleitet
// TypeScript weiß, dass user null sein könnte
return user ? <div>{user.name}</div> : <div>Loading...</div>
}
Umgang mit optionalen und Standard-Props
React-Komponenten haben oft optionale Konfigurations-Props. TypeScripts optionale Eigenschaften kombiniert mit Standardparametern schaffen ein sauberes Pattern:
interface CardProps {
title: string
description?: string
variant?: 'primary' | 'secondary'
className?: string
}
function Card({
title,
description,
variant = 'primary',
className = ''
}: CardProps) {
return (
<div className={`card card-${variant} ${className}`}>
<h2>{title}</h2>
{description && <p>{description}</p>}
</div>
)
}
Dieses Pattern macht Komponenten flexibel und behält dabei die Typsicherheit bei. Erforderliche Props sind klar ersichtlich, und optionale Props haben sinnvolle Standardwerte.
Typisierung von Events und Refs
Event-Handler und Refs sind häufige Quellen für TypeScript-Fehler. Diese Patterns eliminieren das Rätselraten:
Event-Handler-Pattern
import { useState, ChangeEvent, FormEvent } from 'react'
interface FormProps {
onSubmit: (data: { email: string; password: string }) => void
}
function LoginForm({ onSubmit }: FormProps) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value)
}
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
onSubmit({ email, password })
}
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={handleEmailChange} />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit">Login</button>
</form>
)
}
Ref-Pattern
import { useRef, useEffect } from 'react'
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// TypeScript weiß, dass inputRef.current null sein könnte
inputRef.current?.focus()
}, [])
return <input ref={inputRef} type="text" />
}
Verwendung von Utility-Types
TypeScripts Utility-Types reduzieren Code-Duplikation und halten Typen in Ihrer Anwendung synchronisiert.
ComponentProps-Pattern
Extrahieren Sie Props von bestehenden Komponenten, ohne Typdefinitionen zu wiederholen:
import { ComponentProps } from 'react'
// Alle Props abrufen, die ein button-Element akzeptiert
type ButtonProps = ComponentProps<'button'> & {
variant?: 'primary' | 'secondary'
}
function StyledButton({ variant = 'primary', ...props }: ButtonProps) {
return <button className={`btn-${variant}`} {...props} />
}
// Props von eigenen Komponenten extrahieren
type MyButtonProps = ComponentProps<typeof StyledButton>
Pick-Pattern
Wählen Sie spezifische Props beim Erweitern von Komponenten aus:
interface UserData {
id: string
name: string
email: string
role: string
lastLogin: Date
}
// Nur einige Eigenschaften für die Karte benötigt
type UserCardProps = Pick<UserData, 'name' | 'email'> & {
onClick?: () => void
}
function UserCard({ name, email, onClick }: UserCardProps) {
return (
<div onClick={onClick}>
<h3>{name}</h3>
<p>{email}</p>
</div>
)
}
Komponenten-Komposition mit forwardRef
Beim Erstellen von Komponenten-Bibliotheken oder beim Wrappen nativer Elemente behält forwardRef
die Typsicherheit bei und stellt Refs zur Verfügung:
import { forwardRef, useRef, InputHTMLAttributes } from 'react'
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string
error?: string
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
)
}
)
Input.displayName = 'Input'
// Verwendung behält vollständige Typsicherheit bei
function Form() {
const inputRef = useRef<HTMLInputElement>(null)
return <Input ref={inputRef} label="Email" type="email" />
}
Discriminated Unions in Reducern
Discriminated Unions machen Reducer-Actions typsicher und ermöglichen erschöpfende Prüfung:
interface TodoState {
items: Array<{ id: string; text: string; done: boolean }>
filter: 'all' | 'active' | 'completed'
}
type TodoAction =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: string }
| { type: 'SET_FILTER'; payload: TodoState['filter'] }
| { type: 'CLEAR_COMPLETED' }
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
items: [...state.items, {
id: Date.now().toString(),
text: action.payload,
done: false
}]
}
case 'TOGGLE_TODO':
return {
...state,
items: state.items.map(item =>
item.id === action.payload
? { ...item, done: !item.done }
: item
)
}
case 'SET_FILTER':
return { ...state, filter: action.payload }
case 'CLEAR_COMPLETED':
return {
...state,
items: state.items.filter(item => !item.done)
}
default:
// TypeScript stellt sicher, dass alle Fälle behandelt werden
const exhaustive: never = action
return state
}
}
Dieses Pattern stellt sicher, dass Sie alle Action-Typen behandeln. Wenn Sie einen neuen Action-Typ hinzufügen, wird TypeScript einen Fehler ausgeben, bis Sie ihn im Reducer behandeln.
React 19-Updates
Mit React 19 machen neue Funktionen wie Actions (useActionState
, useFormStatus
), die use()
-API für das Konsumieren von Promises oder Context und die erstklassige Unterstützung für Server Components die Typsicherheit noch wichtiger. Beim Typisieren von Actions definieren Sie explizit die Form von FormData
und den Rückgabewert der Action, damit Server- und Client-Code synchron bleiben. Die kommende Umstellung von forwardRef
auf die Übergabe von ref
als Standard-Prop vereinfacht auch die Typisierung – behandeln Sie ref
wie jede andere Prop und lassen Sie TypeScript die Korrektheit über Komponentengrenzen hinweg sicherstellen.
Fazit
Diese React TypeScript-Patterns bilden das Fundament typsicherer React-Anwendungen. Durch explizite Typisierung von Props und State, korrekte Behandlung von Events und Refs und die Nutzung von TypeScripts Utility-Types erstellen Sie Code, der sowohl sicherer als auch wartbarer ist. Die Patterns funktionieren zusammen – ordnungsgemäße Prop-Typisierung ermöglicht bessere Komponenten-Komposition, während Discriminated Unions erschöpfende Behandlung in komplexer State-Logik gewährleisten.
Beginnen Sie mit grundlegender Prop-Typisierung und integrieren Sie schrittweise Utility-Types und erweiterte Patterns, während Ihre Anwendung wächst. Die Investition in ordnungsgemäße Typisierung zahlt sich durch bessere IDE-Unterstützung, weniger Laufzeitfehler und klarere Komponenten-Verträge aus.
Häufig gestellte Fragen
Verwenden Sie interface, wenn Sie möglicherweise später Deklarationen erweitern oder zusammenführen müssen. Verwenden Sie type für Unions, Intersections oder bei der Arbeit mit Utility-Types. Beide funktionieren gut für grundlegende Props, also wählen Sie einen Ansatz und bleiben Sie konsistent.
Verwenden Sie ReactNode für maximale Flexibilität: children: ReactNode. Dies akzeptiert Strings, Zahlen, Elemente, Fragmente und Arrays. Für spezifischere Fälle verwenden Sie ReactElement oder spezifische Element-Typen.
Verwenden Sie generische Funktionen, die die ursprünglichen Prop-Typen der Komponente beibehalten und gleichzeitig neue hinzufügen. Der ComponentProps Utility-Type hilft dabei, bestehende Komponenten-Typen zu extrahieren und zu erweitern, ohne Wiederholungen.
Typisieren Sie Funktionsparameter, Rückgabetypen für exportierte Funktionen und komplexen State explizit. Lassen Sie TypeScript einfache Fälle wie useState mit Primitiven oder offensichtliche Event-Handler ableiten, um Weitschweifigkeit zu reduzieren.