O conversor gera um rascunho a partir dos valores da amostra. Os ajustes manuais abaixo exigem conhecer o contrato da API; os tipos inferidos não validam dados durante a execução.
Vamos começar com os fundamentos. Dado um objeto JSON, como você o transforma em uma interface TypeScript?
Aqui está uma resposta simples de usuário de uma API:
{
"id": 42,
"username": "jdoe",
"email": "[email protected]",
"isActive": true
}
A interface TypeScript correspondente:
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
Cada valor JSON mapeia para um tipo primitivo do TypeScript: number, string ou boolean. As chaves do objeto se tornam nomes de propriedades.
APIs reais raramente retornam estruturas planas. Considere um usuário com endereço:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Separe objetos aninhados em suas próprias interfaces:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Isso mantém cada tipo focado e reutilizável. Se outro endpoint também retorna um Address, você usa a mesma interface.
Quando um campo contém um array, você anota o tipo do elemento:
{
"id": 42,
"username": "jdoe",
"roles": ["admin", "editor"],
"posts": [
{ "id": 1, "title": "Hello World" },
{ "id": 2, "title": "TypeScript Tips" }
]
}
interface Post {
id: number;
title: string;
}
interface User {
id: number;
username: string;
roles: string[];
posts: Post[];
}
Use a sintaxe Type[] para casos simples. Para genéricos complexos, Array<Type> pode ser mais claro, mas consistência importa mais do que qual você escolhe.
APIs frequentemente retornam campos que podem ou não estar presentes. Marque-os com ?:
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // Pode estar ausente da resposta
}
Um campo opcional (bio?: string) significa que a chave pode estar completamente ausente. Isso é diferente de um campo anulável, onde a chave existe mas o valor pode ser null.
APIs em produção são bagunçadas. Veja como lidar com as armadilhas comuns.
Quando um campo pode ser explicitamente null:
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // String de data ISO ou null
}
Se um campo pode ser tanto ausente quanto null, combine os dois:
interface User {
id: number;
username: string;
middleName?: string | null; // Pode estar ausente, ou presente mas null
}
Alguns campos aceitam múltiplos tipos. Uma API de busca pode retornar resultados mistos:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modele isso com uma união discriminada:
interface UserResult {
type: 'user';
id: number;
username: string;
}
interface PostResult {
type: 'post';
id: number;
title: string;
}
type SearchResult = UserResult | PostResult;
interface SearchResponse {
results: SearchResult[];
}
O campo type atua como discriminante. O TypeScript restringe o tipo automaticamente quando você o verifica:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript sabe que é UserResult
} else {
console.log(result.title); // TypeScript sabe que é PostResult
}
}
Quando uma API retorna um conjunto fixo de valores string, use uniões de literais string em vez de enums do TypeScript:
{ "status": "active" }
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
Uniões de literais string são mais simples, eliminam código morto via tree-shaking e não geram JavaScript em tempo de execução como enum faz. Reserve enum para casos onde você precisa de mapeamento reverso (número para nome).
Tanto interface quanto type podem descrever formas de objetos. Veja quando escolher cada um:
Use interface para formas de objetos que você pode estender:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Use type para uniões, interseções e tipos computados:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
Na prática, ambos funcionam para a maioria dos casos. Escolha uma convenção para seu projeto e mantenha-a. Muitas equipes usam interface para modelos de dados e type para todo o resto.
Para máxima segurança, defina suas formas com Zod e infira tipos TypeScript a partir delas:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
isActive: z.boolean(),
bio: z.string().nullable().optional(),
});
type User = z.infer<typeof UserSchema>;
// Agora valide em tempo de execução
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // Lança erro se a forma não corresponder
Com Zod, seus tipos e lógica de validação vivem no mesmo lugar. Se a API muda de forma, parse() lança erro imediatamente em vez de deixar dados inválidos se propagarem pela sua aplicação.
Tipos só são úteis se você os aplicar na fronteira onde os dados entram na sua aplicação.
Crie uma função fetch genérica que garanta os tipos:
async function fetchJson<T>(url: string, schema?: z.ZodType<T>): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (schema) {
return schema.parse(data);
}
return data as T;
}
// Uso
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Se você usa Axios, aplique tipos ao genérico da resposta:
import axios from 'axios';
interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
perPage: number;
lastPage: number;
};
}
async function getUsers(page: number = 1) {
const response = await axios.get<PaginatedResponse<User>>('/api/users', {
params: { page },
});
return response.data; // Completamente tipado
}
Em aplicações React, combine tipos com sua biblioteca de busca de dados:
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// No seu componente
const { data: user, isLoading } = useUser(42);
// user é tipado como User | undefined
No seu tsconfig.json, habilite o modo strict (ou no mínimo strictNullChecks). Isso obriga você a tratar null e undefined explicitamente:
{
"compilerOptions": {
"strict": true
}
}
Sem verificações estritas de null, o TypeScript permite acessar .username em um valor potencialmente null sem reclamar. Isso anula o propósito de tipar suas respostas de API.
readonly para dados imutáveisRespostas de API são dados que você recebeu, não dados que deveria mutar. Marque propriedades como readonly:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
Ou use um tipo utilitário para a interface inteira:
type ImmutableUser = Readonly<User>;
Isso previne mutações acidentais que causam bugs sutis.
Previna a confusão de IDs de diferentes entidades com tipos nominais:
type UserId = number & { readonly __brand: 'UserId' };
type PostId = number & { readonly __brand: 'PostId' };
function getUser(id: UserId): Promise<User> { /* ... */ }
function getPost(id: PostId): Promise<Post> { /* ... */ }
const userId = 42 as UserId;
const postId = 7 as PostId;
getUser(userId); // OK
getUser(postId); // Erro de tipo! Não pode passar PostId onde UserId é esperado
Mantenha todos os tipos relacionados à API em um diretório dedicado:
src/
types/
api/
user.ts
post.ts
common.ts // Tipos compartilhados como PaginatedResponse
index.ts // Re-exportações
Isso torna os tipos fáceis de encontrar, importar e manter. Quando uma API muda, você atualiza um arquivo e o compilador mostra tudo que quebra.
Nunca confie em dados externos. Mesmo com tipos perfeitos, a API pode retornar algo inesperado. Valide as respostas onde elas entram na sua aplicação:
// Não faça isso
const user = (await response.json()) as User; // Confia cegamente
// Faça isso
const data = await response.json();
const user = UserSchema.parse(data); // Valida a forma
Asserções de tipo (as User) silenciam o compilador mas não verificam os dados. Validação em tempo de execução (Zod, io-ts, Valibot) realmente verifica se a forma corresponde.
Ao lidar com payloads JSON grandes ou complexos, use o editor JSON para explorar e entender a estrutura antes de escrever tipos. É muito mais fácil ver relacionamentos aninhados em um editor adequado do que em texto bruto.
Para equipes que trabalham com APIs JSON, confira estes guias relacionados:
Se sua API tem uma especificação OpenAPI (Swagger), pule a conversão de JSON completamente. Ferramentas como openapi-typescript geram tipos diretamente a partir da especificação:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Essa abordagem é superior porque a especificação define campos opcionais, tipos anuláveis e enums explicitamente. Você obtém tipos precisos sem adivinhar a partir de dados de amostra.
O processamento ocorre no navegador. Salvar e compartilhar é opcional, exige uma conta e envia o conteúdo escolhido para o nosso serviço.
Sim. O conversor gera interfaces separadas para cada objeto aninhado e os referencia corretamente.
Os tipos TypeScript gerados estão prontos para produção. Copie-os ou baixe como arquivo .ts.
Sim. O conversor detecta tipos de arrays e gera a sintaxe de array TypeScript correta (Type[]). Campos opcionais são detectados quando os valores podem ser null ou undefined.