Typing Third-Party APIs Without Official Types
Most APIs ship without TypeScript definitions. You get a JSON response, you know it works, but your code treats every field as any. That means no autocomplete, no compile-time checks, and bugs that only surface at runtime.
The usual fix is writing interfaces by hand. For a simple response, that takes a few minutes. For a nested payload with optional fields and mixed-type arrays, it takes much longer and introduces its own errors.
The manual approach and its problems
Consider a response from a project management API:
{
"id": "proj_8a3f",
"name": "Backend Rewrite",
"owner": {
"id": "usr_12",
"email": "dana@example.com",
"role": "admin"
},
"tasks": [
{
"id": "task_1",
"title": "Migrate database",
"assignee": null,
"due": "2026-04-15",
"subtasks": []
},
{
"id": "task_2",
"title": "Update API routes",
"assignee": { "id": "usr_34", "email": "sam@example.com" },
"due": null,
"subtasks": [{ "id": "sub_1", "title": "Auth endpoints", "done": true }]
}
],
"archived": false
}
Writing types for this by hand means handling the assignee field (object or null), the subtasks array (empty in one item, populated in another), and the missing role field on the nested assignee. Get any of those wrong and your types lie to you, which is worse than having no types at all.
Generating interfaces from a sample response
A faster approach: paste the JSON into a converter and get interfaces back. For the payload above, the output looks like this:
interface Subtask {
id: string;
title: string;
done: boolean;
}
interface Assignee {
id: string;
email: string;
role?: string;
}
interface Task {
id: string;
title: string;
assignee: Assignee | null;
due: string | null;
subtasks: Subtask[];
}
interface Root {
id: string;
name: string;
owner: Assignee;
tasks: Task[];
archived: boolean;
}
Notice what happened automatically. The converter merged the two task objects and marked assignee as Assignee | null because one item had null and the other had an object. It also detected that the assignee inside tasks lacks role, so role became optional on the shared Assignee interface. The owner field reuses the same interface since the shape overlaps.
Making it part of your workflow
The generated types are a starting point, not a final product. A few things to do after generating:
- Rename
Rootto something meaningful likeProject. - Tighten union types if you know more than the sample shows. If
dueis always an ISO date string ornull, the generatedstring | nullis correct. If it can also be a Unix timestamp, add that to the union. - Add the types to your API client so every
fetchcall returns a typed response.
async function getProject(id: string): Promise<Project> {
const res = await fetch(`/api/projects/${id}`);
return res.json();
}
This gives you autocomplete on every field and catches typos like project.achived at compile time instead of in production.
When samples are not enough
One JSON response shows one possible shape. If the API returns different structures based on status or user permissions, you need multiple samples. Paste them all into the converter. The array merging logic handles the differences: fields present in some samples but not others become optional, and conflicting types produce unions.
For APIs with pagination or polymorphic responses, generate types from several pages or response variants. Two or three samples usually cover the full shape.
Convert a response now with the converter.