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.
Comecemos pelos fundamentos. Dado um objecto JSON, como o transforma numa interface TypeScript?
Eis uma resposta simples de utilizador vinda 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 primitivo TypeScript: number, string ou boolean. As chaves do objecto tornam-se nomes de propriedades.
As APIs reais raramente devolvem estruturas planas. Considere um utilizador com um endereço:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Separe os objectos aninhados nas suas próprias interfaces:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Isto mantém cada tipo focado e reutilizável. Se outro endpoint também devolver um Address, utiliza a mesma interface.
Quando um campo contém um array, 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[];
}
Utilize a sintaxe Type[] para casos simples. Para genéricos complexos, Array<Type> pode ser mais claro, mas a consistência importa mais do que qual escolhe.
As APIs frequentemente devolvem 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 totalmente ausente. Isto é diferente de um campo anulável, onde a chave existe mas o valor pode ser null.
As APIs de produção são confusas. Eis como lidar com os problemas mais 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 estar tanto ausente como nulo, combine os dois:
interface User {
id: number;
username: string;
middleName?: string | null; // Pode estar ausente, ou presente mas nulo
}
Alguns campos aceitam múltiplos tipos. Uma API de pesquisa pode devolver resultados mistos:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modele isto 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 actua como discriminante. O TypeScript restringe o tipo automaticamente quando o verifica:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // O TypeScript sabe que isto é UserResult
} else {
console.log(result.title); // O TypeScript sabe que isto é PostResult
}
}
Quando uma API devolve um conjunto fixo de valores de string, utilize uniões de literais de string em vez de enums TypeScript:
{ "status": "active" }
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
As uniões de literais de string são mais simples, passíveis de tree-shaking e não geram JavaScript em tempo de execução como o enum faz. Reserve o enum para casos em que necessita de mapeamento inverso (número para nome).
Tanto interface como type podem descrever formas de objectos. Eis quando escolher cada um:
Utilize interface para formas de objectos que possa estender:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Utilize type para uniões, intersecções e tipos computados:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
Na prática, qualquer um funciona para a maioria dos casos. Escolha uma convenção para o seu projecto e mantenha-a. Muitas equipas utilizam interface para modelos de dados e type para tudo o resto.
Para máxima segurança, defina as suas formas com Zod e infira os 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 o Zod, os seus tipos e a lógica de validação vivem no mesmo local. Se a API mudar de forma, o parse() lança erro imediatamente em vez de deixar dados incorrectos propagarem-se pela sua aplicação.
Os tipos só são úteis se os aplicar na fronteira onde os dados entram na sua aplicação.
Crie uma função fetch genérica que imponha 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;
}
// Utilização
const user = await fetchJson<User>('/api/users/42');
const validatedUser = await fetchJson('/api/users/42', UserSchema);
Se utiliza o 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; // Totalmente tipado
}
Em aplicações React, combine tipos com a sua biblioteca de obtenção 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, active o modo strict (ou, no mínimo, strictNullChecks). Isto obriga-o a lidar com null e undefined explicitamente:
{
"compilerOptions": {
"strict": true
}
}
Sem verificações estritas de null, o TypeScript deixa-o aceder a .username num valor potencialmente null sem reclamar. Isso derrota o propósito de tipar as suas respostas de API.
readonly para dados imutáveisAs respostas de API são dados que recebeu, não dados que deve mutar. Marque as propriedades como readonly:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
Ou utilize um tipo utilitário para toda a interface:
type ImmutableUser = Readonly<User>;
Isto previne mutações acidentais que causam erros subtis.
Previna a mistura de IDs de diferentes entidades com tipos com marca:
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 com a API num directório dedicado:
src/
types/
api/
user.ts
post.ts
common.ts // Tipos partilhados como PaginatedResponse
index.ts // Re-exportações
Isto torna os tipos fáceis de encontrar, importar e manter. Quando uma API muda, actualiza um ficheiro e o compilador mostra-lhe tudo o que falha.
Nunca confie em dados externos. Mesmo com tipos perfeitos, a API pode devolver algo inesperado. Valide as respostas onde elas entram na sua aplicação:
// Não faça isto
const user = (await response.json()) as User; // Confia cegamente
// Faça isto
const data = await response.json();
const user = UserSchema.parse(data); // Valida a forma
As asserções de tipo (as User) silenciam o compilador mas não verificam os dados. A validação em tempo de execução (Zod, io-ts, Valibot) verifica de facto se a forma corresponde.
Ao lidar com payloads JSON grandes ou complexos, utilize o editor de JSON para explorar e compreender a estrutura antes de escrever tipos. É muito mais fácil ver relações aninhadas num editor adequado do que em texto simples.
Para equipas que trabalham com APIs JSON, consulte estes guias relacionados:
Se a sua API tem uma especificação OpenAPI (Swagger), salte a conversão de JSON por completo. Ferramentas como openapi-typescript geram tipos directamente a partir da especificação:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Esta abordagem é superior porque a especificação define campos opcionais, tipos anuláveis e enums explicitamente. Obtém tipos precisos sem adivinhar a partir de dados de amostra.
O processamento decorre no navegador. Guardar e partilhar é 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 referencia-os corretamente.
Os tipos TypeScript gerados estão prontos para produção. Copie-os ou transfira-os como ficheiro .ts.
Sim. O conversor deteta tipos de arrays e gera a sintaxe de array TypeScript correta (Type[]). Os campos opcionais são detetados quando os valores podem ser null ou undefined.