How to Type useState in React with TypeScript

Type useState in React with TypeScript by letting inference work, or pass a type argument for unions, objects, and arrays.

5 min read

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.

App.tsxApp.tsx
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.

App.tsxApp.tsx
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.

App.tsxApp.tsx
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.

App.tsxApp.tsx
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.

App.tsxApp.tsx
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.

App.tsxApp.tsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Do I always need a type argument for useState?

No. TypeScript infers the type from the initial value, so useState(false) already knows it holds a boolean.

When do I pass useState<Type>()?

When the value can be several types, like a union, or when the initial value is an empty array that needs an element type.

How do I type an empty array?

Pass the element type explicitly, such as useState<string[]>([]). Otherwise TypeScript infers never[] and rejects later items.

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.