TypeScript with React: Practical Patterns That Actually Save Time
TypeScript in a React codebase pays off fastest when you stick to a handful of patterns rather than chasing full type coverage on day one. These are the ones I actually use.
Typing Props
type ButtonProps = {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
};
function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
return <button className={variant} onClick={onClick}>{label}</button>;
}
A union type like 'primary' | 'secondary' beats a plain string — it turns a typo into a compile-time error instead of a runtime bug.
Typing useState When Inference Isn't Enough
type User = { id: string; name: string } | null;
const [user, setUser] = useState<User>(null);
TypeScript infers the type fine for primitives, but for anything that starts as null or an empty array, an explicit generic saves you from a widened, mostly-useless type.
Typing Event Handlers
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
React ships its own event types — reach for React.ChangeEvent, React.MouseEvent, etc. instead of the native DOM event types, since they include React's synthetic event behavior.
Generic Components
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return <>{items.map(renderItem)}</>;
}
This is the pattern that makes a single List component reusable across completely different data shapes, with full autocomplete on renderItem's parameter — no any, no duplication.