Como Tipar useState no TypeScript
useState tipado: inferência, união e estado inicial null com tipo explícito.
Por que isso é importante
Tipar useState no TypeScript muitas vezes é inferência — até o estado começar null. Aí você declara a união e evita bug silencioso.
Inferência vs generic explícito
O TypeScript é esperto: quando você passa um valor inicial pro useState, ele infere o tipo automaticamente. <code>useState(0)</code> vira <code>number</code>, <code>useState("")</code> vira <code>string</code>, <code>useState(false)</code> vira <code>boolean</code>. Pra tipos primitivos simples, a inferência resolve. Não precisa declarar nada.
Mas a inferência falha em três cenários: quando o valor inicial é <code>null</code> ou <code>undefined</code>, quando o state pode ter mais de um tipo (union), e quando o state é um objeto que começa vazio ou parcial. Nesses casos, você precisa do generic explícito: <code>useState<Tipo>(valorInicial)</code>.
Dá pra pensar assim: se o valor inicial já representa todos os formatos possíveis do state, deixa o TypeScript inferir. Se não representa, declare o tipo. Simples assim.
// Inferência automática - funciona perfeito
const [count, setCount] = useState(0); // number
const [nome, setNome] = useState(""); // string
const [ativo, setAtivo] = useState(true); // boolean
const [tags, setTags] = useState(["react"]); // string[]
// Generic explícito - necessário aqui
const [usuario, setUsuario] = useState<Usuario | null>(null);
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
const [dados, setDados] = useState<Produto[]>([]);
Passo a passo: tipando useState em cada cenário
useState(valorInicial) sem generic.useState<Tipo | null>(null). Isso força check de null antes de acessar propriedades do state.useState<Item[]>([]). Sem o generic, array vazio infere como never[] e trava qualquer push.useState<MeuState>({ campo1: "", campo2: 0 }).useState<"idle" | "loading" | "success" | "error">("idle"). Cada set só aceita valores válidos.Exemplos práticos: do simples ao avançado
State com objeto complexo
Quando o state é um objeto, crie uma interface separada. Isso torna o código legível e deixa o set recusar campos errados ou faltando.
interface Formulario {
nome: string;
email: string;
idade: number;
newsletter: boolean;
}
function CadastroForm() {
const [form, setForm] = useState<Formulario>({
nome: "",
email: "",
idade: 0,
newsletter: false,
});
const atualizarCampo = <K extends keyof Formulario>(
campo: K,
valor: Formulario[K]
) => {
setForm((prev) => ({ ...prev, [campo]: valor }));
};
// TypeScript valida campo e valor
atualizarCampo("nome", "Maria"); // OK
atualizarCampo("idade", 28); // OK
// atualizarCampo("idade", "vinte"); // Erro! Esperava number
}
State null com dados de API
Galera, esse é o cenário mais comum em projetos reais: o state começa null porque os dados ainda não chegaram da API. O TypeScript te obriga a checar antes de renderizar -- e isso evita aquele crash que só aparece em produção.
interface Produto {
id: number;
nome: string;
preco: number;
}
function PaginaProduto({ produtoId }: { produtoId: number }) {
const [produto, setProduto] = useState<Produto | null>(null);
const [carregando, setCarregando] = useState(true);
useEffect(() => {
fetch(`/api/produtos/${produtoId}`)
.then((res) => res.json())
.then((data: Produto) => {
setProduto(data);
setCarregando(false);
});
}, [produtoId]);
if (carregando) return <p>Carregando...</p>;
if (!produto) return <p>Produto não encontrado</p>;
// Aqui TypeScript sabe que produto não é null
return (
<div>
<h1>{produto.nome}</h1>
<p>R$ {produto.preco.toFixed(2)}</p>
</div>
);
}
Lazy initialization
Quando o valor inicial é caro de calcular (ler do localStorage, processar dados grandes), use a forma de função no useState. O TypeScript infere o tipo a partir do retorno da função.
interface Preferencias {
tema: "claro" | "escuro";
idioma: string;
notificacoes: boolean;
}
function usePreferencias() {
// Função executada só na primeira renderização
const [prefs, setPrefs] = useState<Preferencias>(() => {
const salvo = localStorage.getItem("prefs");
if (salvo) {
return JSON.parse(salvo) as Preferencias;
}
return {
tema: "escuro",
idioma: "pt-BR",
notificacoes: true,
};
});
return { prefs, setPrefs };
}
Erros comuns ao tipar useState
Armadilhas recorrentes
Não declarar null no tipo: useState(null) sem generic infere como null puro. Depois não aceita nenhum outro valor no set. Sempre use useState<Tipo | null>(null).
Array vazio sem generic: useState([]) infere never[]. Qualquer push ou set com dados falha. Declare useState<Item[]>([]).
Usar as em vez de generic: useState(valor as Tipo) mascara erros. O generic useState<Tipo>(valor) valida o valor inicial contra o tipo.
State gigante sem interface: objeto com 10+ campos sem interface vira caos. Crie uma interface dedicada pra cada state complexo.
Atualizar state parcial sem spread: setForm({ nome: 'Ana' }) perde os outros campos. Use setForm(prev => ({ ...prev, nome: 'Ana' })).
Checklist: useState tipado corretamente
Checklist de useState + TypeScript
Domine React + TypeScript na prática
useState bem tipado é o começo de um projeto React sólido. No CrazyStack, você constrói aplicações completas onde cada hook, cada estado e cada componente tem tipagem profissional. Aprenda construindo -- não decorando regras.