Tavo-IT Logo
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