← React & TypeScript

React with TypeScript

How do you type `useState` in TypeScript? Do you always have to pass it the type?

Show answer — try answering out loud first

Not always. If the initial value is a primitive, TypeScript infers the type on its own. The explicit generic is needed when the initial value doesn't represent all the possible values, like useState<Usuario | null>(null) or useState<Tarea[]>([]).

useState is a generic hook: useState<T>(). TypeScript tries to deduce T from the initial value you pass it.

  • useState(0) infers number, useState("hola") infers string, useState(false) infers boolean. In these cases don't write the generic: it's noise.
  • The problem shows up when the initial value "falls short". If you start with null because the user hasn't loaded yet, useState(null) infers that the state is null forever, and you won't be able to store a user in it later.
  • Same with empty arrays: useState([]) infers never[], an array where nothing fits. You need to tell it what it will contain: useState<Tarea[]>([]). The practical rule: use the generic when the state can hold more types than the initial value shows.
  • With useRef it's similar: for DOM references use useRef<HTMLInputElement>(null), and TypeScript will know that ref.current is HTMLInputElement | null.
import { useState, useRef } from "react";

type Usuario = { id: number; nombre: string };
type Tarea = { id: number; titulo: string; completada: boolean };

function Panel() {
  // Automatic inference: the generic isn't needed
  const [contador, setContador] = useState(0);  // number
  const [busqueda, setBusqueda] = useState(""); // string
  // Generic needed: starts as null, later it will be a Usuario
  const [usuario, setUsuario] = useState<Usuario | null>(null);
  // Generic needed: [] only infers never[]
  const [tareas, setTareas] = useState<Tarea[]>([]);
  // useRef for the DOM: current is HTMLInputElement | null
  const inputRef = useRef<HTMLInputElement>(null);

  function agregarTarea() {
    setTareas([...tareas, { id: Date.now(), titulo: busqueda, completada: false }]);
    setBusqueda("");
    inputRef.current?.focus(); // optional chaining because it can be null
  }

  return (
    <div>
      <p>{usuario ? `Hola, ${usuario.nombre}` : "Sin sesión"}</p>
      <input ref={inputRef} value={busqueda} onChange={(e) => setBusqueda(e.target.value)} />
      <button onClick={agregarTarea}>Agregar ({tareas.length})</button>
      <button onClick={() => setContador(contador + 1)}>Clics: {contador}</button>
    </div>
  );
}

Starting a state with null or [] without a generic and crashing when updating it:

const [usuario, setUsuario] = useState(null);
setUsuario({ id: 1, nombre: "Ana" });
// Error: Argument of type '{ id: number; nombre: string; }' is not assignable to parameter of type 'SetStateAction<null>'

const [tareas, setTareas] = useState([]);
setTareas([{ id: 1, titulo: "Estudiar", completada: false }]);
// Error: Type '{ id: number; ... }' is not assignable to type 'never'

The fix is to declare the full type of the state:

const [usuario, setUsuario] = useState<Usuario | null>(null);
const [tareas, setTareas] = useState<Tarea[]>([]);

"When the initial value is a primitive, I let TypeScript infer the type: useState(0) already knows it's number. I write the generic when the initial value doesn't tell the whole story: if I start with null because the data hasn't loaded yet, I use useState<Usuario | null>(null), and if I start with an empty array I use useState<Tarea[]>([]), because [] is only inferred as never[]. With useRef for the DOM I do the same: useRef<HTMLInputElement>(null), and that way current is properly typed."

Quick challenge

Type the state of a component that stores the selected producto (there may be none) and the list of favoritos (numeric ids), and add a favorite.

See solution
import { useState } from "react";

type Producto = { id: number; nombre: string; precio: number };

function Tienda() {
  const [producto, setProducto] = useState<Producto | null>(null);
  const [favoritos, setFavoritos] = useState<number[]>([]);

  function agregarFavorito(id: number) {
    setFavoritos([...favoritos, id]);
  }

  return (
    <div>
      <p>{producto ? producto.nombre : "Elige un producto"}</p>
      <button onClick={() => setProducto({ id: 1, nombre: "Teclado", precio: 500 })}>Seleccionar</button>
      <button onClick={() => producto && agregarFavorito(producto.id)}>Favorito ({favoritos.length})</button>
    </div>
  );
}