
generating-typescript-types-from-apis
by WesleySmits
43 production-ready skills for AI coding agents. Works with Claude, GitHub Copilot, Cursor, Windsurf, and Zed.
SKILL.md
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
- Identify API source (JSON response, OpenAPI, endpoint)
- Parse response structure
- Generate TypeScript interfaces
- Handle nested objects and arrays
- Add JSDoc comments
- Export types to appropriate location
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:
// types/api/user.ts
export interface User {
/** Unique identifier */
id: number;
/** User's full name */
name: string;
/** Email address */
email: string;
/** Whether the user account is active */
isActive: boolean;
/** Assigned roles */
roles: string[];
/** User profile information */
profile: UserProfile;
/** Account creation timestamp (ISO 8601) */
createdAt: string;
/** Additional metadata */
metadata: Record<string, unknown>;
}
export interface UserProfile {
/** Avatar image URL */
avatar: string;
/** User biography */
bio: string | null;
/** Social media links */
socialLinks: SocialLink[];
}
export interface SocialLink {
/** Platform name */
platform: string;
/** Profile URL */
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:
// types/branded.ts
export type UUID = string & { readonly __brand: "UUID" };
export type ISODateString = string & { readonly __brand: "ISODateString" };
export type Email = string & { readonly __brand: "Email" };
// Usage
export interface User {
id: UUID;
email: Email;
createdAt: ISODateString;
}
Step 4: Handle Arrays
Homogeneous array:
// JSON: [1, 2, 3]
items: number[];
// JSON: ["a", "b"]
tags: string[];
Array of objects:
// JSON: [{ "id": 1, "name": "Item" }]
items: Item[];
interface Item {
id: number;
name: string;
}
Mixed array (avoid if possible):
// JSON: [1, "two", true]
values: (number | string | boolean)[];
Tuple (fixed length, known types):
// JSON: [37.7749, -122.4194] (lat/lng)
coordinates: [number, number];
Step 5: Handle Optional Fields
Detect optional fields from multiple samples:
// Sample 1: { "name": "John", "nickname": "Johnny" }
// Sample 2: { "name": "Jane" }
export interface User {
name: string;
nickname?: string; // Optional - not present in all responses
}
Nullable vs optional:
export interface User {
bio: string | null; // Present but can be null
nickname?: string; // May not be present
avatar?: string | null; // May not be present, or null
}
Step 6: API Response Wrappers
Paginated response:
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
perPage: number;
total: number;
totalPages: number;
};
}
// Usage
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[]>;
}
// Usage
type UserResponse = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;
Step 7: OpenAPI/Swagger Generation
Using openapi-typescript:
npm install -D openapi-typescript
# From URL
npx openapi-typescript https://api.example.com/openapi.json -o types/api.ts
# From local file
npx openapi-typescript ./openapi.yaml -o types/api.ts
# Watch mode
npx openapi-typescript ./openapi.yaml -o types/api.ts --watch
Generated usage:
import type { paths, components } from "./types/api";
// Extract response type
type User = components["schemas"]["User"];
// Extract endpoint types
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" });
// Fully typed request/response
const { data, error } = await client.GET("/users/{id}", {
params: { path: { id: "123" } },
});
// data is typed as User
Step 8: Fetch and Generate Script
// scripts/generate-types.ts
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:
# .github/workflows/types.yml
name: Generate API Types
on:
schedule:
- cron: "0 0 * * *" # Daily
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:
# .husky/pre-commit
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:
// types/api/index.ts
export * from "./user";
export * from "./product";
export type { ApiResponse, ApiError, PaginatedResponse } from "./common";
Validation
Before completing:
- All interfaces have JSDoc comments
- Nested objects have named interfaces
- Optional fields marked with
? - Nullable fields use
| null - Arrays are properly typed
- No
anytypes in output - Types compile without errors
# Validate generated types
npx tsc --noEmit types/**/*.ts
Error Handling
- Empty object
{}: UseRecord<string, unknown>notobject. - 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
stringwith JSDoc explaining format.
Resources
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon

