Tavo-IT Logo
Web Development18 min read2025-06-12

TypeScript forReact Developers

Type-safe React development with TypeScript and modern patterns – Comprehensive guide for professional React-TypeScript development.

TypeScriptReactType SafetyInterfacesGenerics

🎯 TypeScript & React

TypeScript provides static typing for JavaScript and makes React development safer and more productive. With TypeScript, you can catch errors at development time, benefit from better IntelliSense, and write more maintainable code.

  • Compile-time error checking: Errors are caught before execution
  • Better IntelliSense: Autocompletion and documentation
  • Refactoring safety: Safe code restructuring
  • Self-documenting code: Types as documentation

⚙️ Project Setup

New React-TypeScript Project:

# Create React App with TypeScript
npx create-react-app my-app --template typescript

# Vite with React-TypeScript
npm create vite@latest my-app -- --template react-ts

# Next.js with TypeScript
npx create-next-app@latest my-app --typescript

Migrate Existing Project to TypeScript:

# Install TypeScript dependencies
npm install --save-dev typescript @types/react @types/react-dom @types/node

# Create tsconfig.json
npx tsc --init

# Recommended tsconfig.json for 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 Typing

Functional Components:

// Basic functional component
import React from 'react';

interface WelcomeProps {
  name: string;
  age?: number; // Optional prop
  isLoggedIn: boolean;
}

// Method 1: Explicit typing
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>
  );
};

// Method 2: Implicit typing (recommended)
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>
  );
}

// With children
interface ContainerProps {
  children: React.ReactNode;
  className?: string;
}

function Container({ children, className = '' }: ContainerProps) {
  return <div className={className}>{children}</div>;
}

// Generic component
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

Advanced Props Patterns:

// Union types for 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 with TypeScript

useState Typing:

import { useState } from 'react';

// Simple typing (inferred automatically)
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string

// Explicit typing
const [user, setUser] = useState<User | null>(null);

// More complex 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 and 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 typing
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 Typing:

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>
  );
}

// Generic event handlers
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 Typing

Typed 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 with error handling
export function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  
  return context;
}

// HOC for protected routes
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 with Types:

// Conditional props with 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>;
}

// Usage:
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 with 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>
  );
}

// Usage:
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 with 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 with 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 for Better Tests:

  • Partial<T>: Makes all properties optional for mock data
  • Pick<T, K>: Selects specific properties
  • Omit<T, K>: Excludes properties
  • Required<T>: Makes all properties required
  • Record<K, T>: Creates object types for mappings