El conversor genera un primer borrador a partir de los valores de la muestra. Los ajustes manuales siguientes requieren conocer el contrato de la API; los tipos inferidos no validan datos en tiempo de ejecución.
Comencemos con los fundamentos. Dado un objeto JSON, ¿cómo lo conviertes en una interfaz TypeScript?
Aquí hay una respuesta simple de usuario de una API:
{
"id": 42,
"username": "jdoe",
"email": "[email protected]",
"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.
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.
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.
Las APIs a menudo devuelven campos que pueden o no estar presentes. Márcalos con ?:
{
"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.
Las APIs de producción son desordenadas. Así es cómo manejar los problemas comunes.
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
}
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
}
}
Cuando una API devuelve un conjunto fijo de valores string, usa uniones de literales string en lugar de enums de TypeScript:
{ "status": "active" }
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).
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.
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.
Los tipos solo son útiles si los aplicas en el límite donde los datos entran a tu aplicación.
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);
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
}
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
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.
readonly para datos inmutablesLas 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.
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
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.
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.
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:
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.
El procesamiento se realiza en tu navegador. Guardar y compartir es opcional, requiere una cuenta y envía el contenido elegido a nuestro servicio.
Sí. El convertidor genera interfaces separadas para cada objeto anidado y las referencia correctamente.
Los tipos de TypeScript generados están listos para producción. Cópialos o descárgalos como archivo .ts.