</>
{ }
React · TypeScript · satisfies · Template Literals · Generics
React + TypeScript:
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.

18 Min. Lesezeit satisfies · Template Literals · Generics · discriminated unions · Conditional Types React 18 · TypeScript 5.x

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.

11. FAQ: React + TypeScript mit satisfies, Template Literals und Generics

1satisfies vs. as in TypeScript?
as überschreibt ohne Prüfung. satisfies prüft die Konformität, behält Literal-Types und ermöglicht präzises Autocomplete — immer satisfies bevorzugen.
2Was sind Template Literal Types?
String-Literal-Types auf Typ-Ebene kombinieren: `on${Capitalize<string>}` erzeugt Typen für alle Strings mit Großbuchstaben-Prefix — Compile-Zeit-validiert.
3Warum <T,> in TSX-Generics?
In .tsx wird <T> als JSX-Tag interpretiert. Trailing-Komma <T,> oder <T extends object> macht den Generic-Parameter klar.
4Was ist eine Discriminated Union?
Union-Typen mit gemeinsamem Literal-Type-Feld (Discriminant). TypeScript wählt den korrekten Zweig in Branches — kein Runtime-Check nötig.
5Wann Conditional Types in React?
Wenn Props-Typen sich abhängig von anderen Props ändern sollen — Input type='number' andere Props als type='text'. Compile-Zeit-erzwungen.
6Mapped Types wozu in React?
Event-Handler-Props aus Datentypen ableiten, bestimmte Props optional/required machen — ohne manuelle Interface-Duplizierung, automatisch synchron.
7Warum strict: true in tsconfig?
Aktiviert strictNullChecks, noImplicitAny und weitere Checks — viele Laufzeit-Fehler werden zu Compile-Fehlern, besonders bei Props und Event-Handlern.
8Generics in Custom Hooks?
useFetch<Product[]>('/api/products') gibt { data: Product[] | undefined } zurück — Typ vom Caller bestimmt, vollständig inferiert, kein Cast nötig.
9Partial<T> vs. PartialBy<T, K>?
Partial<T> macht alle Keys optional. PartialBy<T, K> = Omit<T, K> & Partial<Pick<T, K>> macht nur ausgewählte Keys optional — präziser und sicherer.
10Komplexe TypeScript-Fehler debuggen?
Intermediate-Typen auslagern und hovern. TypeScript Playground für isolierte Tests. VS Code "Go to Type Definition" für inferierte Typen nachverfolgen.