<!-- AUTO-GENERATED by export-skills.py — DO NOT EDIT -->
---
name: openapi-to-typescript
description: Convert OpenAPI 3.0 JSON/YAML specifications to TypeScript interfaces and type guards. Use when generating types from an API spec, creating typed API client code, or converting OpenAPI schemas to TypeScript. Produces interfaces from components/schemas, request/response types from paths, and runtime type guards.
---

# OpenAPI to TypeScript

Convert OpenAPI 3.0 specifications to TypeScript interfaces and type guards.

**Input**: OpenAPI file (JSON or YAML)
**Output**: TypeScript file with interfaces, request/response types, and type guards

## When to Use

- "Generate types from OpenAPI"
- "Convert OpenAPI to TypeScript"
- "Create API interfaces from spec"
- New API integration that has an OpenAPI spec
- Keeping TypeScript types in sync with backend API

## Workflow

1. Request OpenAPI file path (if not provided)
2. Read and validate (must be OpenAPI 3.0.x)
3. Extract schemas from `components/schemas`
4. Extract endpoints from `paths` (request/response types)
5. Generate TypeScript (interfaces + type guards)
6. Ask where to save (default: `types/api.ts`)
7. Write the file

## Type Mapping

### Primitives

| OpenAPI | TypeScript |
|---|---|
| `string` | `string` |
| `number` | `number` |
| `integer` | `number` |
| `boolean` | `boolean` |
| `null` | `null` |

### Format Modifiers

| Format | TypeScript | Note |
|---|---|---|
| `uuid` | `string` | Add JSDoc `@format uuid` |
| `date` | `string` | Add JSDoc `@format date` |
| `date-time` | `string` | Add JSDoc `@format ISO 8601` |
| `email` | `string` | Add JSDoc `@format email` |
| `uri` | `string` | Add JSDoc `@format URI` |

### Complex Types

**Object** with required/optional fields:
```typescript
// required: [id], optional: name
interface Example {
  id: string;       // no ? -- required
  name?: string;    // ? -- optional
}
```

**Array**:
```typescript
// items: {type: string}
type Names = string[];
```

**Enum**:
```typescript
// enum: [active, draft]
type Status = "active" | "draft";
```

**oneOf (Union)**:
```typescript
// oneOf: [{$ref: Cat}, {$ref: Dog}]
type Pet = Cat | Dog;
```

**allOf (Intersection/Extends)**:
```typescript
// allOf: [{$ref: Base}, {properties: ...}]
interface Extended extends Base {
  extraField: string;
}
```

## Code Generation

### File Header

```typescript
/**
 * Auto-generated from: {source_file}
 * Generated at: {timestamp}
 *
 * DO NOT EDIT MANUALLY - Regenerate from OpenAPI schema
 */
```

### Interfaces (from components/schemas)

```typescript
export interface Product {
  /** Product unique identifier */
  id: string;
  /** Product title */
  title: string;
  /** Product price */
  price: number;
  /** Created timestamp (ISO 8601) */
  created_at?: string;
}
```

Rules:
- Use OpenAPI `description` as JSDoc comment
- Fields in `required[]` have no `?`
- Fields not in `required[]` have `?`

### Request/Response Types (from paths)

Naming convention: `{Method}{Path}Request` / `{Method}{Path}Response`

```typescript
// GET /products - query params
export interface GetProductsRequest {
  page?: number;
  limit?: number;
}

// GET /products - response 200
export type GetProductsResponse = ProductList;

// POST /products - request body
export interface CreateProductRequest {
  title: string;
  price: number;
}

// POST /products - response 201
export type CreateProductResponse = Product;
```

### Type Guards

For each main interface, generate a runtime type guard:

```typescript
export function isProduct(value: unknown): value is Product {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof (value as any).id === 'string' &&
    'title' in value &&
    typeof (value as any).title === 'string' &&
    'price' in value &&
    typeof (value as any).price === 'number'
  );
}
```

Type guard rules:
- Check `typeof value === 'object' && value !== null`
- Required fields: `'field' in value` + type check
- Arrays: `Array.isArray()`
- Enums: `.includes()`

### Error Type (always include)

```typescript
export interface ApiError {
  status: number;
  error: string;
  detail?: string;
}

export function isApiError(value: unknown): value is ApiError {
  return (
    typeof value === 'object' &&
    value !== null &&
    'status' in value &&
    typeof (value as any).status === 'number' &&
    'error' in value &&
    typeof (value as any).error === 'string'
  );
}
```

## $ref Resolution

When encountering `{"$ref": "#/components/schemas/Product"}`:
1. Extract schema name (`Product`)
2. Use the type directly as a reference (don't inline)

```typescript
// $ref: "#/components/schemas/Product"
items: Product[]  // reference, not inlined
```

## Complete Example

**Input** (OpenAPI):
```json
{
  "openapi": "3.0.0",
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "id": {"type": "string", "format": "uuid"},
          "email": {"type": "string", "format": "email"},
          "role": {"type": "string", "enum": ["admin", "user"]}
        },
        "required": ["id", "email", "role"]
      }
    }
  },
  "paths": {
    "/users/{id}": {
      "get": {
        "parameters": [{"name": "id", "in": "path", "required": true}],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/User"}
              }
            }
          }
        }
      }
    }
  }
}
```

**Output** (TypeScript):
```typescript
/**
 * Auto-generated from: api.openapi.json
 * DO NOT EDIT MANUALLY
 */

export type UserRole = "admin" | "user";

export interface User {
  /** @format uuid */
  id: string;
  /** @format email */
  email: string;
  role: UserRole;
}

export interface GetUserByIdRequest {
  id: string;
}

export type GetUserByIdResponse = User;

export function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value && typeof (value as any).id === 'string' &&
    'email' in value && typeof (value as any).email === 'string' &&
    'role' in value && ['admin', 'user'].includes((value as any).role)
  );
}

export interface ApiError {
  status: number;
  error: string;
  detail?: string;
}
```

## Error Handling

| Error | Action |
|---|---|
| OpenAPI version != 3.0.x | Report: only 3.0 supported |
| Missing `$ref` target | List missing refs, continue with `unknown` |
| Unknown type | Use `unknown` and warn |
| Circular reference | Use type alias with lazy reference |
| No `components/schemas` | Generate only path types |

## Anti-Patterns

| Avoid | Why | Instead |
|---|---|---|
| Inlining `$ref` schemas | Duplicates types, harder to maintain | Use type references |
| Skipping optional markers | Runtime errors on missing fields | Respect `required[]` array |
| Generating `any` types | Defeats purpose of TypeScript | Use `unknown` with type guards |
| Manual edits to generated files | Overwritten on regeneration | Extend types in separate files |
| Ignoring `format` hints | Loses documentation value | Add JSDoc comments |

## References

- Based on [softaworks/agent-toolkit openapi-to-typescript](https://github.com/softaworks/agent-toolkit/tree/main/skills/openapi-to-typescript) (MIT License)
- [OpenAPI 3.0 Specification](https://spec.openapis.org/oas/v3.0.3)

<!-- Source: .faos/custom/skills/backend/openapi-to-typescript/SKILL.md -->
