← React & TypeScript

React con TypeScript

¿Cómo se tipa `useState` en TypeScript? ¿Siempre hay que pasarle el tipo?

Ver respuesta — intenta responderla en voz alta primero

No siempre. Si el valor inicial es un primitivo, TypeScript infiere el tipo solo. El genérico explícito hace falta cuando el valor inicial no representa todos los valores posibles, como useState<Usuario | null>(null) o useState<Tarea[]>([]).

useState es un hook genérico: useState<T>(). TypeScript intenta deducir T a partir del valor inicial que le pasas.

  • useState(0) infiere number, useState("hola") infiere string, useState(false) infiere boolean. En estos casos no escribas el genérico: es ruido.
  • El problema aparece cuando el valor inicial "se queda corto". Si empiezas con null porque el usuario aún no cargó, useState(null) infiere que el estado es null para siempre, y no podrás guardarle un usuario después.
  • Lo mismo con arrays vacíos: useState([]) infiere never[], un array donde no cabe nada. Necesitas decirle qué va a contener: useState<Tarea[]>([]). La regla práctica: usa el genérico cuando el estado puede tener más tipos de los que muestra el valor inicial.
  • Con useRef pasa parecido: para referencias al DOM usa useRef<HTMLInputElement>(null), y TypeScript sabrá que ref.current es HTMLInputElement | null.
import { useState, useRef } from "react";

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

function Panel() {
  // Inferencia automática: no hace falta el genérico
  const [contador, setContador] = useState(0);  // number
  const [busqueda, setBusqueda] = useState(""); // string
  // Genérico necesario: empieza en null, luego será un Usuario
  const [usuario, setUsuario] = useState<Usuario | null>(null);
  // Genérico necesario: [] solo infiere never[]
  const [tareas, setTareas] = useState<Tarea[]>([]);
  // useRef para el DOM: current es HTMLInputElement | null
  const inputRef = useRef<HTMLInputElement>(null);

  function agregarTarea() {
    setTareas([...tareas, { id: Date.now(), titulo: busqueda, completada: false }]);
    setBusqueda("");
    inputRef.current?.focus(); // optional chaining porque puede ser 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>
  );
}

Empezar un estado con null o [] sin genérico y estrellarse al actualizarlo:

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'

La corrección es declarar el tipo completo del estado:

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

"Cuando el valor inicial es un primitivo, dejo que TypeScript infiera el tipo: useState(0) ya sabe que es number. El genérico lo escribo cuando el valor inicial no cuenta toda la historia: si empiezo con null porque los datos aún no cargan, uso useState<Usuario | null>(null), y si empiezo con un array vacío uso useState<Tarea[]>([]), porque [] solo se infiere como never[]. Con useRef para el DOM hago lo mismo: useRef<HTMLInputElement>(null), y así current queda bien tipado."

Reto rápido

Tipa el estado de un componente que guarda el producto seleccionado (puede no haber ninguno) y la lista de favoritos (ids numéricos), y agrega un favorito.

Ver solución
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>
  );
}