Como Tipar API Response no React com TypeScript
API sem tipagem retorna 'any' e o bug aparece só em produção. Aprenda a tipar responses com interfaces, fetch genérico, Axios e error handling completo.
Por que isso é importante
Como Tipar API Response no React com TypeScript. API sem tipagem retorna 'any' e o bug aparece só em produção. Aprenda a tipar responses com interfaces, fetch genérico, Axios e error handling completo.
O problema de não tipar respostas de API
Galera, o fetch e o axios retornam 'any' por padrão. Isso significa que o TypeScript não tem como saber se response.data.userName existe ou se o campo certo era response.data.username. O erro só aparece em runtime — tarde demais.
A solução é criar interfaces que descrevem exatamente o que a API retorna e usar essas interfaces na chamada. Dá pra fazer com fetch nativo, com Axios ou com qualquer client HTTP. O conceito é o mesmo: o dado que entra no front precisa ter tipo definido.
Passo a passo: tipando respostas de API
Interface de response: o contrato da API
O primeiro passo é mapear o que a API retorna. Se ela retorna um usuário, crie a interface User. Se retorna uma lista paginada, crie PaginatedResponse<T>.
// Entidade base
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
// Response padrão da API
interface ApiResponse<T> {
data: T;
message: string;
status: number;
}
// Response paginada
interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
perPage: number;
totalPages: number;
}
// Uso: o tipo se propaga automaticamente
type UserResponse = ApiResponse<User>;
type UserListResponse = PaginatedResponse<User>;
Quando o backend muda um campo, você atualiza a interface e o TypeScript mostra todos os lugares do front que precisam de ajuste. Zero surpresa.
Fetch genérico tipado
Criar um wrapper pro fetch nativo com generic garante que toda chamada retorna o tipo certo. Nada de .json() retornando 'any'.
// Erro customizado da API
class ApiError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
this.name = "ApiError";
}
}
// Fetch tipado genérico
async function fetchApi<T>(
url: string,
options?: RequestInit
): Promise<T> {
const res = await fetch(url, {
headers: { "Content-Type": "application/json" },
...options,
});
if (!res.ok) {
throw new ApiError(res.status, `Erro HTTP: ${res.status}`);
}
const data: T = await res.json();
return data;
}
// Uso:
const user = await fetchApi<User>("/api/users/1");
// user é do tipo User — completo, tipado, seguro
const users = await fetchApi<PaginatedResponse<User>>(
"/api/users?page=1"
);
// users.data é User[], users.total é number
Axios com generics: tipagem nativa
O Axios já suporta generics direto no método. Você passa o tipo e o response.data já vem tipado. Dá pra ir além e criar uma instância configurada.
import axios, { AxiosResponse } from "axios";
// Instância configurada
const api = axios.create({
baseURL: "https://api.exemplo.com",
timeout: 10000,
});
// GET tipado
async function getUser(id: number): Promise<User> {
const { data } = await api.get<User>(`/users/${id}`);
return data; // data já é User
}
// POST tipado: envia CreateUser, recebe User
interface CreateUser {
name: string;
email: string;
}
async function createUser(payload: CreateUser): Promise<User> {
const { data } = await api.post<User>("/users", payload);
return data;
}
// GET com response paginada
async function listUsers(
page: number
): Promise<PaginatedResponse<User>> {
const { data } = await api.get<PaginatedResponse<User>>(
`/users?page=${page}`
);
return data;
}
O Axios cuida do parse do JSON e o generic cuida do tipo. Resultado: menos código, mais segurança.
Tipando estados de loading, error e success
O estado de uma chamada de API sempre passa por 3 fases: carregando, sucesso ou erro. Dá pra modelar isso com discriminated union — o TypeScript sabe exatamente em qual fase você tá.
// Discriminated union pros estados
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
// Uso no componente
function UserProfile({ id }: { id: number }) {
const [state, setState] =
useState<AsyncState<User>>({ status: "idle" });
useEffect(() => {
setState({ status: "loading" });
fetchApi<User>(`/api/users/${id}`)
.then((data) =>
setState({ status: "success", data })
)
.catch((err) =>
setState({ status: "error", error: err.message })
);
}, [id]);
if (state.status === "loading") return <p>Carregando...</p>;
if (state.status === "error") return <p>{state.error}</p>;
if (state.status === "success") {
// Aqui state.data é User — garantido pelo TS
return <h1>{state.data.name}</h1>;
}
return null;
}
Com discriminated union, o TypeScript faz narrowing automático. Dentro do if 'success', o state.data existe com certeza. Sem cast, sem optional chaining desnecessário.
Erros comuns ao tipar respostas de API
Usar 'as' pra forçar tipo: res.json() as User esconde erros. Prefira generic na função de fetch.
Não tratar error como tipo específico: catch(err: any) perde informação. Use instanceof pra narrowing.
Confiar que a API retorna exatamente a interface: runtime e compile-time são coisas diferentes. Valide com Zod quando a fonte não é confiável.
Esquecer campos opcionais: se a API pode retornar null em algum campo, declare como string | null na interface.
Não tipar o body do POST: tipar só a response e enviar body sem tipo é proteger metade e deixar a outra exposta.
Checklist: API response tipada no React
Checklist Final
Integre API + TypeScript em projeto completo
Tipar respostas de API é só o começo. No CrazyStack, você constrói o backend que gera essas responses E o frontend que consome — tudo em TypeScript. Os tipos são compartilhados entre camadas, garantindo que front e back falam a mesma língua.