satisfies, Template Literals & Generics
TypeScript mit React auf das richtige Level zu bringen bedeutet mehr als Props mit Interfaces tippen. Der satisfies-Operator, Template Literal Types, Generics in Komponenten und discriminated unions ermöglichen Props-APIs, die zur Laufzeit unmöglich falsch verwendet werden können — weil TypeScript alle Fehler schon beim Schreiben markiert.
Inhaltsverzeichnis
- 1. Warum fortgeschrittenes TypeScript in React lohnt
- 2. Der satisfies-Operator: Validierung ohne Typverlust
- 3. Template Literal Types: Strings als Typen
- 4. Generics in React-Komponenten
- 5. Discriminated Unions für Props-Varianten
- 6. Conditional Types für dynamische Props
- 7. Mapped Types für Props-Transformationen
- 8. Custom Hooks mit Generics
- 9. TypeScript-Patterns im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum fortgeschrittenes TypeScript in React lohnt
Grundlegendes TypeScript in React — Props als Interfaces tippen, Return-Types annotieren, Events korrekt typen — verhindert die offensichtlichen Fehler. Aber ab einer gewissen Codebase-Größe reicht das nicht mehr. Props-APIs mit optionalen und bedingten Feldern, generische Listen-Komponenten, die mit jedem Datentyp funktionieren sollen, oder Konfigurationsobjekte, die typsicher aber flexibel sein müssen — das sind Szenarien, in denen grundlegendes TypeScript zu any, zu as-Casts oder zu aufgeblähten Union-Types führt. Fortgeschrittene TypeScript-Features lösen diese Probleme strukturell.
Der ROI fortgeschrittener TypeScript-Patterns ist in React-Projekten besonders hoch, weil Komponenten-APIs langlebig sind. Eine Button-Komponente, die heute definiert wird, wird in zwei Jahren noch von anderen Entwicklern verwendet — möglicherweise ohne das Original zu kennen. Ein typsicheres Props-Interface, das falsche Verwendungen zur Compile-Zeit verbietet, ist die beste Dokumentation. TypeScript mit discriminated unions, Generics und Template Literal Types macht diese Absicherung ohne Laufzeit-Overhead möglich — alles passiert im Compile-Schritt.
2. Der satisfies-Operator: Validierung ohne Typverlust
Der satisfies-Operator wurde in TypeScript 4.9 eingeführt und löst ein elegantes Problem: Er prüft, ob ein Wert einem Typ entspricht, ohne den Typ des Wertes auf diesen Typ zu verengen. Das klingt abstrakt, hat aber direkte praktische Auswirkungen. Wenn man ein Konfigurationsobjekt mit const config: RouteConfig = { home: '/', about: '/about' } annotiert, verliert TypeScript die Information, dass config.home genau '/' ist — es kennt nur noch den Typ string. Mit const config = { home: '/', about: '/about' } satisfies RouteConfig prüft TypeScript die Konformität, behält aber die exakten Literal-Types bei.
In React-Projekten ist satisfies besonders wertvoll für Konfigurationsobjekte, die typisiert aber gleichzeitig mit ihren exakten Werten zugänglich sein sollen. Theme-Konfigurationen, Route-Maps, Icon-Registries und Event-Handler-Maps profitieren alle davon. Wer vorher as const und explizite Typ-Annotations kombinieren musste, um beide Ziele zu erreichen, kann jetzt einfacher und ausdrucksstärker mit satisfies arbeiten. Der Operator übergibt die Typen nicht mehr als Constraint, sondern als Validator — ein semantisch wichtiger Unterschied.
// satisfies: validate against type without losing literal type information
import type { ComponentType } from 'react';
// Route configuration type — keys are string, values are component + meta
type RouteConfig = Record<string, {
component: ComponentType;
title: string;
requiresAuth?: boolean;
}>;
// With 'as RouteConfig': TypeScript widens all keys to string, loses literal types
// const routes: RouteConfig = { ... } ← 'home' is just 'string'
// With satisfies: TypeScript validates shape AND preserves literal key types
const routes = {
home: { component: HomePage, title: 'Startseite' },
about: { component: AboutPage, title: 'Über uns' },
dashboard: { component: DashboardPage, title: 'Dashboard', requiresAuth: true },
} satisfies RouteConfig;
// TypeScript now knows exactly which keys exist — autocomplete works!
routes.home.title; // ✓ TypeScript knows 'home' key exists
routes.nonexistent; // ✗ TypeScript error: key does not exist
// Template literal — derive union type from object keys
type AppRoutes = keyof typeof routes; // 'home' | 'about' | 'dashboard'
// Satisfies for icon registry — validate shape, keep icon-name autocomplete
type IconRegistry = Record<string, React.FC<{ size?: number }>>;
const icons = {
arrow: ({ size = 16 }) => <ArrowIcon size={size} />,
close: ({ size = 16 }) => <CloseIcon size={size} />,
menu: ({ size = 16 }) => <MenuIcon size={size} />,
} satisfies IconRegistry;
type IconName = keyof typeof icons; // 'arrow' | 'close' | 'menu'
3. Template Literal Types: Strings als Typen
Template Literal Types kombinieren String-Literal-Types zu neuen Typen — genau wie Template Literals in JavaScript, aber auf der Typ-Ebene. type EventName = `on${Capitalize<string>}` erzeugt einen Typ, der alle Strings akzeptiert, die mit "on" gefolgt von einem Großbuchstaben beginnen. Das ist die Grundlage für typsichere Event-Handler-Namen. type DataAttr = `data-${string}` akzeptiert jeden gültigen Data-Attribut-Namen. Für React-Projekte ermöglichen Template Literal Types, dass dynamisch zusammengesetzte Strings — Klassen-Namen, Event-Namen, API-Endpunkte — zur Compile-Zeit validiert werden.
Ein besonders mächtiges Anwendungsgebiet in React sind variante Klassen-Namen. Wenn man ein Design-System mit farbigen Varianten aufbaut, kann man den Typ type ColorVariant = 'primary' | 'secondary' | 'danger' und type Size = 'sm' | 'md' | 'lg' zu einem vollständigen Klassen-Typ kombinieren: type VariantClass = `btn-${ColorVariant}-${Size}`. TypeScript kennt dann alle neun erlaubten Kombinationen und warnt bei jeder falschen Variante — ohne dass man sie manuell aufzählen muss. Das ist der Unterschied zwischen einer Liste von 9 Strings und einem System, das Korrektheit strukturell garantiert.
4. Generics in React-Komponenten
Generische React-Komponenten sind die mächtigste Form der Wiederverwendung in TypeScript-React-Projekten. Eine generische Table<T>-Komponente kann mit jedem Datentyp arbeiten und gibt dabei vollständige Typsicherheit für alle Operationen: die columns-Prop weiß, welche Keys von T es gibt, der Row-Klick-Handler bekommt ein T-Objekt und nicht any, und die Sortier-Funktion kann nur Felder sortieren, die in T tatsächlich existieren. Das ist der Unterschied zwischen einer Komponente, die "irgendwie" typisiert ist, und einer, die zur Compile-Zeit unmöglich falsch verwendet werden kann.
Die Syntax für Generics in React-Funktionskomponenten mit Arrow-Functions erfordert einen kleinen Trick in TSX-Dateien: <T,> oder <T extends object> statt nur <T>, weil der Compiler sonst den generischen Parameter als JSX-Tag interpretiert. In regulären function-Deklarationen ist dieses Problem nicht vorhanden. Für komplexere Generic-Constraints verwendet man extends: <T extends { id: string }> garantiert, dass jeder Datentyp mindestens ein id-Feld hat, was für eindeutige React-Keys notwendig ist.
// Generic Table component — fully type-safe with any data shape
import React from 'react';
// Column definition knows which keys T has — autocomplete works
type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
sortable?: boolean;
};
interface TableProps<T extends { id: string | number }> {
data: T[];
columns: Column<T>[];
onRowClick?: (row: T) => void; // receives correctly typed T, not any
keyExtractor?: (row: T) => string | number;
}
// Arrow function generic needs trailing comma in TSX to avoid JSX ambiguity
const Table = <T extends { id: string | number }>({
data,
columns,
onRowClick,
keyExtractor = row => row.id,
}: TableProps<T>) => (
<table>
<thead>
<tr>
{columns.map(col => (
<th key={String(col.key)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={keyExtractor(row)} onClick={() => onRowClick?.(row)}>
{columns.map(col => (
<td key={String(col.key)}>
{col.render
? col.render(row[col.key], row)
: String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
// Usage — TypeScript infers T as Product from the data prop
interface Product { id: string; name: string; price: number; stock: number; }
const ProductTable = () => (
<Table<Product>
data={products}
columns={[
{ key: 'name', header: 'Produkt' }, // ✓ only Product keys allowed
{ key: 'price', header: 'Preis', render: (v) => `${v} €` },
{ key: 'nonexistent', header: '...' }, // ✗ TypeScript error immediately
]}
onRowClick={product => console.log(product.name)} // product is Product, not any
/>
);
5. Discriminated Unions für Props-Varianten
Discriminated Unions sind der typsicherste Weg, Props-Varianten in React zu modellieren, die sich gegenseitig ausschließen. Das klassische Beispiel: Ein Button, der entweder als Link (href) oder als normaler Button (onClick) fungiert — aber nicht beides gleichzeitig, und das eine macht ohne das andere keinen Sinn. Ohne discriminated unions würde man beide Props optional machen und Laufzeitprüfungen schreiben. Mit einer discriminated union hat jede Variante ihren eigenen Typ-Zweig, und TypeScript macht es zur Compile-Zeit unmöglich, den falschen Zweig falsch zu verwenden.
Das "Discriminant" ist eine gemeinsame Property mit einem Literal-Type — typischerweise variant, type oder as. TypeScript nutzt diese Property, um den korrekten Typ-Zweig in der Union zu wählen. Innerhalb eines if-Blocks, der auf das Discriminant prüft, kennt TypeScript den exakten Typ — inklusive aller Props, die zu diesem Zweig gehören. Das macht as-Casts überflüssig und eliminiert die Klasse von Bugs, die dadurch entsteht, dass man vergisst zu prüfen, ob eine optionale Prop definiert ist, bevor man sie nutzt.
6. Conditional Types für dynamische Props
Conditional Types — T extends U ? X : Y — ermöglichen Props-Typen, die sich in Abhängigkeit von anderen Props ändern. Das klassische Anwendungsbeispiel: Eine Input-Komponente hat bei type="number" zusätzliche Props min, max und step, die bei type="text" nicht existieren und auch nicht übergeben werden können. Mit Conditional Types ist diese Abhängigkeit zur Compile-Zeit erzwungen: Der TypeScript-Compiler kennt den erlaubten Props-Satz basierend auf dem type-Prop-Wert.
Infer-Keyword in Conditional Types ermöglicht, Typen aus anderen Typen zu extrahieren. type UnwrapPromise<T> = T extends Promise<infer U> ? U : T extrahiert den aufgelösten Typ eines Promise — nützlich für Custom Hooks, die Async-Functions wrappen und typsicher zurückgeben. In React-Projekten nutzt man solche Utility-Types regelmäßig für Hook-Return-Types, API-Response-Transformationen und Props-Ableitungen. TypeScript's eingebaute Utility-Types wie ReturnType<T>, Parameters<T> und Awaited<T> sind alle auf Conditional Types aufgebaut.
7. Mapped Types für Props-Transformationen
Mapped Types transformieren jeden Key eines existierenden Typs in einen neuen Typ. Das ermöglicht es, Typen aus bestehenden abzuleiten, ohne sie manuell zu duplizieren. In React-Projekten sind Mapped Types besonders nützlich, um Props-Sets zu erzeugen: type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>> macht bestimmte Props optional, ohne alle optional zu machen. type RequiredBy<T, K extends keyof T> ist das Gegenteil. Diese Utility-Types sind das Werkzeug, um Props-Interfaces präzise zu formulieren, ohne Copy-Paste zwischen ähnlichen Interfaces.
Für Event-Handler-Props nutzt man Mapped Types, um aus einem Datenschema automatisch alle Handler abzuleiten: type EventHandlers<T> = { [K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void }. Das kombiniert Mapped Types mit Template Literal Types und erzeugt Event-Handler-Props, die automatisch aus dem Datenschema generiert werden. Wenn das Schema ein neues Feld bekommt, erscheint der zugehörige Handler automatisch im Typ — ohne manuelle Interface-Anpassung.
// Discriminated union for mutually exclusive props
type LinkButtonProps = {
as: 'link'; // discriminant property
href: string;
target?: '_blank' | '_self';
onClick?: never; // explicitly excluded
};
type ActionButtonProps = {
as?: 'button'; // discriminant — default value
onClick: () => void;
href?: never; // explicitly excluded
target?: never;
};
// Union — TypeScript picks the right branch based on 'as' prop
type ButtonProps = (LinkButtonProps | ActionButtonProps) & {
children: React.ReactNode;
disabled?: boolean;
className?: string;
};
const Button: React.FC<ButtonProps> = (props) => {
// Discriminant narrows the type in each branch
if (props.as === 'link') {
// TypeScript knows: href is string, onClick is never
return (
<a href={props.href} target={props.target} className={props.className}>
{props.children}
</a>
);
}
// TypeScript knows: onClick is () => void, href is never
return (
<button onClick={props.onClick} disabled={props.disabled} className={props.className}>
{props.children}
</button>
);
};
// Correct usage — TypeScript enforces each variant's required props
const NavBar = () => (
<>
<Button as="link" href="/about">Über uns</Button> {/* ✓ */}
<Button onClick={() => alert('click')}>Klick mich</Button> {/* ✓ */}
<Button as="link" onClick={() => {}}>Falsch</Button> {/* ✗ TS error */}
<Button as="button" href="/foo">Falsch</Button> {/* ✗ TS error */}
</>
);
8. Custom Hooks mit Generics
Custom Hooks mit Generics sind das mächtigste Wiederverwendungs-Pattern in TypeScript-React-Projekten. Ein generischer useFetch<T>-Hook fetchet Daten von einer URL und gibt sie mit dem korrekten Typ zurück — der Nutzer des Hooks deklariert einmal useFetch<Product[]>('/api/products'), und TypeScript weiß für den gesamten restlichen Code, dass data ein Product[] ist. Das eliminiert Casts, sichert API-Konsistenz und macht Re-Renders vorhersehbar, weil der Typ nie mit dem tatsächlichen API-Response-Format divergieren kann, ohne einen Compile-Fehler zu erzeugen.
Für komplexere Hooks nutzt man Generic-Constraints, um sicherzustellen, dass bestimmte Operationen erlaubt sind. Ein useLocalStorage<T extends JsonSerializable>-Hook, der nur Typen akzeptiert, die in JSON serialisiert werden können, verhindert zur Compile-Zeit, dass Maps, Sets oder Klassen-Instanzen versehentlich in localStorage gespeichert werden. Die Generic-Constraint T extends JsonSerializable ist dabei eine strukturelle Prüfung — jeder Typ, der die Anforderungen erfüllt, wird akzeptiert, ohne explizit als JsonSerializable annotiert zu sein.
9. TypeScript-Patterns im Vergleich
Verschiedene TypeScript-Features lösen ähnliche Probleme auf unterschiedlichen Abstraktionsebenen. Die Wahl des richtigen Patterns beeinflusst Lesbarkeit, Wartbarkeit und die Qualität der IDE-Unterstützung.
| Problem | Naiver Ansatz | Fortgeschrittenes Pattern | Vorteil |
|---|---|---|---|
| Sich ausschließende Props | Alle optional + Runtime-Check | Discriminated Union | Compile-Zeit-Fehler, kein Runtime-Check |
| Generische Listen-Komponente | items: any[] |
T extends object Generic |
Vollständige Typsicherheit ohne Casts |
| Konfiguration validieren | as ConfigType cast |
satisfies ConfigType |
Validiert + behält Literal-Types |
| Props aus Datentyp ableiten | Manuell dupliziertes Interface | Mapped Type + Template Literal | Automatisch synchron mit Datentyp |
| Bedingte Props | Alle optional, viele ? |
Conditional Types | Props-Set ändert sich mit einer Prop |
Das wichtigste Prinzip beim Einsatz fortgeschrittener TypeScript-Features: Komplexität ist nur dann gerechtfertigt, wenn sie Korrektheit strukturell erzwingt oder Boilerplate strukturell vermeidet. Ein Mapped Type, der das manuell Duplizieren eines Interfaces verhindert, zahlt sich aus. Ein Conditional Type, der sicherstellt, dass bestimmte Props nur in bestimmten Kombinationen erlaubt sind, zahlt sich aus. Fortgeschrittene Features nur wegen ihrer Eleganz einzusetzen, erhöht die kognitive Last für das gesamte Team — der Nutzen muss die Kosten rechtfertigen.
Mironsoft
Typsichere React-Architekturen mit TypeScript
TypeScript-Qualität in eurer React-Codebase verbessern?
Wir analysieren eure TypeScript-Konfiguration und Props-APIs, ersetzen any-Typen und unsichere Casts durch strukturelle Garantien — und richten strict-Modus, discriminated unions und Generics ein, wo sie wirklich helfen.
TypeScript-Audit
any-Typen, unsichere Casts und fehlende Strict-Mode-Optionen identifizieren
Props-API-Design
Discriminated unions, Generics und satisfies für typsichere Komponenten-APIs
Schulung & Review
TypeScript-Workshop für das Team und Code-Review für neue Komponenten-APIs
10. Zusammenfassung
Fortgeschrittene TypeScript-Features in React verschieben die Fehlererkennung von der Laufzeit zur Compile-Zeit. Der satisfies-Operator validiert Konfigurationsobjekte, ohne Literal-Types zu verbreitern — Autocomplete bleibt präzise. Template Literal Types bauen String-Unions strukturell auf, statt sie manuell aufzuzählen. Generische Komponenten und Hooks sind wiederverwendbar ohne Typsicherheit zu opfern — der Caller-Kontext bestimmt den konkreten Typ. Discriminated Unions machen sich ausschließende Props zu Compile-Zeit-Garantien. Conditional Types und Mapped Types leiten Props-Typen aus bestehenden Datentypen ab, ohne manuelles Duplizieren.
Der Schlüssel ist, diese Features gezielt einzusetzen: Discriminated Unions, wenn Props sich gegenseitig ausschließen sollen. Generics, wenn eine Komponente mit verschiedenen, aber typkonsistenten Datenstrukturen arbeiten muss. satisfies, wenn ein Objekt typisiert aber dennoch mit Literal-Types zugänglich sein soll. Wer diese Patterns kennt und an der richtigen Stelle einsetzt, baut React-Komponenten, die von Kollegen ohne Dokumentation korrekt verwendet werden — weil TypeScript falsche Verwendung schlicht verweigert.
React + TypeScript Patterns — Das Wichtigste auf einen Blick
satisfies statt as
satisfies validiert gegen einen Typ, behält aber Literal-Types. as-Casts überschreiben den Typ ohne Prüfung — immer satisfies bevorzugen.
Discriminant-Property
Ein gemeinsames Literal-Type-Feld als Discriminant macht TypeScript-Narrowing in Branches präzise und eliminiert Runtime-Checks für sich ausschließende Props.
Generic Constraints
T extends { id: string } garantiert strukturell, was Komponenten brauchen — kein any, kein Cast, vollständige Wiederverwendbarkeit.
Mapped + Template Literals
Event-Handler-Typen aus Datentypen ableiten mit [K in keyof T as `on${Capitalize}`] — automatisch synchron, kein manuelles Duplizieren.