Pular para o conteúdo

interface ou type? Gerando tipos TypeScript a partir de JSON

Como transformar uma resposta JSON em tipos TypeScript, quando escolher interface ou type e o que um tipo inferido de uma amostra pode e não pode garantir.

Por Felipe YamaokaPublicado em 5 de novembro de 20265 min de leitura

Tipando uma API que você não escreveu

Você chama um endpoint, recebe um JSON de volta e quer autocomplete e checagem de tipos para ele. A API não tem arquivo OpenAPI nem SDK, só um exemplo na documentação, ou a resposta que você acabou de ver na aba Network do navegador.

Escrever os tipos na mão é lento e propenso a erro, principalmente com objetos e arrays aninhados. Gerar a partir de uma amostra é rápido, mas você precisa saber o que o gerador assume e quais decisões continuam sendo suas. Este guia cobre as duas coisas, e ainda a eterna dúvida: interface ou type?

De uma amostra para os tipos

Pegue esta resposta de uma API de usuários fictícia:

json
{
  "id": 42,
  "name": "Ada",
  "email": null,
  "tags": ["admin", "beta"],
  "address": { "city": "São Paulo", "zip": "01000-000" },
  "orders": [
    { "id": 1, "total": 99.9, "coupon": "BLACK10" },
    { "id": 2, "total": 15 }
  ],
  "created-at": "2026-11-05T12:00:00Z"
}

Colando no conversor JSON para TypeScript com o nome da raiz User, sai:

ts
interface User {
  id: number;
  name: string;
  email: unknown | null;
  tags: string[];
  address: Address;
  orders: Order[];
  "created-at": string;
}

interface Address {
  city: string;
  zip: string;
}

interface Order {
  id: number;
  total: number;
  coupon?: string;
}

Várias regras estão agindo aqui, e são as mesmas que você aplicaria na mão:

  • Objetos aninhados viram declarações com nome. address vira Address, e você pode reutilizar o tipo em outro lugar.
  • Itens de array ganham o nome no singular. orders vira Order[].
  • Objetos dentro de um array são mesclados. O segundo pedido não tem coupon, então o campo vira opcional: coupon?: string.
  • Chaves que não são identificadores válidos ficam entre aspas. "created-at" não pode ser escrito como nome de propriedade simples.
  • Um null sozinho não diz nada sobre o tipo. A amostra mostra que email pode ser nulo, mas não o que ele é quando tem valor, então o resultado é unknown | null. Troque unknown por string quando souber.

Um detalhe para quem trabalha com APIs brasileiras: campos como CEP, CPF e telefone devem continuar string, mesmo quando parecem números. Um CEP como 01000-000 tem hífen e zero à esquerda, e um CPF como número perderia os zeros do começo.

interface ou type?

Para o formato de um objeto simples, os dois funcionam e são intercambiáveis na maior parte do código:

ts
interface Address {
  city: string;
  zip: string;
}

type Address = {
  city: string;
  zip: string;
};

As diferenças aparecem nas bordas.

O que só o type faz. Um alias de tipo pode dar nome a qualquer coisa, não só a objetos: uniões (type Status = "paid" | "pending"), tuplas (type Point = [number, number]), primitivos, tipos de função e tipos mapeados ou condicionais. Quando o formato do dado é "um destes", você precisa de type.

O que só a interface faz. Interfaces aceitam declaration merging: duas declarações interface Window no mesmo escopo viram uma só. Bibliotecas usam isso para deixar você acrescentar propriedades aos tipos delas. Isso também é um risco: se uma interface Response gerada cair num script global, ela se mistura com o Response do DOM em vez de criar um tipo novo. É por isso que o conversor deste site renomeia esses nomes, por exemplo para Response2, em vez de sobrescrever um tipo nativo.

Estendendo. Interfaces usam extends, types usam interseção:

ts
interface Admin extends User {
  permissions: string[];
}

type Admin2 = User & { permissions: string[] };

O time do TypeScript recomenda extends para tipos de objeto quando você monta hierarquias grandes: o compilador guarda em cache as relações entre interfaces, e os erros de propriedades conflitantes ficam mais claros do que o never que às vezes sai de uma interseção.

Uma regra prática. Use interface para formatos de objeto que outro código pode estender, como modelos de API. Use type para uniões, tuplas e tudo o que for calculado a partir de outros tipos. Escolha um estilo para objetos simples no projeto e mantenha; o conversor gera qualquer um dos dois.

O que uma amostra não consegue dizer

Um tipo inferido de uma resposta descreve aquela resposta, e não o contrato da API. Tenha estes limites em mente:

  • Campos ausentes parecem opcionais ou nem existem. Se a amostra nunca mostra phone, o tipo não vai ter. Se só alguns itens têm coupon, ele vira opcional, o que pode ou não valer para toda resposta.
  • Datas são strings. JSON não tem tipo de data, então "2026-11-05T12:00:00Z" vira string. Converta explicitamente ao ler.
  • Todo número é number. A amostra não diferencia um ID inteiro de um preço, e IDs muito grandes podem já ter perdido precisão; nesse caso a API deveria mandar como string.
  • Uniões precisam de mais amostras. Se um campo às vezes é número e às vezes é texto, você só vê isso quando os dois aparecem. Cole um array com várias respostas reais para o gerador mesclar.
  • Arrays vazios viram unknown[]. Não há nada para inferir de [].

Tipo não é validação

Os tipos do TypeScript somem quando o código é compilado. Escrever const user = (await response.json()) as User não confere nada em tempo de execução; se a API mandar outra coisa, o erro aparece depois, longe da causa.

Para dados que vêm de fora do programa, use os tipos gerados como ponto de partida e valide na fronteira: uma biblioteca de schema como Zod ou Valibot, ou uma checagem simples dos campos de que você depende. Aí o tipo passa a descrever um dado que você de fato conferiu.

ts
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error(`Request failed: ${response.status}`);
  const data: unknown = await response.json();
  if (typeof data !== "object" || data === null || !("id" in data))
    throw new Error("Unexpected response");
  return data as User;
}

Erros comuns

  • Confiar numa amostra só. Gere a partir de várias respostas, incluindo casos de borda como listas vazias e campos nulos.
  • Deixar o unknown para trás. Troque assim que souber o tipo real.
  • Tipar datas como Date. O JSON.parse nunca cria objetos Date; o valor é string até você converter.
  • Sobrescrever nomes nativos. Evite chamar seus tipos de Response, Error ou Event.
  • Usar as em vez de validar. Uma asserção de tipo é uma promessa que você faz ao compilador, não uma checagem.

Resumo

Gerar tipos TypeScript a partir de JSON economiza tempo e acerta objetos aninhados, arrays e campos opcionais, desde que você lembre que o resultado descreve a amostra, e não o contrato. Prefira interface para formatos de objeto que podem ser estendidos e type para uniões, tuplas e tipos calculados. E valide os dados que vêm de fora antes de confiar no tipo deles.

Leia também