Cómo tipar la respuesta de una API en TypeScript
fetch le devuelve datos de tipo any, y eso apaga TypeScript justo donde nacen los errores. Así se le da un tipo real a los datos de una API, de forma rápida y segura.
TypeScript revisa su código, pero no puede revisar los datos que llegan en tiempo de ejecución. En el momento en que llama a await response.json(), obtiene un valor de tipo any, y a partir de ahí el compilador le dejará leer user.adress.city con una errata sin quejarse lo más mínimo. Tipar las respuestas de una API cierra ese hueco. Esta guía muestra cómo escribir los tipos rápidamente a partir de una muestra, cómo modelar las partes difíciles y cuándo un tipo por sí solo no es suficiente.
Paso 1: obtenga los tipos de una muestra real
Escribir a mano las interfaces de una respuesta grande es lento y propenso a errores. Copie mejor una respuesta real, por ejemplo desde la pestaña Red (Network) del navegador o desde la documentación de la API, y péguela en JSON a TypeScript. Una respuesta como esta:
{
"id": 1024,
"name": "Ada",
"tags": ["admin", "beta"],
"address": { "city": "London", "zip": "N1 9GU" },
"orders": [
{ "order_id": "A-17", "total": 42, "shipped_at": "2026-09-01T10:00:00Z" },
{ "order_id": "A-18", "total": 18.5, "shipped_at": null, "coupon": "WELCOME" }
]
}
se convierte en lo siguiente, con el Nombre raíz puesto en User:
export interface Address {
city: string;
zip: string;
}
export interface Order {
order_id: string;
total: number;
shipped_at: string | null;
coupon?: string;
}
export interface User {
id: number;
name: string;
tags: string[];
address: Address;
orders: Order[];
}
Fíjese en lo que el generador dedujo de los dos pedidos: shipped_at era null en uno de los elementos, así que queda como string | null; coupon faltaba en uno de ellos, así que es opcional. Los objetos anidados se convirtieron en interfaces con nombre propio, y los elementos de orders pasaron a ser Order. Todo se ejecuta en el navegador, de modo que una respuesta con datos reales de clientes no se envía a ninguna parte.
Paso 2: use el tipo con fetch
La forma más sencilla de conectar el tipo con los datos es una pequeña función auxiliar:
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as T;
}
const user = await getJson<User>('/api/users/1024');
user.address.city; // string, checked by the compiler
El as T es una promesa que usted le hace al compilador, no una comprobación. Si el servidor envía otra cosa, TypeScript no se dará cuenta. Para las API internas que usted controla, ese compromiso suele ser aceptable; para cualquier otra, siga leyendo hasta el paso 4.
Paso 3: modele las partes difíciles
| En el JSON | En TypeScript | Por qué |
|---|---|---|
| Una clave que a veces falta | coupon?: string | Opcional: puede que la propiedad no exista |
| Un valor que a veces es null | shipped_at: string | null | Anulable: la propiedad existe pero puede estar vacía |
| Las dos cosas | note?: string | null | La API puede omitirla o enviar null |
| Una fecha | string | JSON no tiene tipo de fecha; conviértala con new Date() donde haga falta |
| Un ID grande | string si la API lo envía entre comillas | Los números por encima de 2^53 pierden precisión en JavaScript |
| Un array vacío en la muestra | unknown[] | La muestra no enseña cómo son sus elementos |
Prefiera unknown a any para todo lo que la muestra no puede decirle: unknown obliga a hacer una comprobación antes de usar el valor, mientras que any desactiva la revisión sin avisar. Un tipo generado solo es tan completo como su muestra, así que use una respuesta que incluya los campos opcionales, o un array con varios elementos distintos entre sí.
Paso 4: valide en tiempo de ejecución cuando los datos no son suyos
Las API de terceros cambian, los proxies devuelven páginas de error en HTML y los clientes antiguos se encuentran con servidores nuevos. Una comprobación en tiempo de ejecución detecta esos casos en la frontera, con un error claro, en lugar de en lo más profundo de la interfaz. Hay dos enfoques habituales:
- JSON Schema: genere un esquema a partir de la misma muestra con el generador de JSON Schema y valide con una biblioteca como Ajv. El esquema es neutral respecto al lenguaje, así que un backend escrito en otro lenguaje puede usar el mismo archivo.
- Un validador pensado para TypeScript: bibliotecas como Zod le permiten describir la forma una sola vez e inferir de ella el tipo de TypeScript, de modo que el tipo y la comprobación nunca pueden desincronizarse.
En cualquier caso, valide una sola vez, en el punto donde los datos entran en su programa, y deje que el resto del código confíe en el tipo.
Cuando la API cambia
Los tipos generados una sola vez se quedan atrás a medida que la API crece. Guarde en el repositorio, junto a los tipos, la muestra a partir de la cual los generó, para que la siguiente persona pueda regenerarlos y ver en un diff qué ha cambiado. Si la API publica una descripción OpenAPI, generar los tipos a partir de ella es más fiable que hacerlo desde muestras, porque enumera cada campo opcional y cada valor posible, incluidos los que una sola respuesta nunca muestra. Para las API que no la tienen, un validador en tiempo de ejecución como los del paso 4 convierte una discrepancia silenciosa en un error que puede ver en los registros.
¿interface o type?
Para los datos de una API, los dos funcionan. Las interfaces se pueden extender y se fusionan cuando se declaran dos veces, algo que a algunos equipos les gusta para los tipos públicos; los alias de tipo también pueden expresar uniones y tipos mapeados. Elija un estilo para el proyecto y manténgalo. El generador produce cualquiera de los dos.
Otros lenguajes a partir de la misma muestra
La misma muestra de JSON también puede convertirse en structs de Go, modelos de Pydantic o clases de C# en JSON a tipos, lo que ayuda a mantener el frontend y el backend con la misma forma de datos. Si la propia muestra está rota, el formateador de JSON le indica dónde.