Tipando respostas de API com generics
1. O problema de tipar respostas de API
Ao consumir APIs REST, um dos maiores desafios é garantir que os dados retornados sejam tipados corretamente. Sem uma abordagem estruturada, desenvolvedores recorrem frequentemente ao uso de any ou unknown, resultando em perda de segurança de tipos e inferência prejudicada.
Considere o exemplo abaixo, onde uma função de fetch retorna dados sem tipagem adequada:
async function fetchUserData(url: string): Promise<any> {
const response = await fetch(url);
return response.json();
}
// Uso frágil - sem autocomplete ou verificação de tipos
const user = await fetchUserData('/api/users/1');
console.log(user.name); // Nenhum erro em tempo de compilação, mas pode falhar em runtime
Problemas comuns:
- Estruturas de resposta inconsistentes entre endpoints (alguns retornam { data, message, status }, outros apenas o objeto)
- Ausência de tratamento tipado para erros
- Dificuldade em refatorar quando a API muda
2. Fundamentos de Generics em TypeScript
Generics permitem criar componentes que funcionam com vários tipos, mantendo a segurança de tipos. A sintaxe básica utiliza parâmetros de tipo entre <>:
function identity<T>(arg: T): T {
return arg;
}
// Inferência automática
const num = identity(42); // tipo: number
const str = identity("hello"); // tipo: string
Constraints com extends restringem os tipos permitidos:
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength("texto"); // OK
getLength([1, 2, 3]); // OK
// getLength(42); // Erro: number não tem length
3. Criando um tipo genérico para resposta de API
Vamos definir um tipo base que encapsula a estrutura comum de respostas:
interface ApiResponse<T> {
data: T;
message: string;
status: number;
success: boolean;
}
// Uso com diferentes tipos
type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product[]>;
Para suporte a paginação:
interface PaginationMeta {
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: PaginationMeta;
}
// Exemplo: resposta de lista de usuários paginada
type PaginatedUsers = PaginatedResponse<User>;
Lidando com erros de forma mais robusta:
type ApiResult<T, E = string> =
| { success: true; data: T; message: string }
| { success: false; error: E; message: string };
// Uso
type LoginResult = ApiResult<{ token: string }, { code: number; details: string }>;
4. Tipando funções de fetch com generics
Criamos uma função genérica que tipa automaticamente a resposta:
async function fetchData<T>(url: string, options?: RequestInit): Promise<ApiResponse<T>> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result as ApiResponse<T>;
}
// Uso com tipagem explícita
interface User {
id: number;
name: string;
email: string;
}
const userResponse = await fetchData<User>('/api/users/1');
console.log(userResponse.data.name); // Autocomplete funciona!
// Com tratamento de erros genérico
async function fetchWithError<T, E = string>(url: string): Promise<ApiResult<T, E>> {
try {
const response = await fetch(url);
const data = await response.json();
return { success: true, data, message: 'Sucesso' };
} catch (error) {
return { success: false, error: error as E, message: 'Falha na requisição' };
}
}
5. Generics em hooks e custom hooks
Hooks genéricos permitem reutilização com diferentes tipos de dados:
import { useState, useEffect } from 'react';
interface UseApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function useApi<T>(url: string): UseApiState<T> {
const [state, setState] = useState<UseApiState<T>>({
data: null,
loading: true,
error: null
});
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setState({ data: result as T, loading: false, error: null });
} catch (err) {
setState({ data: null, loading: false, error: (err as Error).message });
}
};
fetchData();
}, [url]);
return state;
}
// Hook para listas paginadas
function usePaginatedQuery<T>(baseUrl: string, page: number) {
return useApi<PaginatedResponse<T>>(`${baseUrl}?page=${page}`);
}
// Uso com diferentes tipos
const { data: user } = useApi<User>('/api/users/1');
const { data: products } = usePaginatedQuery<Product>('/api/products', 1);
6. Avançando: validação e transformação de respostas
Bibliotecas como Zod permitem validar dados em runtime com tipos inferidos:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1),
email: z.string().email()
});
type User = z.infer<typeof UserSchema>;
function validateResponse<T>(data: unknown, schema: z.ZodSchema<T>): T {
const result = schema.safeParse(data);
if (!result.success) {
throw new Error(`Validação falhou: ${result.error.message}`);
}
return result.data;
}
// Função de fetch com validação
async function fetchValidated<T>(url: string, schema: z.ZodSchema<T>): Promise<T> {
const response = await fetch(url);
const data = await response.json();
return validateResponse(data, schema);
}
// Transformação genérica de dados
function transformResponse<T, R>(data: T, transformer: (input: T) => R): R {
return transformer(data);
}
7. Boas práticas e armadilhas comuns
Evite any em fallbacks: Prefira unknown com type guards:
// Ruim
function processData(data: any) {
return data.value; // Sem segurança
}
// Bom
function processData(data: unknown) {
if (data && typeof data === 'object' && 'value' in data) {
return (data as { value: number }).value;
}
throw new Error('Dados inválidos');
}
Documente tipos genéricos com JSDoc:
/**
* Busca dados de uma API e retorna uma resposta tipada
* @template T - Tipo dos dados retornados
* @param url - URL do endpoint
* @returns Promise com ApiResponse<T>
*/
Evite complexidade excessiva: Generics muito aninhados ou tipos recursivos podem impactar performance e legibilidade.
8. Exemplo completo: integração com React Query
React Query (TanStack Query) se beneficia enormemente de generics:
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
// Tipo para função de query genérica
type QueryFunction<T> = () => Promise<ApiResponse<T>>;
// Hook tipado para useQuery
function useApiQuery<T>(
key: string[],
queryFn: QueryFunction<T>,
options?: Omit<UseQueryOptions<ApiResponse<T>, Error, T>, 'queryKey' | 'queryFn'>
) {
return useQuery<ApiResponse<T>, Error, T>({
queryKey: key,
queryFn,
select: (response) => response.data, // Extrai apenas os dados
...options
});
}
// Componente usando o hook
function UserProfile({ userId }: { userId: number }) {
const { data: user, isLoading, error } = useApiQuery<User>(
['user', userId.toString()],
() => fetchData<User>(`/api/users/${userId}`)
);
if (isLoading) return <div>Carregando...</div>;
if (error) return <div>Erro: {error.message}</div>;
return <div>Nome: {user?.name}</div>;
}
Benefícios dessa abordagem:
- Autocomplete completo nos dados retornados
- Refatoração segura quando a API muda
- Testes facilitados com mocks tipados
- Reutilização do mesmo padrão para diferentes endpoints
Com generics, você transforma chamadas de API de código frágil em componentes robustos e previsíveis, aproveitando todo o poder do sistema de tipos do TypeScript.
Referências
- Documentação oficial do TypeScript - Generics — Guia completo sobre sintaxe e uso de generics no TypeScript
- TanStack Query - TypeScript — Documentação oficial sobre tipagem com React Query e generics
- Zod - TypeScript-first schema validation — Biblioteca de validação de schemas com inferência de tipos TypeScript
- TypeScript Deep Dive - Generics — Guia avançado sobre generics no TypeScript por Basarat Ali Syed
- React TypeScript Cheatsheet - Hooks with generics — Referência prática para criar hooks genéricos em React com TypeScript