React mit TypeScript bedeutet, Komponenten in .tsx-Dateien zu schreiben, in denen Props, State, Refs und Event-Handler Typen haben, sodass der Compiler eine fehlende Prop oder einen falschen Event-Handler findet, bevor der Code läuft. Die Typen verschwinden beim Build: Im Browser läuft dasselbe JavaScript, das du ohne sie geschrieben hättest. Die Live-Editoren auf dieser Seite führen dieses JavaScript aus, und die typisierte Version steht jeweils daneben in einem statischen Block.
Hier ist derselbe Badge mit Typen. count ist optional (?), also fällt der zweite Badge auf den Standardwert 0 zurück, und children nimmt sowohl ein Element als auch einen einfachen String an.
type BadgeProps = {
label: string;
count?: number;
children: React.ReactNode;
};
function Badge({ label, count = 0, children }: BadgeProps) {
return (
<div>
<strong>{label}</strong> ({count})
<div>{children}</div>
</div>
);
}
<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'
Ein TypeScript-Projekt erstellen
Vite hat ein Template für React plus TypeScript:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
Du bekommst .tsx-Dateien, eine tsconfig.json mit eingeschaltetem strict und die bereits installierten Typdefinitionen für React (@types/react, @types/react-dom). Vite entfernt beim Ausliefern und Bauen die Typen, prüft sie aber nicht; das build-Script des Templates führt zuerst tsc aus, und dein Editor prüft beim Tippen. Frameworks wie Next.js richten TypeScript genauso ein, wenn du mit ihrer CLI ein Projekt erstellst.
Props typisieren
Beschreibe die Props als Objekttyp und annotiere den destrukturierten Parameter. Ein paar Muster decken die meisten Komponenten ab:
interface UserCardProps {
name: string; // required
age?: number; // optional, may be undefined
role: 'admin' | 'member'; // a union of allowed strings
tags: string[];
onSelect: (id: string) => void; // a callback prop
children?: React.ReactNode; // anything React can render
}
function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
// ...
}
type oder interface. Beides funktioniert für Props. interface lässt sich mit extends erweitern und durch doppelte Deklaration zusammenführen; type kann außerdem Unions, Intersections und Mapped Types ausdrücken. Die meisten Teams wählen eins und nutzen es überall.
children. Nutze React.ReactNode für alles, was zwischen die Tags kommt: Elemente, Strings, Zahlen, Arrays, null. Die Seite zu Props erklärt, wie children überhaupt ankommt.
Warum React.FC optional ist. const Badge: React.FC<BadgeProps> = (...) => ... typisiert die ganze Funktion statt des Parameters. Seit den Typen für React 18 fügt es children nicht mehr für dich hinzu, kann für eine generische Komponente keinen Typparameter annehmen und bringt nichts, was ein typisierter Parameter nicht auch bringt. Älterer Code nutzt es häufig; einfache Funktionen mit typisierten Props sind die übliche Wahl für neuen Code.
useState typisieren
Für einen einfachen Anfangswert reicht die Inferenz: useState(0) ist eine number, useState('') ein string. Übergib ein Typargument, wenn der State mehr enthalten kann, als sein Anfangswert vermuten lässt: einen Wert, der als null beginnt, oder einen von mehreren festen Strings.
Klicke auf „Load user“, und der Status wechselt von idle zu loading zu done. Der typisierte State:
type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');
user.name; // Error: 'user' is possibly 'null'
user?.name; // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'
Ohne <User | null> leitet useState(null) den Typ null ab, und setUser(data) kompiliert nicht. Die Union zwingt dich, den leeren Fall zu behandeln, also genau die Prüfung user ? ... : ... im laufenden Beispiel.
useRef für DOM-Elemente
Eine Ref, die auf ein DOM-Element zeigt, nimmt den Elementtyp an und beginnt als null, weil das Element erst existiert, wenn React es nach dem ersten Render anhängt.
Tippe etwas und klicke dann auf „Log the value“: Die Konsole gibt aus, was du getippt hast, direkt aus dem DOM-Element gelesen. Die typisierte Version:
const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null
inputRef.current?.focus(); // OK
inputRef.current.focus(); // Error: 'inputRef.current' is possibly 'null'
const timerId = useRef<number | null>(null); // a mutable value, not a DOM node
Nutze das Element-Interface, das zum Tag passt: HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. In den Typen für React 19 nimmt useRef immer ein Argument und gibt ein RefObject zurück, dessen current du zuweisen kannst, also funktioniert dieselbe Form für DOM-Refs und für Werte wie eine Timer-ID. Siehe useRef für die Seite zur Laufzeit.
Typen für Event-Handler
Inline-Handler brauchen keine Annotation: In onChange={(e) => setName(e.target.value)} kennt TypeScript e aus der Prop, an die er übergeben wird. Annotiere, wenn du einen Handler in eine eigene Funktion verschiebst.
Sende ab, und die Konsole loggt die Adresse; preventDefault verhindert, dass die Seite neu lädt. Mit Typen:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}
function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') { /* ... */ }
}
Seit @types/react 19.2.10 ist onSubmit mit React.SubmitEvent typisiert, und das ältere React.FormEvent ist als veraltet markiert; mit einer älteren Version der Typen schreibst du stattdessen React.FormEvent<HTMLFormElement>. Der Typparameter ist das Element, an dem der Handler hängt. Wenn du dir beim Typ unsicher bist, fahr in deinem Editor mit der Maus über die Prop onChange oder typisiere den ganzen Handler auf einmal: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.
Context typisieren
Gib createContext den Typ des Werts. Wenn es einen sinnvollen Standardwert gibt, übergib ihn, dann bekommt jeder Konsument einen Wert, der nicht null ist:
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
const theme = useContext(ThemeContext); // Theme
Wenn es keinen guten Standardwert gibt (ein angemeldeter Nutzer, ein Store), beginne mit null und pack useContext in einen Hook, der einen Fehler wirft, wenn eine Komponente außerhalb des Providers genutzt wird. Jeder Aufrufer bekommt dann den Typ ohne null und eine klare Fehlermeldung statt eines Absturzes tief im Code.
type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);
export function useAuth(): Auth {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
return value;
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState('Ada');
return (
<AuthContext value={{ user, logout: () => setUser('') }}>
{children}
</AuthContext>
);
}
<AuthContext value={...}> ist die Provider-Syntax von React 19; <AuthContext.Provider value={...}> wird genauso typisiert.
Generische Komponenten
Eine Liste, Tabelle oder Auswahl, die mit jedem Typ von Eintrag funktioniert, ist eine generische Komponente: Ein Typparameter verbindet die Prop items mit dem Callback renderItem.
Die typisierte List:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
getKey: (item: T) => string | number;
};
function List<T>({ items, renderItem, getKey }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error
TypeScript leitet T aus items ab, also schreiben Aufrufer den Typparameter nie selbst. In einer .tsx-Datei braucht eine Arrow Function <T,> statt <T>, weil <T> allein als JSX-Tag gelesen wird.
Props nativer Elemente erweitern
Ein Wrapper um ein button oder input sollte jedes Attribut annehmen, das das native Element annimmt. React.ComponentProps<'button'> ist genau diese Menge; füge deine eigenen Props hinzu und spreade den Rest.
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'ghost';
};
function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
return (
<button
{...rest}
style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
/>
);
}
<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
Save
</Button>;
onClick, disabled, type und aria-* sind alle typisiert, ohne dass du sie auflistest. In React 19 ist ref eine normale Prop, und ComponentProps<'button'> enthält sie, also erreicht <Button ref={buttonRef}> den DOM-Knoten über den Spread, ganz ohne forwardRef. Nutze ComponentPropsWithoutRef<'button'>, wenn du ref weglassen willst, und ComponentProps<typeof UserCard>, um die Props einer deiner eigenen Komponenten zu lesen.
Häufige Typfehler
„'ref.current' is possibly 'null'“. Das Element existiert beim ersten Render noch nicht. Nutze ref.current?.focus() oder prüfe if (ref.current), bevor du es verwendest.
„Type 'string' is not assignable“ bei einem State mit Union. const [status, setStatus] = useState('idle') leitet string ab, was breiter ist als gewünscht, und die Übergabe an eine Prop vom Typ Status schlägt fehl. Schreib useState<Status>('idle').
Event mit dem falschen Element typisiert. React.ChangeEvent<HTMLInputElement> an einem select gibt e.target den falschen Typ. Der Typparameter muss zum Element passen, an dem der Handler hängt.
any nutzen, um einen Fehler zum Schweigen zu bringen. Das schaltet die Prüfung für alles ab, was dieser Wert berührt. Nimm lieber unknown und grenze es ein, oder behebe den Typ an seiner Quelle.
Häufig gestellte Fragen
Wie erstelle ich eine React-App mit TypeScript?
Führe npm create vite@latest my-app -- --template react-ts aus, dann npm install und npm run dev. Komponenten kommen in .tsx-Dateien, und Vite entfernt die Typen beim Bauen; führe tsc aus (das build-Script des Templates tut das), um sie zu prüfen.
Welchen Typ hat children in React?
React.ReactNode. Er nimmt alles an, was React rendern kann: Elemente, Strings, Zahlen, Arrays, null und undefined. Nutze React.ReactElement nur, wenn du genau ein Element brauchst.
Wie typisiere ich useState mit null?
Übergib den Typ explizit als Union: useState<User | null>(null). Ohne Typargument leitet TypeScript null als einzigen erlaubten Wert ab.
Welchen Typ hat ein onChange-Event in React?
React.ChangeEvent<HTMLInputElement> für ein Input (HTMLTextAreaElement oder HTMLSelectElement für diese Elemente). e.target.value ist dann als string typisiert.
Sollte ich React.FC nutzen?
Es ist optional. Den Props-Parameter direkt zu typisieren (function Card({ title }: CardProps)) erledigt dieselbe Aufgabe, liest sich einfacher und funktioniert mit Generics. React.FC fügt children nicht mehr implizit hinzu, bringt also wenig zusätzlich.
Sollte ich type oder interface für Props nutzen?
Beides funktioniert. interface lässt sich erweitern und zusammenführen; type kann Unions und Mapped Types ausdrücken. Wähle eine Konvention für die Codebasis und bleib dabei.