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
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<User[]> 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 <T>, 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
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.