TypeScript with React: Typing Props & State
Most people learn TypeScript to use it with React. Here is the bridge: how to type a component’s props and its useState hook.
What you will learn
- Type a function component’s props with an interface
- Type the useState hook correctly
- Type an event handler in a React component
Why React + TypeScript is the common pairing
React components pass data around as props (the inputs a component receives) and hold changing values as state. Without types it is easy to pass the wrong prop or misuse state. TypeScript makes both safe and gives you autocomplete inside your components. This lesson is a bridge — our full React course goes deeper, but here is the core typing you need.
These snippets are .tsx files — TSX is TypeScript with JSX (the HTML-like syntax React uses). They run after a build step (Vite, Next.js, etc.), so we show them with their rendered Output.
Typing props with an interface
Describe a component’s props with an interface, then annotate the function’s parameter with it. TypeScript now checks every place the component is used.
interface GreetingProps {
name: string;
excited?: boolean; // optional prop
}
function Greeting(props: GreetingProps) {
return <h1>Hello, {props.name}{props.excited ? '!' : '.'}</h1>;
}
// Using it:
// <Greeting name="Asha" excited /> works
// <Greeting name={42} /> error: name must be string
// <Greeting excited /> error: name is requiredNote: Output (rendered):
Hello, Asha!
GreetingProps makes name required and excited optional. Using <Greeting name={42} /> is caught — name must be a string. Forgetting name entirely is also caught. The component’s contract is now enforced everywhere it is used.
Destructuring props (the common style)
Most React code pulls the props apart in the parameter list. The type goes on the destructured object:
interface ButtonProps {
label: string;
onClick: () => void; // a function that takes nothing, returns nothing
}
function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}Note: Output (rendered):
A clickable button showing the label text.
The onClick prop is typed () => void, so passing something that is not a function is an error. Destructuring { label, onClick } is the same props object — just unpacked for convenience.
Typing useState
The useState hook holds a piece of state. TypeScript usually infers the type from the initial value, so useState(0) is already a number. You write the type explicitly with the <...> angle brackets when the initial value alone is not enough — most often when state can also be null.
import { useState } from 'react';
function Counter() {
// Inferred: count is number, setCount takes a number
const [count, setCount] = useState(0);
// Explicit: a user that starts as null
const [user, setUser] = useState<string | null>(null);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}Note: Output (rendered):
A button reading "Clicked 0 times" that counts up on each click.
useState(0) infers count as number, so setCount('x') would error. useState<string | null>(null) needs the explicit type because null alone does not tell TypeScript the eventual value is a string. The angle brackets pass that type in — just like any generic.
Typing an event handler
Event handlers receive a typed event object. React provides the event types; you annotate the parameter so e.target.value and friends are checked.
import { useState } from 'react';
function NameInput() {
const [name, setName] = useState('');
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setName(e.target.value); // e.target.value is typed as string
}
return <input value={name} onChange={handleChange} />;
}Note: Output (rendered):
A text box whose value updates as you type.
React.ChangeEvent<HTMLInputElement> types the event so e.target.value is a known string. Without the type, e would be any and a typo like e.target.valeu would slip through.
The React-typing essentials
| You are typing... | Use |
|---|---|
| Component props | An interface on the function parameter |
| A number/string/boolean state | Let useState infer it |
| State that can be null | Explicit useState<T | null>(null) |
| A change event | React.ChangeEvent<HTMLInputElement> |
| A click event | React.MouseEvent<HTMLButtonElement> |
Tip: Two habits cover most React typing: define a ...Props interface for every component, and let useState infer unless the value can be null/empty (then pass the type explicitly). Our full React course builds on exactly this foundation.
Watch out: A very common mistake: useState(null) with no type. TypeScript infers the type as just null, so you can never set anything else into it. When state starts empty, give it the real type up front: useState<User | null>(null).
Q. You write const [user, setUser] = useState(null) for state that will later hold a User object. What goes wrong?
✍️ Practice
- Write a
Cardcomponent with aCardPropsinterface (title: string,subtitle?: string) and render it. - Make a
Togglecomponent that usesuseState(false)and flips on click; confirm the inferred boolean type.
🏠 Homework
- Build a small typed
TodoItemcomponent: aTodoItemPropsinterface (id: number, text: string, done: boolean, onToggle: () => void). Note which props are required and how the onToggle type prevents passing a non-function.