Die React-TypeScript-Best-Practices, die in echten Projekten den Unterschied machen, sind wenige: strict in der tsconfig, Laufzeitprüfung überall dort, wo Daten von außen kommen, Zustände als Union-Typen statt als lose Booleans und generierte Datenbanktypen statt handgeschriebener. Formatierung, Benennung und Komponentengröße sind nützlich, verhindern aber selten einen Fehler beim Kunden. Wer Zeit für Codequalität hat, setzt sie in dieser Reihenfolge ein.
Grundlage dieses Beitrags sind sieben meiner eigenen Next.js-Projekte mit React 19, vom CRM für Selbstständige bis zur Tracking-App. Ich habe deren Konfiguration und Code für diesen Text ausgewertet.
Was die sieben Projekte zeigen
| Einstellung / Befund | Projekte |
|---|---|
strict: true in der tsconfig | 7 von 7 |
zusätzlich noUncheckedIndexedAccess | 2 von 7 |
| Zod für Laufzeitprüfung im Einsatz | 7 von 7 |
explizite any-Annotationen: 0 bis 3 | 6 von 7 |
explizite any-Annotationen: 231 | 1 von 7 |
Die Ausreißerzeile ist der lehrreichste Befund. Die 231 any stehen nicht verteilt im Code, sondern gebündelt dort, wo Ergebnisse von Supabase-Abfragen in Admin- und Auswertungsseiten weiterverarbeitet werden. Das ist das typische Muster: any entsteht selten aus Faulheit, sondern an der Stelle, wo ein Abfrageergebnis nicht zum erwarteten Typ passt und jemand schnell weiter will. Die Lösung ist deshalb keine Lint-Regel, sondern ein sauberer Typ für die Abfrage.
Fünf Regeln, sortiert nach Wirkung
1. Strenge Compiler-Optionen von Tag eins
strict: true schaltet alle strikten Typprüfungen gleichzeitig ein, unter anderem strictNullChecks und noImplicitAny. Die Sammlung typescript-best-practices auf GitHub empfiehlt, die strengere Konfiguration standardmäßig zu aktivieren und in neuen Projekten strict: true zu setzen.
Ich gehe einen Schritt weiter: noUncheckedIndexedAccess. Damit ist items[0] vom Typ Item | undefined, und der Compiler zwingt dich, den leeren Fall zu behandeln. Genau dieser Fall wirft im Live-Betrieb den Klassiker Cannot read properties of undefined. In einem bestehenden Projekt nachgerüstet, erzeugt die Option schnell dreistellige Fehlerzahlen; in einem neuen kostet sie fast nichts.
2. Laufzeitprüfung an jeder Grenze
TypeScript prüft zur Build-Zeit. Was zur Laufzeit von außen kommt, kennt der Compiler nicht: API-Antworten, Formulardaten, Webhook-Payloads, Umgebungsvariablen, localStorage. Ein as User an dieser Stelle ist eine Behauptung, keine Prüfung.
Die Regel: An jeder Grenze ein Schema (in allen sieben Projekten Zod), daraus den Typ ableiten mit z.infer, und safeParse statt parse, wenn der Fehlerfall eine Antwort an den Nutzer braucht. Innerhalb der App vertraust du dann den Typen.
3. Zustände als Union, nicht als Booleans
Drei Booleans isLoading, isError, hasData erlauben acht Kombinationen, von denen drei sinnvoll sind. Eine Discriminated Union erlaubt nur die sinnvollen:
type LoadState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'success'; data: T };
Im JSX prüfst du status, und TypeScript weiß in jedem Zweig, welche Felder es gibt. Dasselbe Prinzip gilt für Props: Statt variant: string eine Union aus erlaubten Werten. Die GitHub-Sammlung nennt das "Strings should be safe": die möglichen Werte als Typ deklarieren statt string zuzulassen.
4. Datenbanktypen generieren, nicht abschreiben
Wer Supabase oder ein ORM nutzt, lässt die Tabellentypen aus dem Schema erzeugen und leitet Komponenten-Props daraus ab. Handgeschriebene Interfaces für Tabellen veralten bei der nächsten Migration, ohne dass der Compiler es merkt. Die 231 any aus der Tabelle oben sind genau die Folge, wenn das für Abfragen mit Joins oder Aggregaten nicht konsequent passiert.
5. Server- und Client-Grenze in Next.js typisieren
Im App Router laufen Server Components auf dem Server, Client Components im Browser. Props, die vom Server an eine Client Component gehen, müssen serialisierbar sein. Ich definiere für diese Übergabe eigene, schlanke Typen statt ganze Datenbankzeilen durchzureichen. Das hält Client-Bundles klein und verhindert, dass Felder wie interne IDs oder Notizen ungewollt im Browser landen.
Was überschätzt wird
Viele Best-Practice-Listen beginnen mit Formatierung und Werkzeugen. Ein Beispiel auf DEV Community empfiehlt ESLint mit Prettier, einen Pre-commit-Hook über Husky und lint-staged, explizite Rückgabetypen für jede Funktion und Komponenten unter 200 Zeilen. Das ist alles vernünftig, aber unterschiedlich viel wert:
- Prettier und Pre-commit-Hook: einmal einrichten, danach ist Formatierung kein Thema mehr im Review. Lohnt sich ab dem zweiten Entwickler im Projekt.
- Explizite Rückgabetypen überall: Bei exportierten Funktionen und Server Actions sinnvoll, weil sie eine Schnittstelle festlegen. Bei Komponenten und internen Helfern bringt die Inferenz dasselbe mit weniger Rauschen.
- 200-Zeilen-Grenze: taugt als Warnsignal, nicht als Regel. Eine Formularkomponente mit 250 Zeilen und einer klaren Aufgabe ist besser als vier Teile, die nur gemeinsam Sinn ergeben.
Die Faustregel für Teams mit begrenzter Zeit: Erst die fünf Regeln oben, dann Werkzeuge. Ein perfekt formatiertes Projekt ohne Laufzeitprüfung stürzt trotzdem beim ersten unerwarteten API-Feld ab.
Woran du schlechte Typisierung früh erkennst
as-Casts häufen sich an derselben Stelle. Dort fehlt ein Schema oder ein generierter Typ.- Optionale Props mit Fragezeichen nehmen zu, und Komponenten prüfen intern dutzende Kombinationen. Dort fehlt eine Union.
- Ein
// @ts-ignoreüberlebt mehrere Releases. Spätestens bei der Übergabe an ein anderes Team wird daraus eine Wissenslücke.
Wer dieselben TypeScript-Kenntnisse später für eine mobile App nutzen will, steht vor der Frage React Native oder nativ; den Vergleich mit Stundenrechnung habe ich unter React Native vs SwiftUI aufgeschrieben.
Häufige Fragen
Gibt es einen offiziellen TypeScript Style Guide?
Einen verbindlichen Standard für React-Projekte gibt es nicht. In der Praxis setzen Teams auf eine geteilte ESLint-Konfiguration mit TypeScript-Plugin und Prettier, ergänzt um wenige eigene Regeln. Wichtiger als der gewählte Guide ist, dass er automatisch geprüft wird statt im Review.
Wie benenne ich Typen in TypeScript?
Typen und Interfaces in PascalCase, ohne Präfix wie I oder T: User, LoadState, InvoiceRow. Props-Typen einer Komponente bekommen den Komponentennamen plus Props, etwa InvoiceTableProps. So findest du sie per Suche sofort.
Wie benenne ich Booleans?
Mit einem Präfix, das die Ja/Nein-Frage ausdrückt: is, has, can oder should, also isOpen, hasAccess, canEdit. Die GitHub-Sammlung typescript-best-practices empfiehlt is und has, weil der Name dann klar als Boolean erkennbar ist. Wenn mehr als zwei Booleans zusammen einen Zustand beschreiben, ist meist eine Union die bessere Wahl.
Was bedeutet idiomatisches TypeScript in React?
Typen so weit wie möglich ableiten statt doppelt schreiben: Props aus Datenbanktypen, Formulartypen aus Zod-Schemas, Union-Typen aus as const-Arrays. Idiomatisch heißt auch, unknown statt any zu verwenden, wenn ein Wert noch nicht geprüft ist.