← thecodex.expert · The Codex Family of Knowledge
TypeScript

TypeScript with React

Passing the wrong prop to a component, or forgetting a required one, is caught at compile time — not discovered when a user hits a blank screen in production.

TypeScript 6.0 .tsx extension Last verified:
Where this bites you

Inside one event handler, count is a plain constant: the value from the render that created the handler. setCount(count + 1) twice therefore schedules "set it to 1" twice, not "add one" twice, so the button goes up by one per click. The types are completely correct, count is a number and setCount accepts one, which is exactly why no type annotation can catch it.

TypeScriptCounter.tsx
import { useState } from "react";

export function Counter() {
    const [count, setCount] = useState(0);

    function addTwo() {
        setCount(count + 1);
        setCount(count + 1);   // same stale `count` as the line above
    }

    return <button onClick={addTwo}>count: {count}</button>;
}
// click: count: 0 -> count: 1   (not 2)
// click: count: 1 -> count: 2
Canonical Definition

A React file containing JSX must use the .tsx extension (rather than plain .ts), so the compiler knows to parse JSX syntax. A component's props are typically typed with an interface or type alias, and destructured directly in the function signature. Hooks are generic: useState<T> can have its type parameter given explicitly or inferred from the initial value passed in.

Typing component props

A missing required prop, or a prop of the wrong type, is a compile error at every call site — caught long before the component ever renders in a browser.

TypeScriptGreeting.tsx
interface GreetingProps {
    name: string;
    excited?: boolean;   // optional prop
}

function Greeting({ name, excited = false }: GreetingProps) {
    return <div>{excited ? `Hi ${name}!!!` : `Hello, ${name}`}</div>;
}

<Greeting name="Alice" />;          // fine
<Greeting name="Bob" excited />;    // fine
// <Greeting />;                    // COMPILE ERROR: missing required prop 'name'

Typing useState and useEffect

useState infers its type from the initial value when possible; an explicit type argument is needed when the state can be a union that isn't obvious from the initializer alone, like a value that starts as null.

TypeScriptUserProfile.tsx
interface User { name: string; }

function UserProfile() {
    const [count, setCount] = useState(0);              // inferred: number
    const [user, setUser] = useState<User | null>(null); // explicit: needed since null alone infers only null

    useEffect(() => {
        fetchUser().then(setUser);
    }, []);

    return user ? <p>{user.name}</p> : <p>Loading...</p>;
}

The useState setter also accepts a function, and that form is the fix whenever the next state depends on the previous one: React calls it with the latest value, including updates queued earlier in the same handler. (Verified with React 19.2.6 running in Chromium. React's own type definitions, @types/react, weren't available offline, so these examples were compiled by esbuild, which strips types without checking them.)

TypeScriptCounterFix.tsx
import { useState } from "react";

export function Counter() {
    const [count, setCount] = useState(0);

    function addTwo() {
        setCount((c) => c + 1);
        setCount((c) => c + 1);   // each update receives the latest value
    }

    return <button onClick={addTwo}>count: {count}</button>;
}
// click: count: 0 -> count: 2
// click: count: 2 -> count: 4

Typing event handlers

React's own type definitions provide specific event types (React.ChangeEvent<HTMLInputElement>, etc.), giving full autocomplete on the event object rather than treating it as untyped.

TypeScriptSearchInput.tsx
function SearchInput() {
    const [query, setQuery] = useState("");

    function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
        setQuery(e.target.value);   // e.target correctly typed as HTMLInputElement
    }

    return <input value={query} onChange={handleChange} />;
}

Sources

1
Microsoft. TypeScript Handbook, "JSX", typescriptlang.org/docs/handbook/jsx.html, and the React TypeScript Cheatsheet (community-maintained, endorsed by the React docs).