To type useState in React with TypeScript, let TypeScript infer the type from the initial value, or pass an explicit type argument when the value can be several types. The explicit form is only needed where inference cannot do the job, and simple values never need it.
When to annotate is the only real question. Simple values never need it, and complex values only need one type argument.
Inference handles simple values
TypeScript reads the initial value and types the state from it. No annotation is required for a boolean, a number, or a string, and the type is usually exactly right.
import { useState } from "react";
const [enabled, setEnabled] = useState(false);Here enabled is a boolean, and setEnabled accepts either a boolean or a function that returns one. The same inference covers numbers and strings, so most simple counters and inputs need no type at all. The type flows into the setter automatically, so the wrong kind of value is rejected before the code runs.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}count is a number, and the onClick handler type-checks because count plus one is still a number. TypeScript requires no annotations anywhere in this component.
Pass a type argument for unions
A value that starts as one type and becomes another needs a union. The status of a request is the classic case, because it moves through several values over time.
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");The type argument tells TypeScript that status may be any of the four strings, not just the initial "idle". Later calls to setStatus("success") then type-check correctly. A request can also be modeled as a union of objects, where each branch carries its own fields, but a string union is enough for most UI status.
Values that start empty or null
A value that starts as null but later holds a real value needs a union. TypeScript cannot infer the later type from a null initial value.
const [user, setUser] = useState<User | null>(null);The union tells TypeScript that user is either a User or null. The same pattern types data that arrives from a network request before it has loaded, where null represents the not-yet-loaded state.
Type objects and arrays
An object needs its shape described when the initial value does not make every field clear. This happens when fields start empty but must later hold specific types.
type Person = { name: string; age: number };
const [person, setPerson] = useState<Person>({ name: "", age: 0 });person is now a Person, and setPerson rejects any object missing name or age. Arrays need the element type when they start empty. Without it, TypeScript infers never[] and rejects later items.
const [items, setItems] = useState<string[]>([]);Now items holds strings, and setItems accepts a string array. The same pattern applies to an array of typed objects, such as an array of Person. The explicit type argument only appears when inference is not enough, which is why simple values need nothing while empty arrays and unions do.
Typed object state still follows the immutable update rules, so replacing a field uses the same spread shown in updating objects in state.
Common mistakes
- Letting an empty array infer never[] and then fighting the resulting errors.
- Widening a union to string just to make an initial value fit.
- Adding a type argument that duplicates what inference already knows.
Every file that contains JSX must use the .tsx extension. A .ts file with JSX will not compile.
Type definitions ship in @types/react and @types/react-dom, which a framework setup usually installs for you. Check your editor's hover info on the state variable to see the inferred type when something looks off.
What to learn next
The setter's type comes from the state type, so a functional update like setCount(previous => previous + 1) type-checks with no extra work. Typing state builds on the same generic idea as typing component props, and the useState guide covers the Hook itself.
Rune AI
Key Insights
- TypeScript infers state type from the initial value.
- Pass a type argument for union types.
- Type objects and arrays explicitly when needed.
- An empty array needs an element type.
- File names with JSX use the .tsx extension.
Frequently Asked Questions
Do I always need a type argument for useState?
When do I pass useState<Type>()?
How do I type an empty array?
Conclusion
Let TypeScript infer simple state from the initial value, and pass a type argument for unions, objects, and empty arrays. The explicit form only appears where inference is not enough.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.