Skip to main content

TypeScript with React

Typing Component Props

The most common thing you'll type in React is component props:

interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
}

function Button({ label, onClick, variant = "primary", disabled = false }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant}`}
    >
      {label}
    </button>
  );
}

// Usage
<Button label="Submit" onClick={()=> console.log("clicked")} />
<Button label="Delete" onClick={handleDelete} variant="danger" />

Children Props

There are several ways to type components that accept children:

// Using React.ReactNode (most flexible — accepts anything renderable)
interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
}

// Using PropsWithChildren utility
import { PropsWithChildren } from "react";

type LayoutProps = PropsWithChildren<{
  sidebar?: React.ReactNode;
}>;

function Layout({ children, sidebar }: LayoutProps) {
  return (
    <div className="layout">
      {sidebar && <aside>{sidebar}</aside>}
      <main>{children}</main>
    </div>
  );
}

Typing Hooks

useState

// Type is inferred from the initial value
const [count, setCount] = useState(0);           // number
const [name, setName] = useState("Sabaoon");      // string

// Explicit type when initial value doesn't tell the full story
const [user, setUser] = useState<User | null>(null);

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

// Now TypeScript knows user could be null
if (user) {
  console.log(user.name); // safe after null check
}

useRef

// DOM element ref — pass null as initial value
const inputRef = useRef<HTMLInputElement>(null);

function focusInput() {
  inputRef.current?.focus(); // optional chaining because current can be null
}

return <input ref={inputRef} />;

// Mutable value ref (not for DOM elements)
const timerRef = useRef<number>(0);
timerRef.current = window.setTimeout(() => {}, 1000);

useReducer

interface State {
  count: number;
  error: string | null;
}

type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset"; payload: number }
  | { type: "error"; payload: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + 1, error: null };
    case "decrement":
      return { ...state, count: state.count - 1, error: null };
    case "reset":
      return { ...state, count: action.payload, error: null };
    case "error":
      return { ...state, error: action.payload };
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0, error: null });

dispatch({ type: "increment" });
dispatch({ type: "reset", payload: 0 });
// dispatch({ type: "reset" }); // Error: missing 'payload'

Typing Events

React has built-in types for every event:

function Form() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

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

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

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      // submit
    }
  };

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

Tip: If you're not sure which event type to use, hover over the event handler prop in your editor — TypeScript will tell you the expected type.

Typing Context

import { createContext, useContext, useState, ReactNode } from "react";

interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

// Create context with a default value (or null + a custom hook)
const AuthContext = createContext<AuthContextType | null>(null);

// Custom hook with null check — no need to check for null at every call site
function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

// Provider component
function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = async (email: string, password: string) => {
    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    });
    const data = await res.json();
    setUser(data.user);
  };

  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Usage in a component — fully typed, no null checks needed
function Profile() {
  const { user, logout } = useAuth();

  if (!user) return <p>Please log in.</p>;

  return (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Log out</button>
    </div>
  );
}

Generic Components

You can make components generic to work with different data types:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

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

// Usage — TypeScript infers T from the items array
interface Product {
  id: string;
  name: string;
  price: number;
}

const products: Product[] = [
  { id: "1", name: "Keyboard", price: 79 },
  { id: "2", name: "Mouse", price: 49 },
];

<List
  items={products}
  keyExtractor={(p)=> p.id}
  renderItem={(p)=> <span>{p.name} — ${p.price}</span>}
/>;