| name | generating-typescript-types-from-apis |
| description | Generates TypeScript interfaces from API responses or OpenAPI schemas. Use when the user asks about typing API responses, creating interfaces from JSON, parsing Swagger/OpenAPI, or keeping types in sync with backend. |
API Response → TypeScript Types
When to use this skill
- User asks to type an API response
- User has JSON and needs TypeScript interfaces
- User mentions OpenAPI or Swagger schemas
- User wants to generate types from endpoints
- User asks about keeping frontend/backend types in sync
Workflow
Instructions
Step 1: Identify Source Type
| Source | Approach |
|---|
| JSON response | Parse and infer types |
| OpenAPI/Swagger | Use generator tool |
| GraphQL | Use codegen |
| Live endpoint | Fetch and parse |
Step 2: Parse JSON Response
Sample API response:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"isActive": true,
"roles": ["admin", "user"],
"profile": {
"avatar": "https://example.com/avatar.jpg",
"bio": null,
"socialLinks": [
{ "platform": "twitter", "url": "https://twitter.com/john" }
]
},
"createdAt": "2026-01-18T10:00:00Z",
"metadata": {}
}
Generated TypeScript:
export interface User {
id: number;
name: string;
email: string;
isActive: boolean;
roles: string[];
profile: UserProfile;
createdAt: string;
metadata: Record<string, unknown>;
}
export interface UserProfile {
avatar: string;
bio: string | null;
socialLinks: SocialLink[];
}
export interface SocialLink {
platform: string;
url: string;
}
Step 3: Type Inference Rules
| JSON Value | TypeScript Type |
|---|
123 | number |
"text" | string |
true/false | boolean |
null | null (or T | null) |
[] | T[] (infer from items) |
{} empty | Record<string, unknown> |
{} with keys | Named interface |
| ISO date string | string (add comment) |
| UUID string | string (add branded type) |
Branded types for special strings:
export type UUID = string & { readonly __brand: "UUID" };
export type ISODateString = string & { readonly __brand: "ISODateString" };
export type Email = string & { readonly __brand: "Email" };
export interface User {
id: UUID;
email: Email;
createdAt: ISODateString;
}
Step 4: Handle Arrays
Homogeneous array:
items: number[];
tags: string[];
Array of objects:
items: Item[];
interface Item {
id: number;
name: string;
}
Mixed array (avoid if possible):
values: (number | string | boolean)[];
Tuple (fixed length, known types):
coordinates: [number, number];
Step 5: Handle Optional Fields
Detect optional fields from multiple samples:
export interface User {
name: string;
nickname?: string;
}
Nullable vs optional:
export interface User {
bio: string | null;
nickname?: string;
avatar?: string | null;
}
Step 6: API Response Wrappers
Paginated response:
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
perPage: number;
total: number;
totalPages: number;
};
}
type UsersResponse = PaginatedResponse<User>;
API envelope:
export interface ApiResponse<T> {
success: boolean;
data: T;
error?: ApiError;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, string[]>;
}
type UserResponse = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;
Step 7: OpenAPI/Swagger Generation
Using openapi-typescript:
npm install -D openapi-typescript
npx openapi-typescript https://api.example.com/openapi.json -o types/api.ts
npx openapi-typescript ./openapi.yaml -o types/api.ts
npx openapi-typescript ./openapi.yaml -o types/api.ts --watch
Generated usage:
import type { paths, components } from "./types/api";
type User = components["schemas"]["User"];
type GetUsersResponse =
paths["/users"]["get"]["responses"]["200"]["content"]["application/json"];
type CreateUserBody =
paths["/users"]["post"]["requestBody"]["content"]["application/json"];
With openapi-fetch for type-safe requests:
npm install openapi-fetch
import createClient from "openapi-fetch";
import type { paths } from "./types/api";
const client = createClient<paths>({ baseUrl: "https://api.example.com" });
const { data, error } = await client.GET("/users/{id}", {
params: { path: { id: "123" } },
});
Step 8: Fetch and Generate Script
import { writeFileSync } from "fs";
interface TypeDefinition {
name: string;
properties: PropertyDefinition[];
}
interface PropertyDefinition {
name: string;
type: string;
optional: boolean;
nullable: boolean;
comment?: string;
}
function inferType(value: unknown, key: string): string {
if (value === null) return "null";
if (Array.isArray(value)) {
if (value.length === 0) return "unknown[]";
const itemType = inferType(value[0], `${key}Item`);
return `${itemType}[]`;
}
if (typeof value === "object") {
return toPascalCase(key);
}
return typeof value;
}
function toPascalCase(str: string): string {
return str.replace(/(^|_)(\w)/g, (_, __, c) => c.toUpperCase());
}
function generateInterface(
name: string,
obj: Record<string, unknown>,
): string[] {
const lines: string[] = [];
const nested: string[] = [];
lines.push(`export interface ${name} {`);
for (const [key, value] of Object.entries(obj)) {
const type = inferType(value, key);
const nullable = value === null ? " | null" : "";
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
nested.push(
...generateInterface(
toPascalCase(key),
value as Record<string, unknown>,
),
);
}
lines.push(` ${key}: ${type}${nullable};`);
}
lines.push("}");
lines.push("");
return [...nested, ...lines];
}
async function main() {
const response = await fetch("https://api.example.com/users/1");
const data = await response.json();
const types = generateInterface("User", data);
const output = types.join("\n");
writeFileSync("types/user.ts", output);
console.log("Generated types/user.ts");
}
main();
Step 9: Keep Types in Sync
CI check for OpenAPI changes:
name: Generate API Types
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate types
run: npx openapi-typescript ${{ vars.API_SPEC_URL }} -o types/api.ts
- name: Check for changes
id: changes
run: |
if git diff --quiet types/api.ts; then
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Create PR
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v5
with:
title: "chore: update API types"
branch: update-api-types
Pre-commit hook:
npx openapi-typescript ./openapi.yaml -o types/api.ts
git add types/api.ts
Output Location
types/
├── api/
│ ├── user.ts # User-related types
│ ├── product.ts # Product types
│ └── index.ts # Re-exports
├── api.ts # OpenAPI generated (single file)
└── branded.ts # Branded types (UUID, Email, etc.)
Index file:
export * from "./user";
export * from "./product";
export type { ApiResponse, ApiError, PaginatedResponse } from "./common";
Validation
Before completing:
npx tsc --noEmit types/**/*.ts
Error Handling
- Empty object
{}: Use Record<string, unknown> not object.
- Mixed arrays: Union type or
unknown[]; flag for manual review.
- Circular references: OpenAPI generators handle this; manual parsing needs tracking.
- Conflicting samples: Mark field as optional with union of observed types.
- Unknown date format: Default to
string with JSDoc explaining format.
Resources