TypeScript in React: Practical Typing Patterns That Actually Help

I avoided TypeScript in my React projects longer than I should have. The error messages looked cryptic, the setup seemed heavy, and I already had PropTypes. Then a junior dev introduced a subtle prop-shape bug that took three hours to track down — a bug TypeScript would have caught at save time. I've typed my React ever since.
This post skips the philosophy and goes straight to the five patterns I reach for every day.
1. Typing component props with an interface
The most common thing you'll type is a component's props. Define an interface just above the component and pass it as the generic argument to
, or just annotate the destructured parameter directly — the second approach is less noisy:1React.FC
1interface ButtonProps { 2 label: string; 3 onClick: () => void; 4 disabled?: boolean; // optional — note the '?' 5 variant?: 'primary' | 'secondary'; // union type keeps valid values explicit 6} 7 8export function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) { 9 return ( 10 <button 11 className={`btn btn-${variant}`} 12 onClick={onClick} 13 disabled={disabled} 14 > 15 {label} 16 </button> 17 ); 18} 19
The union type
is more useful than1'primary' | 'secondary'
. Passing1string
now gives a compile-time error instead of a silent style mismatch at runtime.1variant="danger"
2. useState with generics
infers its type from the initial value, which works fine for primitives. Where it breaks down is when the initial value is1useState
or an empty array, because the inferred type becomes1null
or1null
and TypeScript won't let you assign anything real to it later.1never[]
Fix this by passing the type explicitly:
1// Bad — TypeScript infers the type as `null` only, so setUser won't accept a User object 2const [user, setUser] = useState(null); 3 4// Good — TypeScript knows it's User | null from the start 5interface User { 6 id: number; 7 name: string; 8 email: string; 9} 10 11const [user, setUser] = useState<User | null>(null); 12const [items, setItems] = useState<string[]>([]); 13
With the typed version,
compiles cleanly, and1setUser({ id: 1, name: 'Alice', email: '[email protected]' })
(wrong shape) errors immediately.1setUser({ id: 1, username: 'alice' })
3. Event handler types
Event types in React are namespaced under
. The ones I use constantly:1React
1function SearchBox() { 2 const [query, setQuery] = useState(''); 3 4 // React.ChangeEvent<HTMLInputElement> is the type of the onChange event 5 function handleChange(e: React.ChangeEvent<HTMLInputElement>) { 6 setQuery(e.target.value); 7 } 8 9 // React.FormEvent<HTMLFormElement> for form submit 10 function handleSubmit(e: React.FormEvent<HTMLFormElement>) { 11 e.preventDefault(); 12 console.log('Searching for:', query); 13 } 14 15 return ( 16 <form onSubmit={handleSubmit}> 17 <input value={query} onChange={handleChange} /> 18 <button type="submit">Search</button> 19 </form> 20 ); 21} 22
The pattern is always
. A few to memorise:1React.<EventType><HTMLElement>
| Event | HTML element | Type | |---|---|---| |
|1onChange
|1<input>
| |1React.ChangeEvent<HTMLInputElement>
|1onChange
|1<select>
| |1React.ChangeEvent<HTMLSelectElement>
|1onSubmit
|1<form>
| |1React.FormEvent<HTMLFormElement>
| anything |1onClick
| |1React.MouseEvent<HTMLButtonElement>
| anything |1onKeyDown
|1React.KeyboardEvent<HTMLInputElement>
4. Typing API responses
A common mistake is leaving
response data as1fetch
. That defeats the whole point. Define an interface that mirrors the API shape and cast once at the boundary:1any
1interface Post { 2 id: number; 3 title: string; 4 body: string; 5 userId: number; 6} 7 8async function fetchPost(id: number): Promise<Post> { 9 const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`); 10 if (!res.ok) throw new Error(`HTTP error: ${res.status}`); 11 return res.json() as Promise<Post>; 12} 13 14// In a component: 15function PostDetail({ id }: { id: number }) { 16 const [post, setPost] = useState<Post | null>(null); 17 18 useEffect(() => { 19 fetchPost(id).then(setPost); 20 }, [id]); 21 22 if (!post) return <p>Loading...</p>; 23 24 return <h2>{post.title}</h2>; // TypeScript knows post.title is a string 25} 26
The
cast on1as Promise<Post>
is the one spot where you're trusting the server. If the shape changes, you only have one place to update.1res.json()
5. Typing custom hooks
Custom hooks are where TypeScript earns its keep most clearly, because the return type becomes part of the hook's public API.
1interface UseFetchResult<T> { 2 data: T | null; 3 loading: boolean; 4 error: string | null; 5} 6 7function useFetch<T>(url: string): UseFetchResult<T> { 8 const [data, setData] = useState<T | null>(null); 9 const [loading, setLoading] = useState(true); 10 const [error, setError] = useState<string | null>(null); 11 12 useEffect(() => { 13 setLoading(true); 14 fetch(url) 15 .then((res) => { 16 if (!res.ok) throw new Error(`HTTP ${res.status}`); 17 return res.json() as Promise<T>; 18 }) 19 .then((json) => { 20 setData(json); 21 setLoading(false); 22 }) 23 .catch((err: Error) => { 24 setError(err.message); 25 setLoading(false); 26 }); 27 }, [url]); 28 29 return { data, loading, error }; 30} 31 32// Usage — TypeScript knows posts is Post[] | null 33const { data: posts, loading } = useFetch<Post[]>('/api/posts'); 34
The generic
makes the hook reusable: call it once for posts, once for users, once for anything — and each callsite gets fully typed data back.1<T>
Where to go from here
These five patterns cover the majority of TypeScript I write in day-to-day React work. Once they feel automatic, the natural next step is
and1React.ReactNode
for components that render children, and1React.PropsWithChildren
with a discriminated union for complex state. But honestly, mastering props, state, events, API data, and custom hooks will make you more productive with TypeScript in React than reading any amount of theory.1useReducer
Start by converting your next new component with typed props. The immediate feedback loop — catching a wrong prop type before you ever run the code — is what converts sceptics.
