JSON a TypeScript: Genera tipos desde respuestas de API
Si alguna vez llamaste a una API REST en TypeScript y tipaste la respuesta como any, conoces el dolor que sigue. El autocompletado desaparece. Los errores tipográficos se cuelan. Un campo que pensabas que era un string resulta ser null, y tu aplicación se rompe en producción.
¿La solución? Convertir tus respuestas JSON en tipos TypeScript adecuados. En esta guía, recorreremos el mapeo manual, las herramientas automatizadas y las mejores prácticas que mantienen tus integraciones de API sólidas como una roca.
Por qué importan los tipos TypeScript desde JSON
El sistema de tipos de TypeScript existe para detectar errores antes de que lleguen a tus usuarios. Cuando modelas una respuesta de API con tipos precisos, suceden tres cosas:
- El autocompletado funciona en todas partes. Tu editor conoce la forma de los datos, así que dejas de adivinar nombres de campos.
- Las refactorizaciones se vuelven seguras. Renombra una propiedad y el compilador te dice cada lugar que se rompe.
- Los errores en tiempo de ejecución disminuyen. Manejas
null,undefinedy formas inesperadas en el límite en lugar de en lo profundo de tu aplicación.
Sin tipos, básicamente estás escribiendo JavaScript con pasos extra. Con tipos, obtienes un contrato vivo entre tu código y los datos que consume.
Mapeo manual de JSON a TypeScript
Comencemos con los fundamentos. Dado un objeto JSON, ¿cómo lo conviertes en una interfaz TypeScript?
Objetos básicos
Aquí hay una respuesta simple de usuario de una API:
{
"id": 42,
"username": "jdoe",
"email": "jdoe@example.com",
"isActive": true
}
La interfaz TypeScript correspondiente:
interface User {
id: number;
username: string;
email: string;
isActive: boolean;
}
Cada valor JSON se mapea a un primitivo de TypeScript: number, string o boolean. Las claves del objeto se convierten en nombres de propiedades.
Objetos anidados
Las APIs reales rara vez devuelven estructuras planas. Considera un usuario con una dirección:
{
"id": 42,
"username": "jdoe",
"address": {
"street": "123 Main St",
"city": "Springfield",
"zipCode": "62704",
"country": "US"
}
}
Separa los objetos anidados en sus propias interfaces:
interface Address {
street: string;
city: string;
zipCode: string;
country: string;
}
interface User {
id: number;
username: string;
address: Address;
}
Esto mantiene cada tipo enfocado y reutilizable. Si otro endpoint también devuelve un Address, usas la misma interfaz.
Arrays
Cuando un campo contiene un array, anotas el tipo del 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[];
}
Usa la sintaxis Type[] para casos simples. Para genéricos complejos, Array<Type> puede ser más claro, pero la consistencia importa más que cuál elijas.
Campos opcionales
Las APIs a menudo devuelven campos que pueden o no estar presentes. Márcalos con ?:
// A veces la API devuelve bio, a veces no
{
"id": 42,
"username": "jdoe",
"bio": "Software developer"
}
interface User {
id: number;
username: string;
bio?: string; // Puede estar ausente de la respuesta
}
Un campo opcional (bio?: string) significa que la clave podría faltar por completo. Esto es diferente de un campo nullable, donde la clave existe pero el valor podría ser null.
Manejo de casos extremos
Las APIs de producción son desordenadas. Así es cómo manejar los problemas comunes.
Campos nullables
Cuando un campo puede ser explícitamente null:
{
"id": 42,
"username": "jdoe",
"avatarUrl": null,
"deletedAt": null
}
interface User {
id: number;
username: string;
avatarUrl: string | null;
deletedAt: string | null; // Cadena de fecha ISO o null
}
Si un campo puede estar tanto ausente como ser null, combina los dos:
interface User {
id: number;
username: string;
middleName?: string | null; // Puede faltar, o estar presente pero ser null
}
Tipos union
Algunos campos aceptan múltiples tipos. Una API de búsqueda podría devolver resultados mixtos:
{
"results": [
{ "type": "user", "id": 1, "username": "jdoe" },
{ "type": "post", "id": 2, "title": "Hello" }
]
}
Modela esto con una unión 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[];
}
El campo type actúa como discriminante. TypeScript reduce el tipo automáticamente cuando lo verificas:
function renderResult(result: SearchResult) {
if (result.type === 'user') {
console.log(result.username); // TypeScript sabe que esto es UserResult
} else {
console.log(result.title); // TypeScript sabe que esto es PostResult
}
}
Enums desde valores string
Cuando una API devuelve un conjunto fijo de valores string, usa uniones de literales string en lugar de enums de TypeScript:
{ "status": "active" }
// Valores posibles: "active", "inactive", "suspended"
type UserStatus = 'active' | 'inactive' | 'suspended';
interface User {
id: number;
status: UserStatus;
}
Las uniones de literales string son más simples, eliminables por tree-shaking, y no generan JavaScript en tiempo de ejecución como lo hace enum. Reserva enum para casos donde necesites mapeo inverso (número a nombre).
Interfaces vs alias de tipo
Tanto interface como type pueden describir formas de objetos. Aquí cuándo elegir cada uno:
Usa interface para formas de objetos que podrías extender:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
username: string;
email: string;
}
Usa type para uniones, intersecciones y tipos computados:
type ApiResponse<T> = {
data: T;
meta: { total: number; page: number };
};
type UserOrAdmin = User | Admin;
En la práctica, cualquiera funciona para la mayoría de los casos. Elige una convención para tu proyecto y mantenla. Muchos equipos usan interface para modelos de datos y type para todo lo demás.
Uso de herramientas automatizadas
Tipar interfaces manualmente funciona para APIs pequeñas, pero se vuelve tedioso cuando tratas con endpoints que devuelven más de 50 campos o estructuras profundamente anidadas.
Antes de convertir JSON a tipos, asegúrate de que tu JSON sea válido y esté bien formateado. Nuestro formateador JSON limpia respuestas de API desordenadas, y el validador JSON detecta errores de sintaxis antes de que desperdicien tu tiempo.
Convertidores en línea
Herramientas como quicktype, json2ts y convertidores de JSON a TypeScript te permiten pegar un payload JSON y obtener interfaces al instante. Manejan objetos anidados, arrays y campos opcionales automáticamente.
El flujo de trabajo típico:
- Llama a tu API y copia la respuesta
- Pega el JSON en el convertidor
- Revisa los tipos generados y ajusta los nombres
- Copia el TypeScript a tu base de código
Generación de código desde especificaciones OpenAPI
Si tu API tiene una especificación OpenAPI (Swagger), omite la conversión de JSON por completo. Herramientas como openapi-typescript generan tipos directamente desde la especificación:
npx openapi-typescript https://api.example.com/openapi.json -o ./src/types/api.ts
Este enfoque es superior porque la especificación define campos opcionales, tipos nullables y enums explícitamente. Obtienes tipos precisos sin adivinar a partir de datos de ejemplo.
Validación en tiempo de ejecución con Zod
Para máxima seguridad, define tus formas con Zod e infiere tipos TypeScript a partir de ellas:
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>;
// Ahora valida en tiempo de ejecución
const response = await fetch('/api/users/42');
const data = await response.json();
const user = UserSchema.parse(data); // Lanza error si la forma no coincide
Con Zod, tus tipos y lógica de validación viven en un solo lugar. Si la API cambia de forma, parse() lanza un error inmediatamente en lugar de dejar que datos malos se propaguen por tu aplicación.
Integración de tipos con llamadas a API
Los tipos solo son útiles si los aplicas en el límite donde los datos entran a tu aplicación.
Wrapper de fetch tipado
Crea una función fetch genérica que imponga 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);
Axios con tipos
Si usas Axios, aplica tipos al genérico de respuesta:
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
}
React Query / TanStack Query
En aplicaciones React, combina tipos con tu librería de obtención de datos:
import { useQuery } from '@tanstack/react-query';
function useUser(id: number) {
return useQuery({
queryKey: ['user', id],
queryFn: () => fetchJson<User>(`/api/users/${id}`),
});
}
// En tu componente
const { data: user, isLoading } = useUser(42);
// user es tipado como User | undefined
Mejores prácticas
Habilita verificaciones estrictas de null
En tu tsconfig.json, habilita el modo strict (o como mínimo strictNullChecks). Esto te obliga a manejar null y undefined explícitamente:
{
"compilerOptions": {
"strict": true
}
}
Sin verificaciones estrictas de null, TypeScript te permite acceder a .username en un valor potencialmente null sin quejarse. Eso anula el propósito de tipar tus respuestas de API.
Usa readonly para datos inmutables
Las respuestas de API son datos que recibiste, no datos que debas mutar. Marca las propiedades como readonly:
interface User {
readonly id: number;
readonly username: string;
readonly email: string;
readonly createdAt: string;
}
O usa un tipo utilitario para toda la interfaz:
type ImmutableUser = Readonly<User>;
Esto previene mutaciones accidentales que causan errores sutiles.
Tipos branded para IDs
Prevén mezclar IDs de diferentes entidades con tipos branded:
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); // ¡Error de tipo! No puedes pasar PostId donde se espera UserId
Centraliza tus tipos
Mantén todos los tipos relacionados con la API en un directorio dedicado:
src/
types/
api/
user.ts
post.ts
common.ts // Tipos compartidos como PaginatedResponse
index.ts // Re-exportaciones
Esto hace que los tipos sean fáciles de encontrar, importar y mantener. Cuando una API cambia, actualizas un archivo y el compilador te muestra todo lo que se rompe.
Valida en el límite
Nunca confíes en datos externos. Incluso con tipos perfectos, la API podría devolver algo inesperado. Valida las respuestas donde entran a tu aplicación:
// No hagas esto
const user = (await response.json()) as User; // Confía ciegamente
// Haz esto
const data = await response.json();
const user = UserSchema.parse(data); // Valida la forma
Las aserciones de tipo (as User) silencian al compilador pero no verifican los datos. La validación en tiempo de ejecución (Zod, io-ts, Valibot) realmente verifica que la forma coincida.
Trabajando con estructuras JSON complejas
Al tratar con payloads JSON grandes o complejos, usa el editor JSON para explorar y entender la estructura antes de escribir tipos. Es mucho más fácil ver relaciones anidadas en un editor adecuado que en texto sin formato.
Para equipos que trabajan con APIs JSON, consulta estas guías relacionadas:
- Mejores prácticas de formato JSON — mantén tu JSON limpio y legible
- Guía de validación de JSON Schema — valida contratos de API con JSON Schema
- Patrones de diseño de API JSON — diseña APIs que sean fáciles de tipar
- Cómo validar JSON — detecta payloads malformados temprano
Poniéndolo todo junto
Aquí hay un ejemplo completo que une todo — desde JSON sin procesar hasta datos validados y tipados en un componente React:
import { z } from 'zod';
// 1. Define el esquema (fuente única de verdad)
const UserSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
isActive: z.boolean(),
role: z.enum(['admin', 'editor', 'viewer']),
profile: z.object({
bio: z.string().nullable(),
avatarUrl: z.string().url().nullable(),
socialLinks: z.array(z.object({
platform: z.string(),
url: z.string().url(),
})).optional(),
}),
});
// 2. Infiere el tipo desde el esquema
type User = z.infer<typeof UserSchema>;
// 3. Crea una función fetch tipada
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`Error al obtener usuario ${id}`);
const data = await response.json();
return UserSchema.parse(data);
}
// 4. Usa en tu aplicación con seguridad de tipos completa
const user = await getUser(42);
console.log(user.profile.bio); // string | null — el compilador lo sabe
console.log(user.profile.socialLinks); // array o undefined — el compilador lo sabe
Sin any. Sin adivinanzas. Sin sorpresas en tiempo de ejecución.
Conclusión
Convertir JSON a tipos TypeScript no se trata solo de hacer feliz al compilador. Se trata de construir una red de seguridad que detecte errores temprano, potencie el autocompletado de tu editor y haga tu base de código más fácil de mantener.
Comienza simple: copia una respuesta JSON, escribe la interfaz manualmente y tipa tus llamadas fetch. A medida que tu proyecto crezca, adopta Zod para validación en tiempo de ejecución y generación de código OpenAPI para APIs grandes. La inversión inicial se paga sola la primera vez que un error de tipo detecta un bug que habría llegado a producción.