Most real React codebases today are written in TypeScript rather than plain JavaScript — see React Fundamentals for the component/hooks side of that pairing. A few patterns specific to combining the two:
interface CardProps {
title: string;
onClose?: () => void; // optional prop
children: React.ReactNode; // the type for "anything React can render," including strings and other elements
}
function Card({ title, onClose, children }: CardProps) {
return (
<div className="card">
<h3>{title}</h3>
{children}
{onClose && <button onClick={onClose}>Close</button>}
</div>
);
}
Typing hooks correctly is where most of the friction shows up in practice:
// useState: TS infers from the initial value when possible, but an explicit type
// argument is needed when the initial value doesn't cover every later possibility
const [user, setUser] = useState<User | null>(null);
// useRef targeting a DOM node needs an explicit element type, initialized to null
const inputRef = useRef<HTMLInputElement>(null);
// A typed event handler, instead of an untyped inline function
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value); // e.target is correctly typed as HTMLInputElement, with .value
}
useState<User | null>(null) is the common pattern for state that starts empty and is filled in later (e.g., after a fetch resolves) — without the explicit type argument, TypeScript would infer the state's type as just null from the initial value, and never allow assigning an actual User to it later.