Get the FREE Ultimate OpenClaw Setup Guide →

generating-typescript-types-from-apis

npx machina-cli add skill WesleySmits/agent-skills/api-typescript-generator --openclaw
Files (1)
SKILL.md
10.4 KB

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

SourceApproach
JSON responseParse and infer types
OpenAPI/SwaggerUse generator tool
GraphQLUse codegen
Live endpointFetch 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 ValueTypeScript Type
123number
"text"string
true/falseboolean
nullnull (or T | null)
[]T[] (infer from items)
{} emptyRecord<string, unknown>
{} with keysNamed interface
ISO date stringstring (add comment)
UUID stringstring (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 any types in output
  • Types compile without errors
# Validate generated types
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

Source

git clone https://github.com/WesleySmits/agent-skills/blob/main/.agent/skills/api-typescript-generator/SKILL.mdView on GitHub

Overview

Turns API responses and OpenAPI schemas into TypeScript interfaces. It helps you type API payloads, build interfaces from JSON, and keep frontend and backend types in sync.

How This Skill Works

Identify the API source (JSON response, OpenAPI, or endpoint), parse the response structure, and generate TypeScript interfaces. The process handles nested objects and arrays, adds JSDoc comments, and exports the types to the appropriate location.

When to Use It

  • When you need to type an API response
  • When you have JSON and need TypeScript interfaces
  • When working with OpenAPI/Swagger schemas
  • When generating types from endpoints
  • When keeping frontend and backend types in sync

Quick Start

  1. Step 1: Identify Source Type (JSON, OpenAPI, or endpoint)
  2. Step 2: Parse the response structure and infer types
  3. Step 3: Generate interfaces, add JSDoc comments, and export

Best Practices

  • Start from representative samples to accurately infer optional fields
  • Ensure nested objects and arrays are properly modeled
  • Add JSDoc comments for each property to improve IDE help
  • Use branded types for special strings (e.g., ISODateString, UUID, Email)
  • Export interfaces to a shared types folder and keep them in sync with OpenAPI

Example Use Cases

  • Generate a User interface from a sample JSON response
  • Create interfaces from an OpenAPI/Swagger /users endpoint
  • Infer nested types like UserProfile and SocialLink
  • Apply branded types for ISODateString and UUID in a User
  • Keep frontend types aligned with backend changes via OpenAPI updates

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers