Pular para o conteúdo
TypeScript

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

  1. Crie interfaces pro shape da response — Olhe a documentação da API ou inspecione o retorno. Cada campo vira propriedade tipada na interface.
  2. Crie um wrapper de response padrão — APIs retornam padrão { data, error, message }. Crie um ApiResponse<T> genérico pra cobrir isso.
  3. Tipar o fetch com generic — Crie uma função fetchTyped<T>(url) que retorna Promise<T> já tipada.
  4. Tipar loading, error e data no state — Use interface ou union type pra representar os 3 estados: loading, success e error.
  5. Propagar o tipo pra componentes — O dado tipado que veio da API vai direto pras props dos componentes. Sem cast, sem 'as any'.

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.

// 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

  • Interfaces criadas pra cada entidade retornada pela API
  • ApiResponse genérico criado pro padrão de response
  • Fetch wrapper tipado com generic (fetchApi)
  • Axios configurado com generics nas chamadas
  • Estado de loading/error/success com discriminated union
  • Campos opcionais marcados com | null na interface
  • Body do POST/PUT tipado com interface própria
  • Erro tratado com classe customizada (ApiError)
  • Tipo propagado pros componentes sem cast manual

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.