📝 typescript
6 min read

TypeScript Best Practices for React Developers

Learn essential TypeScript patterns and best practices that will make your React applications more robust, maintainable, and developer-friendly.

Topics covered:

#typescript
#react
#best-practices
#javascript
Lazar Kapsarov - Full Stack Developer

Lazar Kapsarov

✨ Full Stack Developer

Building high-performing SaaS, e-commerce & landing pages with Next.js, React & TypeScript. Helping businesses create digital experiences that convert.

Available for new projects
TypeScript Best Practices for React Developers - Technical illustration

TypeScript Best Practices for React Developers

TypeScript has become an essential tool for React developers, providing type safety, better IDE support, and improved code maintainability. In this guide, we'll explore the best practices that will elevate your TypeScript React development.

Component Props Typing

Interface vs Type Aliases

When defining component props, prefer interfaces for their extensibility and better error messages:

Code
// ✅ Good - using interface for component props
interface ButtonProps {
  children: React.ReactNode
  variant?: 'primary' | 'secondary' | 'destructive' | 'outline'
  size?: 'sm' | 'md' | 'lg'
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
  disabled?: boolean
  loading?: boolean
  icon?: React.ComponentType<{ className?: string }>
  className?: string
}

// ✅ Extending interfaces is clean and readable
interface IconButtonProps extends ButtonProps {
  'aria-label': string // Required for icon-only buttons
  icon: React.ComponentType<{ className?: string }> // Required for icon buttons
}

// ✅ Type aliases for union types and complex compositions
type Status = 'idle' | 'loading' | 'success' | 'error'
type AsyncState<T> = {
  data: T | null
  status: Status
  error: string | null
}

// ✅ Implementation with proper prop handling
const Button: React.FC<ButtonProps> = ({
  children,
  variant = 'primary',
  size = 'md',
  loading = false,
  disabled = false,
  icon: Icon,
  onClick,
  className,
  ...rest
}) => {
  const baseClasses = 'inline-flex items-center justify-center font-medium transition-colors'
  const variantClasses = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
    destructive: 'bg-red-600 text-white hover:bg-red-700',
    outline: 'border border-gray-300 bg-transparent hover:bg-gray-50'
  }
  const sizeClasses = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  }

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className || ''}`}
      disabled={disabled || loading}
      onClick={onClick}
      {...rest}
    >
      {loading ? (
        <LoadingSpinner className="mr-2" />
      ) : Icon ? (
        <Icon className="mr-2 h-4 w-4" />
      ) : null}
      {children}
    </button>
  )
}

Advanced Props Patterns

Code
// ✅ Conditional props with discriminated unions
type AlertProps =
  | {
      variant: 'success'
      title: string
      message: string
      onClose?: () => void
    }
  | {
      variant: 'error'
      title: string
      message: string
      onRetry: () => void
      onClose?: () => void
    }
  | {
      variant: 'loading'
      title: string
      message?: string
      progress?: number
    }

// ✅ Polymorphic components for maximum flexibility
type PolymorphicProps<T extends React.ElementType> = {
  as?: T
  children: React.ReactNode
  className?: string
} & React.ComponentPropsWithoutRef<T>

function Text<T extends React.ElementType = 'p'>({
  as,
  children,
  className,
  ...rest
}: PolymorphicProps<T>) {
  const Component = as || 'p'
  return (
    <Component className={className} {...rest}>
      {children}
    </Component>
  )
}

// Usage examples:
// <Text>Default paragraph</Text>
// <Text as="h1">Heading text</Text>
// <Text as="span" onClick={handleClick}>Clickable span</Text>

Generic Components

Create reusable, type-safe components with generics:

Code
// ✅ Advanced generic select with comprehensive typing
interface SelectOption {
  id: string | number
  label: string
  disabled?: boolean
}

interface SelectProps<T extends SelectOption> {
  options: T[]
  value: T | null
  onChange: (value: T | null) => void
  placeholder?: string
  multiple?: boolean
  searchable?: boolean
  disabled?: boolean
  error?: string
  loading?: boolean
  className?: string
  'data-testid'?: string
}

function Select<T extends SelectOption>({
  options,
  value,
  onChange,
  placeholder = 'Select an option...',
  multiple = false,
  searchable = false,
  disabled = false,
  error,
  loading = false,
  className,
  ...rest
}: SelectProps<T>) {
  const [searchTerm, setSearchTerm] = useState('')
  const [isOpen, setIsOpen] = useState(false)

  const filteredOptions = useMemo(() => {
    if (!searchable || !searchTerm) return options
    return options.filter(option =>
      option.label.toLowerCase().includes(searchTerm.toLowerCase())
    )
  }, [options, searchTerm, searchable])

  const handleSelect = (option: T) => {
    onChange(option)
    if (!multiple) setIsOpen(false)
  }

  return (
    <div className={`relative ${className || ''}`} {...rest}>
      <button
        type="button"
        className={`
          w-full px-3 py-2 border rounded-md text-left
          ${error ? 'border-red-500' : 'border-gray-300'}
          ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
        `}
        onClick={() => !disabled && setIsOpen(!isOpen)}
        disabled={disabled}
      >
        {loading ? (
          <span className="flex items-center">
            <LoadingSpinner size="sm" className="mr-2" />
            Loading...
          </span>
        ) : value ? (
          value.label
        ) : (
          <span className="text-gray-500">{placeholder}</span>
        )}
      </button>

      {isOpen && !disabled && (
        <div className="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg">
          {searchable && (
            <div className="p-2 border-b">
              <input
                type="text"
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
                placeholder="Search..."
                className="w-full px-2 py-1 border rounded text-sm"
                autoFocus
              />
            </div>
          )}
          <div className="max-h-60 overflow-y-auto">
            {filteredOptions.map((option) => (
              <button
                key={option.id}
                type="button"
                className={`
                  w-full px-3 py-2 text-left hover:bg-gray-100 transition-colors
                  ${option.disabled ? 'opacity-50 cursor-not-allowed' : ''}
                  ${value?.id === option.id ? 'bg-blue-50 text-blue-700' : ''}
                `}
                onClick={() => !option.disabled && handleSelect(option)}
                disabled={option.disabled}
              >
                {option.label}
              </button>
            ))}
          </div>
        </div>
      )}

      {error && <p className="mt-1 text-sm text-red-600">{error}</p>}
    </div>
  )
}

// ✅ Usage with specific types
interface User extends SelectOption {
  id: number
  label: string
  email: string
  role: 'admin' | 'user'
}

function UserSelector() {
  const [selectedUser, setSelectedUser] = useState<User | null>(null)
  const users: User[] = [
    { id: 1, label: 'John Doe', email: 'john@example.com', role: 'admin' },
    { id: 2, label: 'Jane Smith', email: 'jane@example.com', role: 'user' },
  ]

  return (
    <Select<User>
      options={users}
      value={selectedUser}
      onChange={setSelectedUser}
      searchable
      placeholder="Select a user..."
      data-testid="user-selector"
    />
  )
}

// ✅ Generic data table component
interface DataTableColumn<T> {
  key: keyof T
  title: string
  render?: (value: T[keyof T], row: T) => React.ReactNode
  sortable?: boolean
  width?: string
}

interface DataTableProps<T extends Record<string, any>> {
  data: T[]
  columns: DataTableColumn<T>[]
  loading?: boolean
  emptyMessage?: string
  onRowClick?: (row: T) => void
  keyExtractor: (row: T) => string | number
}

function DataTable<T extends Record<string, any>>({
  data,
  columns,
  loading = false,
  emptyMessage = 'No data available',
  onRowClick,
  keyExtractor
}: DataTableProps<T>) {
  const [sortConfig, setSortConfig] = useState<{
    key: keyof T
    direction: 'asc' | 'desc'
  } | null>(null)

  const sortedData = useMemo(() => {
    if (!sortConfig) return data

    return [...data].sort((a, b) => {
      const aValue = a[sortConfig.key]
      const bValue = b[sortConfig.key]

      if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1
      if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1
      return 0
    })
  }, [data, sortConfig])

  const handleSort = (key: keyof T) => {
    setSortConfig(current => ({
      key,
      direction: current?.key === key && current.direction === 'asc' ? 'desc' : 'asc'
    }))
  }

  if (loading) {
    return <div className="p-4 text-center">Loading...</div>
  }

  if (data.length === 0) {
    return <div className="p-4 text-center text-gray-500">{emptyMessage}</div>
  }

  return (
    <div className="overflow-x-auto">
      <table className="w-full border-collapse border border-gray-300">
        <thead className="bg-gray-50">
          <tr>
            {columns.map((column) => (
              <th
                key={String(column.key)}
                className={`
                  px-4 py-2 text-left border-b font-medium
                  ${column.sortable ? 'cursor-pointer hover:bg-gray-100' : ''}
                `}
                style={{ width: column.width }}
                onClick={() => column.sortable && handleSort(column.key)}
              >
                <div className="flex items-center gap-2">
                  {column.title}
                  {column.sortable && sortConfig?.key === column.key && (
                    <span>{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
                  )}
                </div>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {sortedData.map((row) => (
            <tr
              key={keyExtractor(row)}
              className={onRowClick ? 'cursor-pointer hover:bg-gray-50' : ''}
              onClick={() => onRowClick?.(row)}
            >
              {columns.map((column) => (
                <td key={String(column.key)} className="px-4 py-2 border-b">
                  {column.render
                    ? column.render(row[column.key], row)
                    : String(row[column.key])
                  }
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

Advanced State Management Patterns

Typed useState with Complex States

Proper typing with useState for different scenarios:

Code
// ✅ Excellent - explicit typing for nullable states
const [user, setUser] = useState<User | null>(null);

// ✅ Good - TypeScript can infer primitive types
const [count, setCount] = useState(0);
const [isVisible, setIsVisible] = useState(false);

// ✅ Advanced - discriminated union for async states
type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };

const [apiState, setApiState] = useState<AsyncState<User[]>>({
  status: "idle",
});

// ✅ Complex form state with validation
interface FormState {
  values: {
    email: string;
    password: string;
    confirmPassword: string;
  };
  errors: Partial<Record<keyof FormState["values"], string>>;
  touched: Partial<Record<keyof FormState["values"], boolean>>;
  isSubmitting: boolean;
}

const [formState, setFormState] = useState<FormState>({
  values: { email: "", password: "", confirmPassword: "" },
  errors: {},
  touched: {},
  isSubmitting: false,
});

// ✅ Helper functions for form state updates
const updateFormValue = <K extends keyof FormState["values"]>(
  field: K,
  value: FormState["values"][K],
) => {
  setFormState((prev) => ({
    ...prev,
    values: { ...prev.values, [field]: value },
    errors: { ...prev.errors, [field]: undefined }, // Clear error on change
    touched: { ...prev.touched, [field]: true },
  }));
};

Advanced useReducer Patterns

Type-safe reducers with sophisticated action patterns:

Code
// ✅ Comprehensive action types with payloads
type UserAction =
  | { type: "FETCH_USER_REQUEST" }
  | { type: "FETCH_USER_SUCCESS"; payload: { user: User; timestamp: number } }
  | { type: "FETCH_USER_FAILURE"; payload: { error: string; code?: number } }
  | {
      type: "UPDATE_USER_FIELD";
      payload: { field: keyof User; value: User[keyof User] };
    }
  | { type: "CLEAR_ERROR" }
  | { type: "RESET_STATE" };

interface UserState {
  user: User | null;
  loading: boolean;
  error: { message: string; code?: number } | null;
  lastUpdated: number | null;
  isDirty: boolean;
}

const initialUserState: UserState = {
  user: null,
  loading: false,
  error: null,
  lastUpdated: null,
  isDirty: false,
};

function userReducer(state: UserState, action: UserAction): UserState {
  switch (action.type) {
    case "FETCH_USER_REQUEST":
      return {
        ...state,
        loading: true,
        error: null,
      };

    case "FETCH_USER_SUCCESS":
      return {
        ...state,
        user: action.payload.user,
        loading: false,
        error: null,
        lastUpdated: action.payload.timestamp,
        isDirty: false,
      };

    case "FETCH_USER_FAILURE":
      return {
        ...state,
        loading: false,
        error: {
          message: action.payload.error,
          code: action.payload.code,
        },
      };

    case "UPDATE_USER_FIELD":
      if (!state.user) return state;
      return {
        ...state,
        user: {
          ...state.user,
          [action.payload.field]: action.payload.value,
        },
        isDirty: true,
        error: null,
      };

    case "CLEAR_ERROR":
      return {
        ...state,
        error: null,
      };

    case "RESET_STATE":
      return initialUserState;

    default:
      // Exhaustive check - TypeScript will error if we miss an action type
      const _exhaustiveCheck: never = action;
      return state;
  }
}

// ✅ Custom hook with typed dispatch and actions
function useUserState(initialUser?: User) {
  const [state, dispatch] = useReducer(userReducer, {
    ...initialUserState,
    user: initialUser || null,
  });

  // Typed action creators
  const actions = useMemo(
    () => ({
      fetchUser: () => dispatch({ type: "FETCH_USER_REQUEST" }),
      setUser: (user: User, timestamp = Date.now()) =>
        dispatch({ type: "FETCH_USER_SUCCESS", payload: { user, timestamp } }),
      setError: (error: string, code?: number) =>
        dispatch({ type: "FETCH_USER_FAILURE", payload: { error, code } }),
      updateUserField: <K extends keyof User>(field: K, value: User[K]) =>
        dispatch({ type: "UPDATE_USER_FIELD", payload: { field, value } }),
      clearError: () => dispatch({ type: "CLEAR_ERROR" }),
      reset: () => dispatch({ type: "RESET_STATE" }),
    }),
    [],
  );

  return { state, actions };
}

Context API with TypeScript

Type-safe context for complex application state:

Code
// ✅ Theme context with comprehensive typing
interface ThemeContextValue {
  theme: 'light' | 'dark' | 'system';
  effectiveTheme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark' | 'system') => void;
  colors: {
    primary: string;
    secondary: string;
    background: string;
    foreground: string;
  };
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

// ✅ Custom hook with proper error handling
export function useTheme(): ThemeContextValue {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// ✅ Provider component with comprehensive logic
interface ThemeProviderProps {
  children: React.ReactNode;
  defaultTheme?: 'light' | 'dark' | 'system';
  storageKey?: string;
}

export function ThemeProvider({
  children,
  defaultTheme = 'system',
  storageKey = 'theme'
}: ThemeProviderProps) {
  const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(defaultTheme);
  const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>('light');

  // System theme detection
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    setSystemTheme(mediaQuery.matches ? 'dark' : 'light');

    const handleChange = (e: MediaQueryListEvent) => {
      setSystemTheme(e.matches ? 'dark' : 'light');
    };

    mediaQuery.addEventListener('change', handleChange);
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, []);

  // Persistence
  useEffect(() => {
    const stored = localStorage.getItem(storageKey);
    if (stored && ['light', 'dark', 'system'].includes(stored)) {
      setTheme(stored as 'light' | 'dark' | 'system');
    }
  }, [storageKey]);

  const effectiveTheme = theme === 'system' ? systemTheme : theme;

  const colors = useMemo(() => ({
    primary: effectiveTheme === 'dark' ? '#3b82f6' : '#1d4ed8',
    secondary: effectiveTheme === 'dark' ? '#64748b' : '#475569',
    background: effectiveTheme === 'dark' ? '#0f172a' : '#ffffff',
    foreground: effectiveTheme === 'dark' ? '#f1f5f9' : '#0f172a',
  }), [effectiveTheme]);

  const contextValue = useMemo<ThemeContextValue>(() => ({
    theme,
    effectiveTheme,
    setTheme: (newTheme) => {
      setTheme(newTheme);
      localStorage.setItem(storageKey, newTheme);
    },
    colors,
  }), [theme, effectiveTheme, colors, storageKey]);

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

Advanced Custom Hooks Typing

Generic Utility Hooks

Create powerful, reusable custom hooks with proper typing:

Code
// ✅ Enhanced localStorage hook with serialization options
interface UseLocalStorageOptions<T> {
  serializer?: {
    read: (value: string) => T;
    write: (value: T) => string;
  };
  initializeWithValue?: boolean;
}

type SetValue<T> = T | ((prevValue: T) => T);

function useLocalStorage<T>(
  key: string,
  initialValue: T,
  options: UseLocalStorageOptions<T> = {},
): [T, (value: SetValue<T>) => void, () => void] {
  const {
    serializer = {
      read: JSON.parse,
      write: JSON.stringify,
    },
    initializeWithValue = true,
  } = options;

  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === "undefined") return initialValue;

    try {
      const item = localStorage.getItem(key);
      if (item === null) {
        if (initializeWithValue) {
          localStorage.setItem(key, serializer.write(initialValue));
        }
        return initialValue;
      }
      return serializer.read(item);
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  const setValue = useCallback(
    (value: SetValue<T>) => {
      if (typeof window === "undefined") {
        console.warn(`Tried to set localStorage key "${key}" on server`);
        return;
      }

      try {
        const newValue = value instanceof Function ? value(storedValue) : value;
        localStorage.setItem(key, serializer.write(newValue));
        setStoredValue(newValue);
      } catch (error) {
        console.error(`Error setting localStorage key "${key}":`, error);
      }
    },
    [key, storedValue, serializer],
  );

  const removeValue = useCallback(() => {
    if (typeof window === "undefined") return;

    try {
      localStorage.removeItem(key);
      setStoredValue(initialValue);
    } catch (error) {
      console.error(`Error removing localStorage key "${key}":`, error);
    }
  }, [key, initialValue]);

  return [storedValue, setValue, removeValue];
}

// ✅ Advanced debounce hook with proper typing
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// ✅ Previous value hook with generic typing
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

Advanced API Hooks

Sophisticated data fetching patterns with comprehensive typing:

Code
// ✅ Comprehensive API state management
interface ApiState<T> {
  data: T | null;
  loading: boolean;
  error: ApiError | null;
  lastFetch: number | null;
}

interface ApiError {
  message: string;
  status?: number;
  code?: string;
}

interface UseApiOptions {
  enabled?: boolean;
  refetchInterval?: number;
  retry?: number;
  retryDelay?: number;
  staleTime?: number;
}

interface UseApiResult<T> extends ApiState<T> {
  refetch: () => Promise<T | null>;
  mutate: (data: T | null) => void;
  isStale: boolean;
}

function useApi<T>(url: string, options: UseApiOptions = {}): UseApiResult<T> {
  const {
    enabled = true,
    refetchInterval,
    retry = 0,
    retryDelay = 1000,
    staleTime = 5 * 60 * 1000, // 5 minutes
  } = options;

  const [state, setState] = useState<ApiState<T>>({
    data: null,
    loading: enabled,
    error: null,
    lastFetch: null,
  });

  const retryCountRef = useRef(0);
  const abortControllerRef = useRef<AbortController | null>(null);

  const fetchData = useCallback(async (): Promise<T | null> => {
    if (!enabled) return null;

    // Cancel previous request
    abortControllerRef.current?.abort();
    abortControllerRef.current = new AbortController();

    setState((prev) => ({ ...prev, loading: true, error: null }));

    try {
      const response = await fetch(url, {
        signal: abortControllerRef.current.signal,
      });

      if (!response.ok) {
        throw new ApiError({
          message: `HTTP ${response.status}: ${response.statusText}`,
          status: response.status,
        });
      }

      const data: T = await response.json();
      const now = Date.now();

      setState({
        data,
        loading: false,
        error: null,
        lastFetch: now,
      });

      retryCountRef.current = 0;
      return data;
    } catch (error) {
      if (error.name === "AbortError") return null;

      const apiError: ApiError = {
        message: error instanceof Error ? error.message : "Unknown error",
        status: error.status,
        code: error.code,
      };

      if (retryCountRef.current < retry) {
        retryCountRef.current++;
        setTimeout(() => fetchData(), retryDelay * retryCountRef.current);
        return null;
      }

      setState((prev) => ({
        ...prev,
        loading: false,
        error: apiError,
      }));

      return null;
    }
  }, [url, enabled, retry, retryDelay]);

  // Initial fetch
  useEffect(() => {
    if (enabled) {
      fetchData();
    }

    return () => {
      abortControllerRef.current?.abort();
    };
  }, [fetchData]);

  // Refetch interval
  useEffect(() => {
    if (!refetchInterval || !enabled) return;

    const interval = setInterval(fetchData, refetchInterval);
    return () => clearInterval(interval);
  }, [fetchData, refetchInterval, enabled]);

  const mutate = useCallback((data: T | null) => {
    setState((prev) => ({
      ...prev,
      data,
      lastFetch: data ? Date.now() : prev.lastFetch,
    }));
  }, []);

  const isStale = useMemo(() => {
    if (!state.lastFetch) return true;
    return Date.now() - state.lastFetch > staleTime;
  }, [state.lastFetch, staleTime]);

  return {
    ...state,
    refetch: fetchData,
    mutate,
    isStale,
  };
}

// ✅ Mutation hook for POST/PUT/DELETE operations
interface UseMutationOptions<TVariables, TData> {
  onSuccess?: (data: TData, variables: TVariables) => void;
  onError?: (error: ApiError, variables: TVariables) => void;
  onSettled?: (
    data: TData | null,
    error: ApiError | null,
    variables: TVariables,
  ) => void;
}

interface MutationState<TData> {
  data: TData | null;
  loading: boolean;
  error: ApiError | null;
}

interface MutationResult<TVariables, TData> extends MutationState<TData> {
  mutate: (variables: TVariables) => Promise<TData | null>;
  reset: () => void;
}

function useMutation<TVariables, TData>(
  mutationFn: (variables: TVariables) => Promise<TData>,
  options: UseMutationOptions<TVariables, TData> = {},
): MutationResult<TVariables, TData> {
  const { onSuccess, onError, onSettled } = options;

  const [state, setState] = useState<MutationState<TData>>({
    data: null,
    loading: false,
    error: null,
  });

  const mutate = useCallback(
    async (variables: TVariables): Promise<TData | null> => {
      setState({ data: null, loading: true, error: null });

      try {
        const data = await mutationFn(variables);
        setState({ data, loading: false, error: null });

        onSuccess?.(data, variables);
        onSettled?.(data, null, variables);

        return data;
      } catch (error) {
        const apiError: ApiError = {
          message: error instanceof Error ? error.message : "Mutation failed",
          status: error.status,
          code: error.code,
        };

        setState({ data: null, loading: false, error: apiError });

        onError?.(apiError, variables);
        onSettled?.(null, apiError, variables);

        return null;
      }
    },
    [mutationFn, onSuccess, onError, onSettled],
  );

  const reset = useCallback(() => {
    setState({ data: null, loading: false, error: null });
  }, []);

  return { ...state, mutate, reset };
}

Advanced Event Handling Patterns

Comprehensive Event Handler Typing

Master TypeScript event handling with sophisticated patterns:

Code
// ✅ Generic event handler types for reusability
type EventHandler<T extends HTMLElement, E extends Event = Event> = (
  event: E & { currentTarget: T }
) => void;

type ChangeHandler<T extends HTMLElement> = EventHandler<
  T,
  React.ChangeEvent<T>
>;

type ClickHandler<T extends HTMLElement> = EventHandler<
  T,
  React.MouseEvent<T>
>;

type KeyHandler<T extends HTMLElement> = EventHandler<
  T,
  React.KeyboardEvent<T>
>;

// ✅ Advanced form component with comprehensive event handling
interface FormData {
  email: string;
  password: string;
  rememberMe: boolean;
  preferences: {
    newsletter: boolean;
    notifications: 'all' | 'important' | 'none';
  };
}

interface FormValidation {
  isValid: boolean;
  errors: Partial<Record<keyof FormData, string>>;
}

interface AdvancedFormProps {
  initialData?: Partial<FormData>;
  onSubmit: (data: FormData) => Promise<void>;
  onValidation?: (validation: FormValidation) => void;
  onFieldChange?: <K extends keyof FormData>(
    field: K,
    value: FormData[K],
    isValid: boolean
  ) => void;
}

const AdvancedForm: React.FC<AdvancedFormProps> = ({
  initialData = {},
  onSubmit,
  onValidation,
  onFieldChange,
}) => {
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: '',
    rememberMe: false,
    preferences: {
      newsletter: true,
      notifications: 'important',
    },
    ...initialData,
  });

  const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // ✅ Strongly typed field update handler
  const handleFieldChange = useCallback(
    <K extends keyof FormData>(field: K, value: FormData[K]) => {
      setFormData(prev => ({ ...prev, [field]: value }));

      // Clear error for this field
      if (errors[field]) {
        setErrors(prev => ({ ...prev, [field]: undefined }));
      }

      // Validate field and notify parent
      const isValid = validateField(field, value);
      onFieldChange?.(field, value, isValid);
    },
    [errors, onFieldChange]
  );

  // ✅ Email input with proper typing
  const handleEmailChange: ChangeHandler<HTMLInputElement> = (e) => {
    const email = e.currentTarget.value;
    handleFieldChange('email', email);
  };

  // ✅ Password input with visibility toggle
  const [showPassword, setShowPassword] = useState(false);
  const handlePasswordChange: ChangeHandler<HTMLInputElement> = (e) => {
    handleFieldChange('password', e.currentTarget.value);
  };

  const togglePasswordVisibility: ClickHandler<HTMLButtonElement> = (e) => {
    e.preventDefault();
    setShowPassword(prev => !prev);
  };

  // ✅ Checkbox handling
  const handleCheckboxChange: ChangeHandler<HTMLInputElement> = (e) => {
    const { name, checked } = e.currentTarget;

    if (name === 'rememberMe') {
      handleFieldChange('rememberMe', checked);
    } else if (name === 'newsletter') {
      handleFieldChange('preferences', {
        ...formData.preferences,
        newsletter: checked,
      });
    }
  };

  // ✅ Select/dropdown handling
  const handleNotificationChange: ChangeHandler<HTMLSelectElement> = (e) => {
    const value = e.currentTarget.value as FormData['preferences']['notifications'];
    handleFieldChange('preferences', {
      ...formData.preferences,
      notifications: value,
    });
  };

  // ✅ Form submission with error handling
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const validation = validateForm(formData);
    setErrors(validation.errors);
    onValidation?.(validation);

    if (!validation.isValid) return;

    setIsSubmitting(true);
    try {
      await onSubmit(formData);
    } catch (error) {
      console.error('Form submission error:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  // ✅ Keyboard shortcuts
  const handleKeyDown: KeyHandler<HTMLFormElement> = (e) => {
    // Submit on Ctrl/Cmd + Enter
    if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
      e.preventDefault();
      handleSubmit(e as any);
    }
  };

  return (
    <form onSubmit={handleSubmit} onKeyDown={handleKeyDown} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          id="email"
          type="email"
          value={formData.email}
          onChange={handleEmailChange}
          className={`mt-1 block w-full rounded-md border ${
            errors.email ? 'border-red-500' : 'border-gray-300'
          }`}
          required
          aria-describedby={errors.email ? 'email-error' : undefined}
        />
        {errors.email && (
          <p id="email-error" className="mt-1 text-sm text-red-600">
            {errors.email}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          Password
        </label>
        <div className="relative">
          <input
            id="password"
            type={showPassword ? 'text' : 'password'}
            value={formData.password}
            onChange={handlePasswordChange}
            className={`mt-1 block w-full rounded-md border pr-10 ${
              errors.password ? 'border-red-500' : 'border-gray-300'
            }`}
            required
          />
          <button
            type="button"
            onClick={togglePasswordVisibility}
            className="absolute inset-y-0 right-0 flex items-center pr-3"
            aria-label={showPassword ? 'Hide password' : 'Show password'}
          >
            {showPassword ? '🙈' : '👁️'}
          </button>
        </div>
        {errors.password && (
          <p className="mt-1 text-sm text-red-600">{errors.password}</p>
        )}
      </div>

      <div className="space-y-2">
        <label className="flex items-center">
          <input
            type="checkbox"
            name="rememberMe"
            checked={formData.rememberMe}
            onChange={handleCheckboxChange}
            className="mr-2"
          />
          Remember me
        </label>

        <label className="flex items-center">
          <input
            type="checkbox"
            name="newsletter"
            checked={formData.preferences.newsletter}
            onChange={handleCheckboxChange}
            className="mr-2"
          />
          Subscribe to newsletter
        </label>
      </div>

      <div>
        <label htmlFor="notifications" className="block text-sm font-medium">
          Notification Preferences
        </label>
        <select
          id="notifications"
          value={formData.preferences.notifications}
          onChange={handleNotificationChange}
          className="mt-1 block w-full rounded-md border border-gray-300"
        >
          <option value="all">All notifications</option>
          <option value="important">Important only</option>
          <option value="none">None</option>
        </select>
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full rounded-md bg-blue-600 py-2 px-4 text-white disabled:opacity-50"
      >
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
};

// ✅ Validation helper functions
function validateField<K extends keyof FormData>(
  field: K,
  value: FormData[K]
): boolean {
  switch (field) {
    case 'email':
      return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
    case 'password':
      return typeof value === 'string' && value.length >= 8;
    default:
      return true;
  }
}

function validateForm(data: FormData): FormValidation {
  const errors: Partial<Record<keyof FormData, string>> = {};

  if (!validateField('email', data.email)) {
    errors.email = 'Please enter a valid email address';
  }

  if (!validateField('password', data.password)) {
    errors.password = 'Password must be at least 8 characters long';
  }

  return {
    isValid: Object.keys(errors).length === 0,
    errors,
  };
}

Advanced TypeScript Patterns

Sophisticated Discriminated Unions

Master complex component variants with advanced discriminated unions:

Code
// ✅ Advanced notification system with conditional props
type BaseNotificationProps = {
  id: string;
  title: string;
  timestamp: number;
  onDismiss?: (id: string) => void;
};

type NotificationProps =
  | (BaseNotificationProps & {
      type: 'info';
      message: string;
      icon?: React.ReactNode;
    })
  | (BaseNotificationProps & {
      type: 'success';
      message: string;
      duration?: number;
      showProgress?: boolean;
    })
  | (BaseNotificationProps & {
      type: 'warning';
      message: string;
      actions?: Array<{ label: string; onClick: () => void }>;
    })
  | (BaseNotificationProps & {
      type: 'error';
      message: string;
      error?: Error;
      onRetry: () => void;
      maxRetries?: number;
      currentRetry?: number;
    })
  | (BaseNotificationProps & {
      type: 'loading';
      message?: string;
      progress?: number;
      onCancel?: () => void;
    });

const Notification: React.FC<NotificationProps> = (props) => {
  const baseClasses = "p-4 rounded-lg shadow-lg border-l-4";

  switch (props.type) {
    case 'info':
      return (
        <div className={`${baseClasses} border-blue-500 bg-blue-50`}>
          <div className="flex items-start">
            {props.icon && <div className="mr-3">{props.icon}</div>}
            <div>
              <h4 className="font-medium text-blue-800">{props.title}</h4>
              <p className="text-blue-700">{props.message}</p>
            </div>
          </div>
        </div>
      );

    case 'success':
      return (
        <div className={`${baseClasses} border-green-500 bg-green-50 relative`}>
          <div className="flex items-start justify-between">
            <div>
              <h4 className="font-medium text-green-800">{props.title}</h4>
              <p className="text-green-700">{props.message}</p>
            </div>
            {props.onDismiss && (
              <button
                onClick={() => props.onDismiss!(props.id)}
                className="text-green-500 hover:text-green-700"
              >
                ×
              </button>
            )}
          </div>
          {props.showProgress && props.duration && (
            <div className="absolute bottom-0 left-0 h-1 bg-green-500 animate-pulse" />
          )}
        </div>
      );

    case 'warning':
      return (
        <div className={`${baseClasses} border-yellow-500 bg-yellow-50`}>
          <div>
            <h4 className="font-medium text-yellow-800">{props.title}</h4>
            <p className="text-yellow-700 mb-3">{props.message}</p>
            {props.actions && (
              <div className="flex space-x-2">
                {props.actions.map((action, index) => (
                  <button
                    key={index}
                    onClick={action.onClick}
                    className="px-3 py-1 bg-yellow-200 text-yellow-800 rounded text-sm hover:bg-yellow-300"
                  >
                    {action.label}
                  </button>
                ))}
              </div>
            )}
          </div>
        </div>
      );

    case 'error':
      return (
        <div className={`${baseClasses} border-red-500 bg-red-50`}>
          <div>
            <h4 className="font-medium text-red-800">{props.title}</h4>
            <p className="text-red-700 mb-3">{props.message}</p>
            {props.error && (
              <details className="mb-3">
                <summary className="text-sm text-red-600 cursor-pointer">
                  Error Details
                </summary>
                <pre className="text-xs bg-red-100 p-2 rounded mt-1 overflow-x-auto">
                  {props.error.stack || props.error.message}
                </pre>
              </details>
            )}
            <div className="flex items-center space-x-2">
              <button
                onClick={props.onRetry}
                className="px-3 py-1 bg-red-200 text-red-800 rounded text-sm hover:bg-red-300"
              >
                Retry {props.currentRetry ? `(${props.currentRetry}/${props.maxRetries || 3})` : ''}
              </button>
            </div>
          </div>
        </div>
      );

    case 'loading':
      return (
        <div className={`${baseClasses} border-gray-500 bg-gray-50`}>
          <div className="flex items-center justify-between">
            <div className="flex items-center">
              <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-3" />
              <div>
                <h4 className="font-medium text-gray-800">{props.title}</h4>
                {props.message && <p className="text-gray-600">{props.message}</p>}
              </div>
            </div>
            {props.onCancel && (
              <button
                onClick={props.onCancel}
                className="text-gray-500 hover:text-gray-700"
              >
                Cancel
              </button>
            )}
          </div>
          {typeof props.progress === 'number' && (
            <div className="mt-3">
              <div className="flex justify-between text-xs text-gray-600">
                <span>Progress</span>
                <span>{Math.round(props.progress * 100)}%</span>
              </div>
              <div className="w-full bg-gray-200 rounded-full h-2">
                <div
                  className="bg-blue-600 h-2 rounded-full transition-all duration-300"
                  style={{ width: `${props.progress * 100}%` }}
                />
              </div>
            </div>
          )}
        </div>
      );

    default:
      // TypeScript ensures this is never reached
      const _exhaustiveCheck: never = props;
      return null;
  }
};

Advanced Utility Types and Transformations

Leverage sophisticated TypeScript utility patterns:

Code
// ✅ Comprehensive user interface
interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
  profile: {
    bio?: string;
    location?: string;
    website?: string;
  };
  preferences: {
    theme: "light" | "dark" | "auto";
    notifications: {
      email: boolean;
      push: boolean;
      sms: boolean;
    };
  };
  metadata: {
    createdAt: Date;
    updatedAt: Date;
    lastLoginAt?: Date;
  };
}

// ✅ Advanced utility type combinations
type CreateUserInput = Omit<User, "id" | "metadata"> & {
  metadata?: Pick<User["metadata"], "createdAt">;
};

type UpdateUserInput = Partial<Omit<User, "id" | "metadata">> & {
  id: string;
};

type UserPublicProfile = Pick<User, "id" | "name" | "avatar" | "profile"> & {
  isOnline?: boolean;
};

type UserPreferences = User["preferences"];
type NotificationSettings = UserPreferences["notifications"];

// ✅ Conditional types for dynamic API responses
type ApiResponse<T> =
  | {
      success: true;
      data: T;
      meta?: {
        pagination?: {
          page: number;
          limit: number;
          total: number;
          pages: number;
        };
      };
    }
  | {
      success: false;
      error: {
        message: string;
        code: string;
        details?: Record<string, any>;
      };
    };

// ✅ Mapped types for form field validation
type ValidationResult<T> = {
  [K in keyof T]?: {
    isValid: boolean;
    message?: string;
  };
};

type FormErrors<T> = {
  [K in keyof T]?: string;
};

// ✅ Deep partial for nested updates
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// ✅ Template literal types for API endpoints
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiEndpoint = `/api/${"users" | "posts" | "comments"}${"" | `/${string}`}`;

// ✅ Branded types for type safety
type UserId = string & { readonly brand: unique symbol };
type Email = string & { readonly brand: unique symbol };

function createUserId(id: string): UserId {
  if (!id || typeof id !== "string") {
    throw new Error("Invalid user ID");
  }
  return id as UserId;
}

function createEmail(email: string): Email {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    throw new Error("Invalid email format");
  }
  return email as Email;
}

// ✅ Function overloads for flexible APIs
function fetchUser(id: UserId): Promise<User>;
function fetchUser(email: Email): Promise<User>;
function fetchUser(ids: UserId[]): Promise<User[]>;
function fetchUser(criteria: { name?: string; email?: Email }): Promise<User[]>;

async function fetchUser(
  param: UserId | Email | UserId[] | { name?: string; email?: Email },
): Promise<User | User[]> {
  if (typeof param === "string") {
    // Handle single ID or email
    const isEmail = param.includes("@");
    const response = await fetch(
      `/api/users/${isEmail ? "by-email" : "by-id"}/${param}`,
    );
    return response.json();
  }

  if (Array.isArray(param)) {
    // Handle multiple IDs
    const response = await fetch("/api/users/batch", {
      method: "POST",
      body: JSON.stringify({ ids: param }),
    });
    return response.json();
  }

  // Handle criteria object
  const queryParams = new URLSearchParams();
  if (param.name) queryParams.set("name", param.name);
  if (param.email) queryParams.set("email", param.email);

  const response = await fetch(`/api/users/search?${queryParams}`);
  return response.json();
}

// ✅ Generic constraint patterns
interface Repository<T extends { id: string }> {
  findById(id: string): Promise<T | null>;
  findMany(criteria: Partial<T>): Promise<T[]>;
  create(data: Omit<T, "id">): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> {
    const response = await fetch(`/api/users/${id}`);
    return response.ok ? response.json() : null;
  }

  async findMany(criteria: Partial<User>): Promise<User[]> {
    const queryParams = new URLSearchParams();
    Object.entries(criteria).forEach(([key, value]) => {
      if (value !== undefined) queryParams.set(key, String(value));
    });

    const response = await fetch(`/api/users?${queryParams}`);
    return response.json();
  }

  async create(data: Omit<User, "id">): Promise<User> {
    const response = await fetch("/api/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    return response.json();
  }

  async update(id: string, data: Partial<User>): Promise<User> {
    const response = await fetch(`/api/users/${id}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    return response.json();
  }

  async delete(id: string): Promise<void> {
    await fetch(`/api/users/${id}`, { method: "DELETE" });
  }
}

Best Practices Summary

Key Takeaways

  1. Always use explicit types - Don't rely on any or implicit typing
  2. Leverage generic patterns - Create reusable, type-safe components
  3. Use discriminated unions - Handle complex component variants safely
  4. Implement proper error boundaries - Type your error handling
  5. Master utility types - Transform types efficiently with built-in utilities
  6. Create custom hooks - Encapsulate logic with proper typing
  7. Handle events properly - Use specific event handler types
  8. Validate at boundaries - Type your API interactions and form inputs

Development Workflow

  1. Start with interfaces - Define your data structures first
  2. Build incrementally - Add types as you develop features
  3. Test thoroughly - Use TypeScript's compiler as your first test
  4. Refactor confidently - Let types guide safe refactoring
  5. Document with types - Use TypeScript as living documentation

By following these patterns, you'll build React applications that are not only type-safe but also maintainable, scalable, and robust. TypeScript's type system becomes your ally in creating better software architecture and catching bugs before they reach production.