React & TypeScript

React with TypeScript

TypeScript adds static type checking to JavaScript. Using it with React catches bugs at compile time, powers autocomplete in your editor, and makes refactoring safe.

Setup

# New project
npm create vite@latest my-app -- --template react-ts

# Add to existing Vite project
npm install -D typescript @types/react @types/react-dom

Typing Props

// Option 1: inline interface
function UserCard({ name, age, role, onDelete }: {
  name:     string;
  age?:     number;      // optional prop
  role:     "admin" | "user" | "moderator"; // union type
  onDelete: (id: string) => void;
}) {
  return <div>{name}</div>;
}

// Option 2: separate interface (preferred for reuse)
interface Product {
  id:       string;
  name:     string;
  price:    number;
  inStock:  boolean;
  tags?:    string[];
}

function ProductCard({ product }: { product: Product }) {
  return <div>{product.name} โ€” ${product.price}</div>;
}

Typing useState

// Type inferred from initial value
const [count, setCount]   = useState(0);          // number
const [name, setName]     = useState("");          // string
const [isOpen, setIsOpen] = useState(false);       // boolean

// Explicit type needed when initial value is null or empty
const [user, setUser]     = useState<User | null>(null);
const [items, setItems]   = useState<Product[]>([]);

// useRef
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<number | null>(null);

Typing useEffect & Events

// Events are typed automatically
function SearchInput() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === "Enter") search();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKeyDown} />
    </form>
  );
}

Typing Custom Hooks

interface FetchResult<T> {
  data:    T | null;
  loading: boolean;
  error:   string | null;
}

function useFetch<T>(url: string): FetchResult<T> {
  const [data, setData]       = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError]     = useState<string | null>(null);

  useEffect(() => {
    fetch(url)
      .then(r => r.json() as Promise<T>)
      .then(setData)
      .catch((e: Error) => setError(e.message))
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// Usage โ€” fully typed
const { data: users } = useFetch<User[]>("/api/users");
// users is User[] | null โ€” TypeScript knows the shape
TipStart new projects with TypeScript from day one. Adding it to an existing project later is painful. The react-ts Vite template sets everything up for you instantly.

๐Ÿง  Test Yourself

What file extension do TypeScript React components use?