TypeScript with React

Difficulty: Intermediate

Question

How do you use TypeScript effectively with React? Show typing patterns for components, hooks, and event handlers.

Answer

TypeScript and React work together to provide type-safe components, hooks, and event handling. Key patterns:

- Props: Use interfaces or type aliases for component props - useState: TypeScript infers from initial value, or use explicit generic for complex types - useRef: Use generic for DOM element refs (HTMLDivElement, etc.) - Events: Use React's typed event types (ChangeEvent, MouseEvent, etc.) - Children: Use React.ReactNode for the children prop - Generic components: Components that work with any data type while maintaining type safety

Avoid using React.FC - prefer plain function declarations with typed props for simpler, more predictable types.

Code examples

Component Props & Children

import { ReactNode } from 'react';

// Props interface
interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick: () => void;
  children: ReactNode;
}

// Function component with typed props
function Button({ variant, size = 'md', disabled = false, onClick, children }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// Props with discriminated union
type AlertProps =
  | { type: 'success'; message: string }
  | { type: 'error'; message: string; retry: () => void }
  | { type: 'loading' };

function Alert(props: AlertProps) {
  switch (props.type) {
    case 'success': return <div className="alert-success">{props.message}</div>;
    case 'error': return (
      <div className="alert-error">
        {props.message}
        <button onClick={props.retry}>Retry</button>
      </div>
    );
    case 'loading': return <div className="alert-loading">Loading...</div>;
  }
}

Type props as interfaces with explicit children: ReactNode. Discriminated unions in props enable conditional rendering with type safety.

Hooks Typing

import { useState, useRef, useEffect, useCallback } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  // useState with inference
  const [count, setCount] = useState(0); // inferred as number

  // useState with explicit type (for complex/null initial)
  const [user, setUser] = useState<User | null>(null);
  const [errors, setErrors] = useState<string[]>([]);

  // useRef for DOM elements
  const inputRef = useRef<HTMLInputElement>(null);
  const divRef = useRef<HTMLDivElement>(null);

  // useRef for mutable values (no null)
  const timerRef = useRef<number>(0);

  useEffect(() => {
    async function loadUser() {
      const res = await fetch(`/api/users/${userId}`);
      const data: User = await res.json();
      setUser(data);
    }
    loadUser();
  }, [userId]);

  const handleSubmit = useCallback((name: string) => {
    if (inputRef.current) {
      inputRef.current.focus(); // OK: narrowed from null
    }
  }, []);

  if (!user) return <div>Loading...</div>;
  return <div>{user.name} ({user.email})</div>;
}

useState infers from the initial value. Use explicit generics when the initial value is null or does not represent all possible states. useRef<Element>(null) for DOM refs.

Event Handlers & Generic Components

import { ChangeEvent, FormEvent, MouseEvent } from 'react';

// Typed event handlers
function LoginForm() {
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value); // string, fully typed
  };

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // form submission logic
  };

  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    console.log(e.clientX, e.clientY);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}

// Generic component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  );
}

// Usage: T is inferred from items
<List
  items={users}          // User[]
  renderItem={(user) => <span>{user.name}</span>} // user is User
  keyExtractor={(user) => String(user.id)}
/>

Use React's typed event types for handlers. Generic components accept a type parameter that flows through props, providing type safety without manual annotations.

Key points

Concepts covered

React Components, Props Typing, useState, useRef, Event Handlers, Generic Components