Pular para o conteúdo
TypeScript

Como Tipar Custom Hook no React com TypeScript

Custom hooks sem tipagem certa viram caixa preta. Aprenda a tipar retornos, generics, tuples e callbacks pra criar hooks reutilizáveis e seguros.

Por que isso é importante

Como Tipar Custom Hook no React com TypeScript. Custom hooks sem tipagem certa viram caixa preta. Aprenda a tipar retornos, generics, tuples e callbacks pra criar hooks reutilizáveis e seguros.

Custom hooks e TypeScript: a combinação que faz diferença

Galera, custom hook nada mais é do que uma função que começa com 'use' e chama outros hooks por dentro. A diferença quando você adiciona TypeScript é que o retorno fica explícito: quem importar o hook já sabe o tipo de cada valor, cada função, cada estado.

Sem tipagem, o retorno vira um 'any' implícito e toda aquela segurança que o TS oferece desaparece. Com tipagem, o hook vira um contrato: previsível, testável e fácil de manter.

Passo a passo: criando hooks tipados

  1. Defina o tipo de retorno explicitamente — Crie uma interface ou type alias pro que o hook retorna. Isso documenta e protege ao mesmo tempo.
  2. Tipar parâmetros de entrada — Se o hook recebe config, URL, callback, tipe cada parâmetro. Sem 'any'.
  3. Use generics quando o tipo varia — Um hook de fetch que busca User, Product ou Order precisa de generic, não de tipo fixo.
  4. Retorne tuple quando fizer sentido — Padrão [valor, setter] como useState. Use 'as const' pra TypeScript inferir tupla e não array.
  5. Tipar callbacks internos — Funções que o hook expõe (reset, refetch, toggle) precisam de tipo explícito no retorno.

Exemplo: hook simples com retorno tipado

O hook mais básico: useToggle. Retorna o estado booleano e uma função pra alternar. Veja como o tipo de retorno fica explícito.

import { useState, useCallback } from "react";

interface UseToggleReturn {
  value: boolean;
  toggle: () => void;
  setTrue: () => void;
  setFalse: () => void;
}

function useToggle(initial = false): UseToggleReturn {
  const [value, setValue] = useState(initial);

  const toggle = useCallback(() => setValue((v) => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return { value, toggle, setTrue, setFalse };
}

// Uso:
const { value: isOpen, toggle } = useToggle();
// isOpen é boolean, toggle é () => void

Tuple return: padrão useState-style

Quando o hook segue o padrão do useState — retorna [valor, setter] — você precisa do 'as const' pra TypeScript entender que é uma tupla e não um array genérico.

function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);

  const increment = useCallback(() => setCount((c) => c + 1), []);
  const decrement = useCallback(() => setCount((c) => c - 1), []);
  const reset = useCallback(() => setCount(initial), [initial]);

  // Sem 'as const': TS infere (number | Function)[]
  // Com 'as const': TS infere [number, ...functions]
  return [count, increment, decrement, reset] as const;
}

// Uso:
const [count, increment, decrement, reset] = useCounter(10);
// count: number, increment: () => void

Sem o 'as const', ao desestruturar, o TypeScript acha que qualquer posição pode ser number ou function. Com 'as const', cada posição tem seu tipo exato.

Hook genérico: useFetch com TypeScript

Esse é o hook que todo projeto precisa. Ele busca dados de uma API e retorna data, loading e error — tudo tipado com generic pra funcionar com qualquer entidade.

import { useState, useEffect, useCallback } from "react";

interface UseFetchReturn<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

function useFetch<T>(url: string): UseFetchReturn<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json: T = await res.json();
      setData(json);
    } catch (err) {
      setError(
        err instanceof Error ? err.message : "Erro desconhecido"
      );
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

// Uso:
interface User {
  id: number;
  name: string;
  email: string;
}

const { data, loading, error } = useFetch<User[]>("/api/users");
// data é User[] | null — tipado perfeitamente

Repare que o generic T propaga pra todo o retorno. Quem chama useFetch sabe que data vai ser User[] | null. Autocomplete funciona, refatoração segura.

Hook com callback tipado

Hooks que recebem callbacks como parâmetro precisam tipar a assinatura da função. Isso garante que quem usa o hook passe a função certa.

interface UseDebounceOptions<T> {
  value: T;
  delay: number;
  onChange?: (value: T) => void;
}

function useDebounce<T>({
  value,
  delay,
  onChange,
}: UseDebounceOptions<T>): T {
  const [debounced, setDebounced] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounced(value);
      onChange?.(value);
    }, delay);
    return () => clearTimeout(timer);
  }, [value, delay, onChange]);

  return debounced;
}

// Uso:
const debouncedSearch = useDebounce({
  value: searchTerm,
  delay: 300,
  onChange: (val) => console.log(val), // val tipado como string
});

Erros comuns ao tipar custom hooks

Retornar array sem 'as const': o TypeScript perde a posição de cada tipo na tupla. Sempre use 'as const' pra retornos do tipo [valor, setter].

Não tipar o generic do useFetch: sem o , o retorno vira 'any' e a tipagem toda se perde.

Esquecer de tipar o error como string | null: tratar error como 'any' esconde o tipo real do problema.

Callback sem assinatura: receber 'Function' como tipo é quase igual a 'any'. Defina (params: Tipo) => RetornoTipo.

Não exportar o tipo de retorno: quem importa o hook precisa do tipo pra usar em props e estados derivados.

Checklist: custom hook tipado no React

Checklist Final

  • Interface ou type criado pro retorno do hook
  • Parâmetros de entrada tipados (sem any)
  • Generic aplicado quando tipo varia entre usos
  • Tuple com 'as const' quando retorno é [valor, setter]
  • Callbacks internos com assinatura explícita
  • Tipo de retorno exportado junto com o hook
  • Estados internos tipados (useState)
  • Error tipado como string | null (não any)
  • Hook testado com diferentes tipos no generic

Domine hooks profissionais com TypeScript

Custom hooks tipados são a base de qualquer projeto React profissional. No CrazyStack, você constrói hooks reais — fetch, auth, form, cache — todos com tipagem completa, usados em projeto de produção com Node.js e React.