📋 Inhaltsverzeichnis
Webentwicklung18 min Lesezeit2025-06-12
TypeScript fürReact Entwickler
Typsichere React-Entwicklung mit TypeScript und modernen Patterns – Comprehensive Guide für professionelle React-TypeScript-Entwicklung.
TypeScriptReactType SafetyInterfacesGenerics
🎯 TypeScript & React
TypeScript bietet statische Typisierung für JavaScript und macht React-Entwicklung sicherer und produktiver. Mit TypeScript können Sie Fehler zur Entwicklungszeit erkennen, bessere IntelliSense nutzen und wartbareren Code schreiben.
- Compile-time Fehlerprüfung: Fehler werden vor der Ausführung erkannt
- Bessere IntelliSense: Autocompletions und Dokumentation
- Refactoring-Sicherheit: Sichere Code-Umstrukturierung
- Selbstdokumentierender Code: Typen als Dokumentation
⚙️ Project Setup
Neues React-TypeScript Projekt:
# Create React App mit TypeScript
npx create-react-app my-app --template typescript
# Vite mit React-TypeScript
npm create vite@latest my-app -- --template react-ts
# Next.js mit TypeScript
npx create-next-app@latest my-app --typescript
Existierendes Projekt zu TypeScript migrieren:
# TypeScript Dependencies installieren
npm install --save-dev typescript @types/react @types/react-dom @types/node
# tsconfig.json erstellen
npx tsc --init
# Empfohlene tsconfig.json für React:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@/components/*": ["components/*"],
"@/hooks/*": ["hooks/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
}
🧩 Component Typisierung
Funktionale Komponenten:
// Basis funktionale Komponente
import React from 'react';
interface WelcomeProps {
name: string;
age?: number; // Optional prop
isLoggedIn: boolean;
}
// Methode 1: Explizite Typisierung
const Welcome: React.FC<WelcomeProps> = ({ name, age, isLoggedIn }) => {
return (
<div>
<h1>Welcome, {name}!</h1>
{age && <p>Age: {age}</p>}
{isLoggedIn ? <p>You are logged in</p> : <p>Please log in</p>}
</div>
);
};
// Methode 2: Implizite Typisierung (empfohlen)
function WelcomeImplicit({ name, age, isLoggedIn }: WelcomeProps) {
return (
<div>
<h1>Welcome, {name}!</h1>
{age && <p>Age: {age}</p>}
{isLoggedIn ? <p>You are logged in</p> : <p>Please log in</p>}
</div>
);
}
// Mit children
interface ContainerProps {
children: React.ReactNode;
className?: string;
}
function Container({ children, className = '' }: ContainerProps) {
return <div className={className}>{children}</div>;
}
// Generische Komponente
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
🔗 Props & Interfaces
Erweiterte Props Patterns:
// Union Types für Props
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
onClick: () => void;
children: React.ReactNode;
disabled?: boolean;
}
function Button({ variant, size, onClick, children, disabled = false }: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded font-medium transition-colors';
const variantClasses = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
danger: 'bg-red-500 hover:bg-red-600 text-white'
};
const sizeClasses = {
small: 'text-sm px-2 py-1',
medium: 'text-base px-4 py-2',
large: 'text-lg px-6 py-3'
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
// Extending HTML Elements
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
function Input({ label, error, className = '', ...props }: InputProps) {
return (
<div className="mb-4">
{label && <label className="block text-sm font-medium mb-1">{label}</label>}
<input
className={`border rounded px-3 py-2 w-full ${className} ${
error ? 'border-red-500' : 'border-gray-300'
}`}
{...props}
/>
{error && <p className="text-red-500 text-sm mt-1">{error}</p>}
</div>
);
}
// Discriminated Unions
interface LoadingState {
status: 'loading';
}
interface SuccessState {
status: 'success';
data: any;
}
interface ErrorState {
status: 'error';
error: string;
}
type AsyncState = LoadingState | SuccessState | ErrorState;
interface DataDisplayProps {
state: AsyncState;
}
function DataDisplay({ state }: DataDisplayProps) {
switch (state.status) {
case 'loading':
return <div>Loading...</div>;
case 'success':
return <div>Data: {JSON.stringify(state.data)}</div>;
case 'error':
return <div>Error: {state.error}</div>;
default:
// TypeScript erkennt, dass alle Fälle abgedeckt sind
const exhaustiveCheck: never = state;
return exhaustiveCheck;
}
}
🪝 Hooks mit TypeScript
useState Typisierung:
import { useState } from 'react';
// Einfache Typisierung (automatisch inferiert)
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
// Explizite Typisierung
const [user, setUser] = useState<User | null>(null);
// Komplexere States
interface FormData {
email: string;
password: string;
rememberMe: boolean;
}
function LoginForm() {
const [formData, setFormData] = useState<FormData>({
email: '',
password: '',
rememberMe: false
});
const handleInputChange = (field: keyof FormData) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = field === 'rememberMe' ? e.target.checked : e.target.value;
setFormData(prev => ({
...prev,
[field]: value
}));
};
return (
<form>
<input
type="email"
value={formData.email}
onChange={handleInputChange('email')}
/>
<input
type="password"
value={formData.password}
onChange={handleInputChange('password')}
/>
<input
type="checkbox"
checked={formData.rememberMe}
onChange={handleInputChange('rememberMe')}
/>
</form>
);
}
useEffect und useRef:
import { useEffect, useRef, useState } from 'react';
function TimerComponent() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<p>Seconds: {seconds}</p>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
// Custom Hook Typisierung
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
}
🎪 Event Handling
Event Handler Typisierung:
import React from 'react';
interface FormComponentProps {
onSubmit: (data: { name: string; email: string }) => void;
}
function FormComponent({ onSubmit }: FormComponentProps) {
// Form Submit Handler
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit({
name: formData.get('name') as string,
email: formData.get('email') as string
});
};
// Input Change Handler
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log('Input changed:', e.target.value);
};
// Button Click Handler
const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked:', e.currentTarget.textContent);
};
// Keyboard Handler
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
};
// Focus Handler
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
console.log('Input focused:', e.target.name);
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
type="text"
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
placeholder="Name"
/>
<input
name="email"
type="email"
onChange={handleInputChange}
placeholder="Email"
/>
<button type="submit" onClick={handleButtonClick}>
Submit
</button>
</form>
);
}
// Generische Event Handler
interface ClickableProps<T extends HTMLElement> {
onClick: (e: React.MouseEvent<T>) => void;
children: React.ReactNode;
}
function ClickableDiv({ onClick, children }: ClickableProps<HTMLDivElement>) {
return <div onClick={onClick}>{children}</div>;
}
function ClickableButton({ onClick, children }: ClickableProps<HTMLButtonElement>) {
return <button onClick={onClick}>{children}</button>;
}
🌐 Context API Typisierung
Typisierter Context:
// context/AuthContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isLoading: boolean;
}
// Context mit undefined als initial value
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
// Simulate API call
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
throw new Error('Login failed');
}
} catch (error) {
console.error('Login error:', error);
throw error;
} finally {
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
};
const value: AuthContextType = {
user,
login,
logout,
isLoading
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Custom Hook mit Fehlerbehandlung
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// HOC für geschützte Routen
interface WithAuthProps {
requiredRole?: 'admin' | 'user';
}
export function withAuth<P extends object>(
Component: React.ComponentType<P>,
{ requiredRole }: WithAuthProps = {}
) {
return function AuthenticatedComponent(props: P) {
const { user } = useAuth();
if (!user) {
return <div>Please log in to access this page.</div>;
}
if (requiredRole && user.role !== requiredRole) {
return <div>You don't have permission to access this page.</div>;
}
return <Component {...props} />;
};
}
🚀 Advanced Patterns
Conditional Rendering mit Types:
// Conditional Props mit never
interface BaseProps {
title: string;
}
interface PropsWithButton extends BaseProps {
showButton: true;
onButtonClick: () => void;
buttonText: string;
}
interface PropsWithoutButton extends BaseProps {
showButton: false;
onButtonClick?: never;
buttonText?: never;
}
type ConditionalProps = PropsWithButton | PropsWithoutButton;
function ConditionalComponent(props: ConditionalProps) {
const { title, showButton } = props;
return (
<div>
<h1>{title}</h1>
{showButton && (
<button onClick={props.onButtonClick}>
{props.buttonText}
</button>
)}
</div>
);
}
// Polymorphic Components
interface PolymorphicProps<T extends React.ElementType> {
as?: T;
children: React.ReactNode;
}
type Props<T extends React.ElementType> = PolymorphicProps<T> &
Omit<React.ComponentPropsWithoutRef<T>, keyof PolymorphicProps<T>>;
function PolymorphicComponent<T extends React.ElementType = 'div'>({
as,
children,
...props
}: Props<T>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
// Verwendung:
function App() {
return (
<>
<PolymorphicComponent>Default div</PolymorphicComponent>
<PolymorphicComponent as="button" onClick={() => console.log('clicked')}>
Button
</PolymorphicComponent>
<PolymorphicComponent as="a" href="https://example.com">
Link
</PolymorphicComponent>
</>
);
}
// Render Props mit Generics
interface RenderPropsComponent<T> {
data: T[];
render: (item: T, index: number) => React.ReactNode;
}
function DataRenderer<T>({ data, render }: RenderPropsComponent<T>) {
return (
<div>
{data.map((item, index) => (
<div key={index}>{render(item, index)}</div>
))}
</div>
);
}
// Verwendung:
interface Product {
id: number;
name: string;
price: number;
}
function ProductList() {
const products: Product[] = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 29 }
];
return (
<DataRenderer
data={products}
render={(product, index) => (
<div>
{index + 1}. {product.name} - ${product.price}
</div>
)}
/>
);
}
🧪 Testing mit TypeScript
Jest & React Testing Library:
// __tests__/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '../components/Button';
describe('Button Component', () => {
const mockOnClick = jest.fn();
beforeEach(() => {
mockOnClick.mockClear();
});
it('renders with correct text', () => {
render(
<Button variant="primary" size="medium" onClick={mockOnClick}>
Click me
</Button>
);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
render(
<Button variant="primary" size="medium" onClick={mockOnClick}>
Click me
</Button>
);
fireEvent.click(screen.getByText('Click me'));
expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(
<Button variant="primary" size="medium" onClick={mockOnClick} disabled>
Click me
</Button>
);
const button = screen.getByText('Click me');
expect(button).toBeDisabled();
});
});
// Custom Render mit Provider
import { render as rtlRender } from '@testing-library/react';
import { AuthProvider } from '../context/AuthContext';
interface RenderOptions {
initialUser?: User | null;
}
function render(ui: React.ReactElement, options: RenderOptions = {}) {
function Wrapper({ children }: { children: React.ReactNode }) {
return <AuthProvider>{children}</AuthProvider>;
}
return rtlRender(ui, { wrapper: Wrapper, ...options });
}
// Type-safe Mocks
const mockUser: User = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
role: 'user'
};
const mockAuthContext: AuthContextType = {
user: mockUser,
login: jest.fn(),
logout: jest.fn(),
isLoading: false
};
Utility Types für bessere Tests:
- Partial<T>: Macht alle Properties optional für Mock-Daten
- Pick<T, K>: Wählt spezifische Properties aus
- Omit<T, K>: Schließt Properties aus
- Required<T>: Macht alle Properties erforderlich
- Record<K, T>: Erstellt Objekt-Typen für Mappings