Question
In a React project using TypeScript and Hooks, how can I explicitly type the state returned by useState when destructuring its value and setter?
interface IUser {
name: string;
}
const [user, setUser] = useState({ name: "Jon" });
I want user to have the type IUser. I also want setUser to be typed as a function that accepts an IUser value and returns nothing. Type inference works in this simple example, but how can I explicitly enforce the state type—especially when the initial value may depend on props?
Short Answer
You will learn how React's useState generic type parameter defines the type of both the state value and its setter. You will also see when TypeScript inference is enough, when an explicit type is useful, and how to model state that starts as null or is updated from previous state.
Concept
useState is a generic React Hook. A generic is a type placeholder that lets one function work safely with many kinds of data.
For an object state value, provide the desired type in angle brackets immediately after useState:
const [user, setUser] = useState<IUser>({ name: "Jon" });
This declaration gives both destructured variables their types:
userisIUser.setUseris React's state setter forIUser.
The setter is more flexible than a simple (user: IUser) => void function. It accepts either:
- A new
IUservalue. - An updater function that receives the previous
IUserand returns the nextIUser.
Conceptually, its type is:
type SetUser = React.Dispatch<React.SetStateAction<IUser>>;
That expands roughly to:
type SetUser = (next: IUser | ((previous: IUser) => IUser)) => void;
This matters because React can safely update state based on the latest previous value, even when multiple updates are scheduled.
In the original example, TypeScript can infer { name: string } from the initial object. Because TypeScript uses structural typing, that inferred object is compatible with IUser. Still, writing useState<IUser>(...) documents the intended state shape and makes TypeScript check the initializer against that interface.
Mental Model
Think of useState as a labelled storage box plus a replacement tool.
const [user, setUser] = useState<IUser>({ name: "Jon" });
IUseris the label on the box: only values shaped like a user belong in it.useris whatever is currently inside the box.setUseris the tool React gives you to replace the box's contents.
The label applies to both parts of the destructuring assignment. You do not need to label user and setUser separately, because useState<IUser> defines the complete pair.
Syntax and Examples
Use a generic type argument after useState:
const [state, setState] = useState<StateType>(initialValue);
For a user object:
import { useState } from "react";
interface IUser {
name: string;
}
function Profile() {
const [user, setUser] = useState<IUser>({ name: "Jon" });
function renameUser() {
setUser({ name: "Ada" });
}
return <p>{user.name}</p>;
}
TypeScript checks the initial value and later replacements:
setUser({ name: "Grace" }); // Valid
// setUser({ age: 32 });
// Error: property 'name' is missing from the value.
You can also use an updater function:
setUser((previousUser) => ({
...previousUser,
name: previousUser.name.toUpperCase(),
}));
The updater receives an IUser and must return an IUser.
Step by Step Execution
Consider this component:
interface IUser {
name: string;
}
function Greeting() {
const [user, setUser] = useState<IUser>({ name: "Jon" });
const handleClick = () => {
setUser((previousUser) => ({
name: `${previousUser.name}!`,
}));
};
return <button onClick={handleClick}>Hello, {user.name}</button>;
}
Execution flow:
- On the first render,
useState<IUser>({ name: "Jon" })stores a user object. userrefers to{ name: "Jon" }for that render.- React provides
setUser, which only accepts anIUseror an updater returningIUser. - When the button is clicked, React calls the updater with the latest state as
previousUser. - The updater returns
{ name: "Jon!" }. - React stores the new object and renders the component again.
- On the next render,
user.nameis"Jon!".
The generic type remains the same throughout the component's lifetime: the state must always match IUser.
Real World Use Cases
Explicitly typed state is useful when state represents a known domain model.
- Current signed-in user: Store an
IUserafter authentication. - API responses: Type a product, order, or dashboard data object returned from an API.
- Forms: Model fields such as name, email, and address with a form interface.
- Component props copied into initial state: Ensure a prop-derived initializer produces the required state shape.
- Selection state: Store a selected record, which often starts with no selected value.
For example, selected user state commonly starts empty:
const [selectedUser, setSelectedUser] = useState<IUser | null>(null);
The | null explicitly says that, before a user is selected, there may be no user object.
Real Codebase Usage
In real applications, developers usually type state at the useState call rather than typing destructured variables separately.
Use inference for obvious primitive values
const [isOpen, setIsOpen] = useState(false);
const [page, setPage] = useState(1);
Here, TypeScript infers boolean and number clearly.
Use explicit types for domain objects
interface IUser {
id: string;
name: string;
email: string;
}
const [user, setUser] = useState<IUser>(initialUser);
Model loading or absent data honestly
const [user, setUser] = useState<IUser | null>(null);
if (user === null) {
return <p>Loading user...</p>;
}
return <p>{user.name}</p>;
The if statement is a guard clause. After it, TypeScript knows user is an IUser.
Update object state without losing fields
State setters replace the entire state value; they do not automatically merge object fields.
setUser((previousUser) => ({
...previousUser,
name: "Ada",
}));
This pattern preserves existing fields while changing one field.
Common Mistakes
Typing the variables in a separate declaration
Avoid separating the state Hook from its destructuring assignment:
let user: IUser;
let setUser: any;
[user, setUser] = useState({ name: "Jon" });
This loses React's precise setter type by using any, and it is not how Hooks are intended to be declared. Type the Hook instead:
const [user, setUser] = useState<IUser>({ name: "Jon" });
Typing the setter too narrowly
This type prevents valid functional updates:
// Too narrow for a React state setter
let setUser: (user: IUser) => void;
React setters also accept updater functions. Let useState infer the setter type, or use React.Dispatch<React.SetStateAction<IUser>> only when you genuinely need to pass the setter as a separately typed prop.
Forgetting null in initially empty state
This is invalid because null is not an IUser:
// const [user, setUser] = useState<IUser>(null);
Use a union:
const [user, setUser] = useState<IUser | null>(null);
Replacing an object with a partial object
Given this interface:
interface IUser {
name: string;
email: string;
}
This fails because it replaces the whole object and omits email:
// setUser({ name: "Ada" });
Preserve the other fields with an updater:
setUser((previousUser) => ({ ...previousUser, name: "Ada" }));
Assuming a type assertion validates data
Avoid using an assertion to silence errors:
const [user, setUser] = useState<IUser>({} as IUser);
This tells TypeScript to trust you even though the runtime object has no name. Prefer a valid initial value or IUser | null while data is unavailable.
Comparisons
| Approach | Resulting state type | Best use |
|---|---|---|
useState({ name: "Jon" }) | Inferred as { name: string } | Small, obvious local state |
useState<IUser>({ name: "Jon" }) | IUser | State with a named domain model |
useState<IUser | null>(null) | IUser or null | Data loaded later or optional selection |
useState({} as IUser) | Claimed as IUser | Usually avoid; it can hide missing data |
interface versus inferred object type
const [user, setUser] = useState({ name: "Jon" });
Inference creates a type based on this particular value: { name: string }. An interface gives that shape a reusable name and can include fields that the initial value does not naturally communicate.
Direct value versus functional update
setUser({ name: "Ada" });
Use a direct value when you already have the complete next object.
setUser((previousUser) => ({
...previousUser,
name: previousUser.name.toUpperCase(),
}));
Use a functional update when the next value depends on the previous state.
Cheat Sheet
// Explicit object state type
const [user, setUser] = useState<IUser>({ name: "Jon" });
// State that may not exist yet
const [user, setUser] = useState<IUser | null>(null);
// Replace state with a complete value
setUser({ name: "Ada" });
// Calculate next state from previous state
setUser((previousUser) => ({
...previousUser,
name: "Ada",
}));
- Write the type as
useState<Type>(initialValue). - TypeScript infers both the state value and setter from that one generic.
- A React setter accepts a value or an updater function.
- Object state updates replace the object; spread old fields when changing only part of it.
- Use
Type | nullorType | undefinedwhen no valid initial object exists. - Avoid
anyandas Typemerely to bypass missing-property errors.
FAQ
How do I type useState with an interface in TypeScript?
Pass the interface as the useState generic:
const [user, setUser] = useState<IUser>({ name: "Jon" });
Do I always need to write a type for useState?
No. TypeScript can infer simple initial values such as strings, numbers, booleans, and simple objects. Add an explicit type when a named model improves clarity or when the initial value does not fully represent the state type.
What type does a React state setter have?
For IUser state, it is React.Dispatch<React.SetStateAction<IUser>>. It can receive an IUser or a function from the previous IUser to the next one.
Why can setUser accept a function?
A functional update lets React provide the latest previous state. It is safer whenever the new value is calculated from existing state.
How do I initialize typed state as empty?
Use a union with null:
const [user, setUser] = useState<IUser | null>(null);
Check for null before reading user.name.
Does setUser({ name: "Ada" }) merge object fields?
No. The setter replaces the entire state value. Use the spread operator in a functional update to retain fields you are not changing.
Should I use as IUser for an empty initial object?
Usually no. An assertion can claim an invalid value is an IUser, which risks runtime errors. Use a complete object or nullable state instead.
Mini Project
Description
Build a small profile editor that stores a nullable typed user in React state. It demonstrates explicit useState generics, safe handling of data that has not loaded yet, and immutable object updates.
Goal
Create a component that loads a user, displays the user's name, and updates the name safely.
Requirements
- Define an
IUserinterface withid,name, andemailfields. - Initialize the user state as
null. - Add a button that loads a valid user object into state.
- Show a loading message while no user exists.
- Add a button that changes only the loaded user's name.
- Use a functional state update for the name change.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.