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.
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: 2A 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.
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.
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.)
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: 4Typing 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.
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} />;
}