TypeScript with React
Reviewed & published by Brayan K
By the end of this lesson you'll be able to type a React component's props, its useState and useRef, and its event handlers — so your editor catches mistakes as you type instead of your users finding them at runtime.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Type component props with an interface (or a type alias) — and know when to use each
- Type useState<T>() — and why an empty array needs an explicit generic
- Type useRef<T>() for DOM elements and for plain mutable values
- Type event handlers: React.ChangeEvent<HTMLInputElement> and React.MouseEvent
- Accept nested content with the children type React.ReactNode
- Understand React.FC — and why many teams now avoid it
💡 Real-World Analogy
A component's props interface is the label on a shipping box: "Contains: 1 string (name), 1 number (age), optional VIP sticker." Anyone sending you a box must match that label, or the post office rejects it at the counter — before it ships. Plain React lets any box through and you discover the missing parts after delivery (at runtime). TypeScript checks the label at the counter, so wrong or missing props never reach your users.
1. Typing Component Props
Every React component receives a single props object. To type it, you describe its shape once with an interface, then annotate the parameter with that interface. From then on TypeScript enforces required props, rejects wrong types, and autocompletes prop names for you. A ? after a prop name makes it optional. Read this worked example carefully.
You can describe props with either an interface or a type alias — for prop shapes they're nearly identical. A common convention: use interface for object shapes you might extend, and type for unions and one-offs. Notice the "primary" | "danger" union — that prop can only be one of those two strings.
🎯 Your Turn: finish the props interface
Fill in each ___ below. This snippet is read-only — work it out in your head (or type it into a real .tsx file) and check against the // ✅ Expected comments.
2. Typing useState & useRef
Both hooks are generic — they take a type inside angle brackets, like useState<number>(). Most of the time you don't need to write it: useState(0) is inferred as a number from its initial value. You add an explicit <T> when the initial value can't describe the real type — an empty array, or a value that's null now but will hold an object later.
The never[] trap
Write const [items, setItems] = useState([]); and TypeScript infers the type as never[] — "an array that can hold nothing." The moment you call setItems(["a"]) you get "Argument of type string is not assignable to parameter of type never."
The fix is to tell it what the array holds: useState<string[]>([]). This is the single most common state-typing mistake.
🎯 Your Turn: type three pieces of state
Replace each ___ with the correct generic. Check yourself against the // ✅ Expected lines.
3. Typing Events & Children
React gives each event a specific type, and you tell it which HTML element fired it using a generic. Type a change handler as React.ChangeEvent<HTMLInputElement> and e.target.value is correctly known to be a string. A click handler is React.MouseEvent<HTMLButtonElement>. To accept nested content (anything between your component's tags), type the children prop as React.ReactNode — the catch-all for "anything renderable."
4. Run It: the immutable state-update pattern
The TSX above can't run in this editor, but the update pattern you'll use with a typed setState<Todo[]> is plain JavaScript. The golden rule: never mutate the existing state — build and return a brand-new value. This runnable demo simulates setTodos with a function so you can press Run and watch it work.
// This runs in the editor (plain JS) to show the IMMUTABLE update pattern
// you'll use with React's typed setState. In real React + TS you'd write:
// const [todos, setTodos] = useState<Todo[]>([]);
// Here we simulate setTodos with a plain function so you can run it.
let todos = []; // pretend this is state: an array of { id, text, done }
// setState replaces the value — never mutate the old array, build a NEW one.
function addTodo(list, text) {
return [...list, { id: list.length + 1, text: text, done: false }];
}
function toggleTodo(list, id) {
// map returns a brand-new array; only the matching item is replaced.
return list.map(function (t) {
return t.id === id ? { ...t, done: !t.done } : t;
});
}
todos = addTodo(todos, "Learn props");
todos = addTodo(todos, "Type useState");
todos = toggleTodo(todos, 1);
todos.forEach(function (t) {
console.log((t.done ? "[x] " : "[ ] ") + t.text);
});
// Expected output:
// [x] Learn props
// [ ] Type useState🔎 Deep Dive: React.FC — and why many avoid it
You'll meet React.FC (short for "Function Component") in older code and tutorials. It works, but most teams have moved away from it in favour of typing the props directly on a plain function.
Common Errors (and the fix)
- Untyped props — "Parameter 'props' implicitly has an 'any' type": you wrote function Greeting(props) { with no type. Add an interface and annotate it: function Greeting(props: GreetingProps).
- Wrong event type — "Property 'value' does not exist on type EventTarget": you typed a change handler as a generic Event. Use React.ChangeEvent<HTMLInputElement> so e.target.value exists.
- useState inferred as never — "Argument of type 'string' is not assignable to parameter of type 'never'": you started state at []. Add the generic: useState<string[]>([]).
- Reading nullable state — "Object is possibly 'null'": state typed User | null must be guarded. Wrap access in if (user) { ... } or use optional chaining user?.name.
- Ref used too early — "Object is possibly 'null'" on inputRef.current: it's null until React attaches the element. Access it with inputRef.current?.focus().
📋 Quick Reference
| What you're typing | Code |
|---|---|
| Component props | function C(props: Props) { ... } |
| Optional prop | isVip?: boolean; |
| State (inferred) | useState(0) |
| State (array) | useState<string[]>([]) |
| State (nullable) | useState<User | null>(null) |
| DOM ref | useRef<HTMLInputElement>(null) |
| Change event | React.ChangeEvent<HTMLInputElement> |
| Click event | React.MouseEvent<HTMLButtonElement> |
| Children | children: React.ReactNode |
Frequently Asked Questions
Q: Should I use interface or type for props?
For props they're almost interchangeable. A common convention: interface for object/prop shapes (it can be extended later), and type for unions, intersections, and one-offs. Pick one style and stay consistent within a project.
Q: Do I always have to write the generic on useState?
No — TypeScript infers it from the initial value, so useState(0) is a number automatically. Add the explicit <T> only when the initial value can't describe the real type: empty arrays (useState<string[]>([])) and nullable values (useState<User | null>(null)).
Q: Why does e.target.value sometimes error?
Because the event wasn't typed to a specific element. A plain Event has no value. Type the handler React.ChangeEvent<HTMLInputElement> and TypeScript knows e.target is an input, so .value appears.
Q: Is React.FC wrong?
Not wrong — just out of fashion. It used to silently add a children prop and makes generic components awkward. The React docs now show plain functions with directly-typed props, so prefer function C(props: Props) unless your codebase already standardises on FC.
Mini-Challenge: a typed Counter
No blanks this time — just a brief and an outline. Build it in a real react-ts project, then use the self-check at the bottom to confirm your types are right.
Pro Tips
- 💡 Let inference do the work: only add an explicit <T> when the initial value can't describe the type.
- 💡 Hover, don't memorise: in VS Code, hover over onChange on any JSX element to see the exact event type it expects.
- 💡 Type the children prop as React.ReactNode for any wrapper component — it accepts text, JSX, numbers, and arrays.
- 💡 Prefer plain typed functions over React.FC for new components.
🎉 Lesson Complete
- ✅ Describe props with an interface (or type) and annotate the props parameter
- ✅ useState usually infers; add <T> for empty arrays and nullable values
- ✅ useRef<HTMLInputElement>(null) for DOM, useRef<number>(0) for mutable values
- ✅ Event handlers carry their element type: React.ChangeEvent<HTMLInputElement>, React.MouseEvent
- ✅ children: React.ReactNode accepts any renderable content
- ✅ Prefer plain typed functions over React.FC
Practice quiz
How do you type a React component's props with an interface?
- Annotate the props parameter: function C(props: Props)
- Wrap the function in Props<>
- Pass Props to useState
- Props are inferred from JSX automatically
Answer: Annotate the props parameter: function C(props: Props). Describe the shape once with an interface, then annotate the single props parameter with it.
What does a '?' after a prop name mean in a props interface?
- The prop is private
- The prop is optional
- The prop is readonly
- The prop is a boolean
Answer: The prop is optional. isVip?: boolean makes the prop optional, so callers may omit it.
Why does 'const [items, setItems] = useState([])' cause trouble?
- It throws at runtime
- It is inferred as never[], so setItems(['a']) errors
- It is inferred as any[]
- useState cannot take an array
Answer: It is inferred as never[], so setItems(['a']) errors. An empty array is inferred as never[]; add a generic like useState<string[]>([]).
How do you type state that is null now but holds a User later?
- useState<User>(null)
- useState(null as User)
- useState<User | null>(null)
- useState<User?>(null)
Answer: useState<User | null>(null). useState<User | null>(null) makes the null check necessary and correct.
How do you type a ref to an <input> DOM element?
- useRef<HTMLInputElement>(null)
- useRef(input)
- useRef<Input>(0)
- useRef<DOMElement>(null)
Answer: useRef<HTMLInputElement>(null). Type the ref with the element type and start it at null; React attaches it later.
What type makes 'e.target.value' a known string in a change handler?
- Event
- React.ChangeEvent<HTMLInputElement>
- React.FormEvent
- InputEvent
Answer: React.ChangeEvent<HTMLInputElement>. React.ChangeEvent<HTMLInputElement> tells TypeScript e.target is an input.
Which type accepts any renderable nested content for a children prop?
- React.ReactNode
- string
- JSX.Element only
- any[]
Answer: React.ReactNode. React.ReactNode covers text, JSX, numbers, and arrays - anything renderable.
Why do many teams now avoid React.FC?
- It is deprecated and removed
- It used to add an implicit children prop and makes generics awkward
- It cannot type props at all
- It only works in class components
Answer: It used to add an implicit children prop and makes generics awkward. Plain typed functions are simpler; React.FC once added implicit children and complicates generics.
When must you write an explicit generic on useState?
- Always
- Never
- When the initial value can't describe the real type (empty array, null)
- Only for numbers
Answer: When the initial value can't describe the real type (empty array, null). useState(0) infers number; add <T> when the initial value is ambiguous like [] or null.
What is the golden rule for updating React state immutably?
- Mutate the existing object then setState
- Build and return a brand-new value
- Call setState twice
- Use push to add items
Answer: Build and return a brand-new value. Never mutate existing state; create a new array or object (spread, map) and pass that.
Continue this course
- Previous: Checkpoint: Applied TypeScript
- Next: TypeScript Best Practices — Real-world patterns, strictness settings, and tips for clean TS codebases
- Quick reference: TypeScript cheat sheet