¿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)infierenumber,useState("hola")infierestring,useState(false)infiereboolean. En estos casos no escribas el genérico: es ruido.- El problema aparece cuando el valor inicial "se queda corto". Si empiezas con
nullporque el usuario aún no cargó,useState(null)infiere que el estado esnullpara siempre, y no podrás guardarle un usuario después. - Lo mismo con arrays vacíos:
useState([])infierenever[], 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
useRefpasa parecido: para referencias al DOM usauseRef<HTMLInputElement>(null), y TypeScript sabrá queref.currentesHTMLInputElement | 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 esnumber. El genérico lo escribo cuando el valor inicial no cuenta toda la historia: si empiezo connullporque los datos aún no cargan, usouseState<Usuario | null>(null), y si empiezo con un array vacío usouseState<Tarea[]>([]), porque[]solo se infiere comonever[]. ConuseRefpara el DOM hago lo mismo:useRef<HTMLInputElement>(null), y asícurrentqueda bien tipado."
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>
);
}