Pular para o conteúdo
TypeScript

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. useState(0) vira number, useState("") vira string, useState(false) vira boolean. 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 é null ou undefined, 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: useState<Tipo>(valorInicial).

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

  1. Tipos primitivos: confie na inferência — Para string, number, boolean, a inferência do TypeScript é precisa. Declare useState(valorInicial) sem generic.
  2. State que começa null: use union com generic — useState<Tipo | null>(null). Isso força check de null antes de acessar propriedades do state.
  3. Arrays tipados: declare o tipo do item — useState<Item[]>([]). Sem o generic, array vazio infere como never[] e trava qualquer push.
  4. Objetos complexos: crie interface dedicada — Defina a interface do state, depois use: useState<MeuState>({ campo1: "", campo2: 0 }).
  5. Union state para máquinas de estado — 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(null).

Array vazio sem generic: useState([]) infere never[]. Qualquer push ou set com dados falha. Declare useState([]).

Usar as em vez de generic: useState(valor as Tipo) mascara erros. O generic useState(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

  • Tipos primitivos simples usam inferência (sem generic)
  • State que começa null declara Tipo | null no generic
  • Arrays vazios declaram o tipo do item no generic
  • Objetos complexos têm interface dedicada
  • Union state usa literais para máquinas de estado
  • Lazy initialization usa função no valor inicial
  • Atualizações parciais usam spread com callback (prev => ...)
  • Nenhum useState usa any ou as para contornar tipos

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.