Question

Typing React useState with TypeScript

typescriptreactjsreact-hooks

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:

  • user is IUser.
  • setUser is React's state setter for IUser.

The setter is more flexible than a simple (user: IUser) => void function. It accepts either:

  1. A new IUser value.
  2. An updater function that receives the previous IUser and returns the next IUser.

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" });
  • IUser is the label on the box: only values shaped like a user belong in it.
  • user is whatever is currently inside the box.
  • setUser is 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.

Take Quiz

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:

  1. On the first render, useState<IUser>({ name: "Jon" }) stores a user object.
  2. user refers to { name: "Jon" } for that render.
  3. React provides setUser, which only accepts an IUser or an updater returning IUser.
  4. When the button is clicked, React calls the updater with the latest state as previousUser.
  5. The updater returns { name: "Jon!" }.
  6. React stores the new object and renders the component again.
  7. On the next render, user.name is "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 IUser after 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.

Take Quiz

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

ApproachResulting state typeBest use
useState({ name: "Jon" })Inferred as { name: string }Small, obvious local state
useState<IUser>({ name: "Jon" })IUserState with a named domain model
useState<IUser | null>(null)IUser or nullData loaded later or optional selection
useState({} as IUser)Claimed as IUserUsually 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.

Take Quiz

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 | null or Type | undefined when no valid initial object exists.
  • Avoid any and as Type merely 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.

Take Quiz

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 IUser interface with id, name, and email fields.
  • 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.
Take Quiz

Keep learning

Related questions

View more