DevToolBox無料
ブログ

OpenAPIからTypeScriptへ:openapi-typescriptによるコード生成完全ガイド

11分by DevToolBox

TL;DR

Generate TypeScript types from OpenAPI specs using openapi-typescript (CLI), @hey-api/openapi-ts (full API clients), or openapi-zod-client (Zod schemas). Combine with fetch or React Query for type-safe API calls. Try our free JSON to TypeScript tool →

Key Takeaways

  • openapi-typescript generates TypeScript interfaces from OpenAPI 3.x YAML/JSON specs with zero runtime overhead.
  • npx openapi-typescript schema.yaml -o types.ts creates a complete type file instantly from a local file or remote URL.
  • @hey-api/openapi-ts generates full API clients with typed request/response methods for batteries-included usage.
  • openapi-zod-client creates Zod schemas for runtime validation plus TypeScript types from the same OpenAPI source.
  • nullable: true in OpenAPI 3.0 becomes T | null; absent from required becomes optional (?).
  • oneOf maps to TypeScript union (A | B), allOf to intersection (A & B), anyOf to union.
  • Generated types stay in sync with backend — regenerate whenever the API spec changes by running the generate:api script.

What is OpenAPI/Swagger and Why Generate TypeScript Types?

OpenAPI (formerly Swagger) is a language-agnostic specification format for describing RESTful APIs. An OpenAPI document — written in YAML or JSON — defines every endpoint, HTTP method, request parameter, request body schema, and response body schema for your entire API surface. It is the machine-readable contract between your backend and your clients.

When you write TypeScript frontend or backend code that calls a REST API, you face a choice: manually write interfaces that mirror the API contract, or auto-generate them from the OpenAPI spec. Manual interfaces drift out of sync every time the backend changes. Auto-generated types are always accurate because they come directly from the authoritative source.

The benefits of generating TypeScript from OpenAPI are compelling: compile-time errors when you pass the wrong field, autocomplete on every request and response property, refactoring safety across your entire codebase, and zero cost to keep types updated — just re-run the generator.

OpenAPI vs Swagger: What's the Difference?

Swagger 2.0 was the original specification. The OpenAPI Initiative standardized it as OpenAPI 3.0 in 2017, then OpenAPI 3.1 in 2021 (which is now a proper superset of JSON Schema draft 2020-12). The word "Swagger" now typically refers to tooling (Swagger UI, Swagger Editor), while "OpenAPI" refers to the specification. Most modern tools target OpenAPI 3.x; Swagger 2.0 is still supported for legacy projects.

OpenAPI Specification Basics: YAML/JSON Schema Structure

An OpenAPI 3.x document has a predictable top-level structure. Understanding it helps you read generated types and troubleshoot codegen issues.

SectionPurposeTypeScript Impact
infoAPI title, version, descriptionNone (metadata only)
pathsAll endpoints with methods, params, bodiesMain source of request/response types
components/schemasReusable data models ($ref targets)Generates named interfaces
components/parametersReusable query/path/header paramsGenerates param types
components/responsesReusable response shapesGenerates response types
components/securitySchemesAuth methods (Bearer, OAuth, API key)Not directly typed

A minimal OpenAPI 3.0 YAML document defining one GET endpoint looks like this:

openapi: "3.0.3"
info:
  title: Users API
  version: "1.0.0"
paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: A user object
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      type: object
      required: [id, name, email]
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
        avatar:
          type: string
          nullable: true

From this spec, openapi-typescript generates a User interface withid,name, andemail as required strings, and avatar asstring | null.

openapi-typescript: The CLI Tool

openapi-typescript (npm package) is the most popular tool for generating TypeScript types from OpenAPI specs. It is maintained by the Drizzle team (previously by Drew Powers) and focuses on generating types only — no runtime code, no HTTP client, just pure TypeScript interfaces.

Install and run:

# Install as a dev dependency
npm install -D openapi-typescript typescript

# Generate types from a local YAML file
npx openapi-typescript schema.yaml -o src/api/types.ts

# Generate types from a remote URL
npx openapi-typescript https://api.example.com/openapi.json -o src/api/types.ts

# Use with authentication header
npx openapi-typescript https://api.example.com/openapi.json \
  --header "Authorization: Bearer ${API_TOKEN}" \
  -o src/api/types.ts

The output file uses a paths namespace that mirrors your API routes, plus a components namespace for reusable schemas. Here is what the generated output looks like for the Users API above:

// Auto-generated by openapi-typescript — do not edit manually
export interface paths {
  "/users/{id}": {
    get: operations["getUser"];
  };
}

export interface operations {
  getUser: {
    parameters: {
      path: {
        id: string;
      };
    };
    responses: {
      200: {
        content: {
          "application/json": components["schemas"]["User"];
        };
      };
    };
  };
}

export interface components {
  schemas: {
    User: {
      id: string;
      name: string;
      email: string;
      avatar: string | null;
    };
  };
}

You can then use these types directly. The openapi-fetch companion library (from the same maintainer) provides a tiny typed fetch wrapper that consumes thepaths type automatically.

@hey-api/openapi-ts: Modern Full API Client Generator

@hey-api/openapi-ts is the modern successor to the older openapi-typescript-codegen package. Unlike openapi-typescript, it generates a full client: typed service functions, models, and optionally Zod validators.

# Install
npm install -D @hey-api/openapi-ts @hey-api/client-fetch

# Generate client
npx @hey-api/openapi-ts \
  --input https://api.example.com/openapi.json \
  --output src/api \
  --client @hey-api/client-fetch

The generated output includes:

  • src/api/models.ts — TypeScript interfaces for all schemas
  • src/api/services.gen.ts — Typed async functions for every operation
  • src/api/types.gen.ts — Request/response type helpers

Usage after generation:

import { UsersService } from './api/services.gen';
import { client } from '@hey-api/client-fetch';

// Configure once
client.setConfig({ baseUrl: 'https://api.example.com' });

// Fully typed call — no manual type annotations needed
const { data, error } = await UsersService.getUser({ path: { id: '123' } });
// data is typed as User | undefined
// error is typed as ApiError | undefined

The key difference from openapi-typescript: you get ready-to-call functions rather than just types to annotate your own fetch calls. The trade-off is more generated code and tighter coupling to the generator's HTTP client.

swagger-typescript-api: Template-Based Generator

swagger-typescript-api is another popular code generator that uses ETA templates to produce customizable TypeScript API clients. It supports Swagger 2.0 and OpenAPI 3.x, and generates both types and an HTTP client class.

npm install -D swagger-typescript-api

npx swagger-typescript-api \
  -p https://petstore3.swagger.io/api/v3/openapi.json \
  -o ./src/api \
  -n myApi.ts \
  --axios

The --axios flag generates an Axios-based client instead of the built-in fetch client. You can also supply custom ETA templates via--templates ./templates to completely control the output shape — useful when your team has strong opinions about client structure.

ToolOutputRuntime CodeBest For
openapi-typescriptTypes onlyNoMaximum flexibility, use any HTTP lib
@hey-api/openapi-tsTypes + clientYesBatteries-included, opinionated projects
swagger-typescript-apiTypes + clientYesCustom templates, Axios projects
openapi-zod-clientZod schemas + typesYesRuntime validation + compile-time safety

Zod Schema Generation from OpenAPI: openapi-zod-client

TypeScript types are erased at runtime. If you want to validate that an API response actually matches your schema — not just at compile time, but when the bytes arrive — you need runtime validation. Zod is the most popular TypeScript-first validation library, and openapi-zod-client bridges OpenAPI to Zod automatically.

npm install -D openapi-zod-client

npx openapi-zod-client ./openapi.yaml -o ./src/api/client.ts

The generated file uses zodios or plain Zod schemas depending on configuration. A generated schema for the User model looks like:

import { z } from 'zod';

// Auto-generated — do not edit
export const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  avatar: z.string().nullable(),
});

export type User = z.infer<typeof UserSchema>;

// Use in your fetch function:
async function getUser(id: string): Promise<User> {
  const res = await fetch(`/users/${id}`);
  const data = await res.json();
  return UserSchema.parse(data); // throws if API response doesn't match
}

The advantage: if the backend starts returning an unexpected field or changes a type, your application throws a clear validation error immediately rather than silently passing bad data deeper into your UI.

Using Generated Types in Fetch Calls

The most lightweight approach is using openapi-typescript for types and openapi-fetch as a tiny typed wrapper around the native Fetch API. This keeps your bundle small while giving full type safety.

// 1. Generate types
// npx openapi-typescript schema.yaml -o src/api/types.ts

// 2. Install openapi-fetch
// npm install openapi-fetch

// 3. Create a typed client
import createClient from 'openapi-fetch';
import type { paths } from './types';

const api = createClient<paths>({ baseUrl: 'https://api.example.com' });

// 4. Make typed requests
async function loadUser(userId: string) {
  const { data, error } = await api.GET('/users/{id}', {
    params: { path: { id: userId } },
  });

  if (error) {
    console.error('API error:', error);
    return null;
  }

  // data is fully typed as the response body
  console.log(data.name, data.email); // TypeScript knows these exist
  return data;
}

// Typed POST with request body
async function createUser(payload: { name: string; email: string }) {
  const { data, error } = await api.POST('/users', {
    body: payload, // TypeScript checks this against the request body schema
  });
  return { data, error };
}

If you prefer not to use openapi-fetch, you can extract types manually using TypeScript utility types against the generatedpaths interface.

React Query with Generated API Types

TanStack Query (React Query) is the standard for data fetching in React applications. Combining it with OpenAPI-generated types gives you type-safe queries with automatic caching, background refetching, and loading/error states.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import createClient from 'openapi-fetch';
import type { paths, components } from './api/types';

const api = createClient<paths>({ baseUrl: '/api' });

// Typed response extracted from generated types
type User = components['schemas']['User'];

// Custom hook with full type safety
export function useUser(userId: string) {
  return useQuery({
    queryKey: ['users', userId],
    queryFn: async (): Promise<User> => {
      const { data, error } = await api.GET('/users/{id}', {
        params: { path: { id: userId } },
      });
      if (error) throw new Error('Failed to fetch user');
      return data;
    },
  });
}

// Usage in a component
function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useUser(userId);

  if (isLoading) return <div>Loading...</div>;
  if (error || !user) return <div>Error loading user</div>;

  // user.name, user.email are fully typed
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Typed mutation
export function useCreateUser() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: async (payload: { name: string; email: string }) => {
      const { data, error } = await api.POST('/users', { body: payload });
      if (error) throw error;
      return data;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

Handling nullable and optional in OpenAPI 3.x

One of the most common sources of confusion when generating TypeScript from OpenAPI is the distinction between nullable and optional. They are different concepts that map to different TypeScript syntax.

OpenAPI 3.0 / 3.1MeaningTypeScript Output
type: string (in required)Required, cannot be nullname: string
type: string (not in required)Optional, can be absentname?: string
type: string, nullable: true (3.0)Required but can be nullname: string | null
type: [string, null] (3.1)Required but can be nullname: string | null
type: string, nullable: true (not in required)Optional and can be nullname?: string | null

In OpenAPI 3.1, the nullable keyword is deprecated in favor of JSON Schema's type: [string, null] array syntax. openapi-typescript handles both styles and generates the correct T | null union.

oneOf, anyOf, allOf in TypeScript

OpenAPI's composition keywords map directly to TypeScript's type algebra:

# OpenAPI YAML
components:
  schemas:
    # oneOf: exactly one of these schemas must match
    # → TypeScript union: Cat | Dog
    Pet:
      oneOf:
        - $ref: '#/components/schemas/Cat'
        - $ref: '#/components/schemas/Dog'

    # allOf: must satisfy ALL schemas (composition/inheritance)
    # → TypeScript intersection: BaseEntity & UserDetails
    FullUser:
      allOf:
        - $ref: '#/components/schemas/BaseEntity'
        - $ref: '#/components/schemas/UserDetails'

    # anyOf: must satisfy one or more schemas
    # → TypeScript union (same as oneOf in TS, no runtime difference)
    StringOrNumber:
      anyOf:
        - type: string
        - type: number

The generated TypeScript:

// oneOf → union
export type Pet = Cat | Dog;

// allOf → intersection
export type FullUser = BaseEntity & UserDetails;

// anyOf → union
export type StringOrNumber = string | number;

For oneOf with adiscriminator, the generator can produce discriminated unions — TypeScript's most powerful way to narrow union types:

# OpenAPI with discriminator
Event:
  oneOf:
    - $ref: '#/components/schemas/ClickEvent'
    - $ref: '#/components/schemas/KeyEvent'
  discriminator:
    propertyName: type

ClickEvent:
  type: object
  required: [type, x, y]
  properties:
    type:
      type: string
      enum: [click]
    x: { type: number }
    y: { type: number }

KeyEvent:
  type: object
  required: [type, key]
  properties:
    type:
      type: string
      enum: [key]
    key: { type: string }

# Generated TypeScript → discriminated union
type ClickEvent = { type: 'click'; x: number; y: number };
type KeyEvent = { type: 'key'; key: string };
type Event = ClickEvent | KeyEvent;

// TypeScript narrows automatically:
function handleEvent(event: Event) {
  if (event.type === 'click') {
    console.log(event.x, event.y); // OK — x, y exist
  } else {
    console.log(event.key);         // OK — key exists
  }
}

Enums and const Values from OpenAPI

OpenAPI supports two ways to express constrained string values: enum arrays and const (single value).openapi-typescript converts them to TypeScript literal union types rather than TypeScript enums — a deliberate choice because TypeScript enums have some quirks at runtime.

# OpenAPI YAML
UserStatus:
  type: string
  enum: [active, inactive, suspended]

Priority:
  type: string
  const: high

# Generated TypeScript
type UserStatus = 'active' | 'inactive' | 'suspended';
type Priority = 'high';

If you prefer TypeScript enums (e.g., for reverse lookup), most generators support an--enum flag. However, literal union types are generally preferred in modern TypeScript because they are simpler, produce no runtime code, and work perfectly with discriminated unions.

Validation at Runtime vs Compile-Time

TypeScript's type system operates at compile time only. Once your code is compiled to JavaScript and runs in a browser or Node.js, all type information is erased. This means:

  • An API returning { age: "twenty" } instead of { age: 20 } will not cause a TypeScript error at runtime
  • Only Zod (or similar) validation actually checks the data shape at runtime
  • openapi-typescript alone is compile-time only; openapi-zod-client adds runtime safety
ApproachCompile-Time CheckRuntime CheckBundle Size
openapi-typescript + fetchYesNoMinimal
openapi-typescript + openapi-fetchYesNoTiny (~2KB)
openapi-zod-clientYesYesZod (~12KB)
@hey-api/openapi-tsYesOptionalMedium

For public-facing APIs where you control the schema, compile-time types alone are usually sufficient. For third-party APIs or any case where you want to catch contract violations immediately, add Zod runtime validation via openapi-zod-client.

Online Tools for JSON and TypeScript

When you have a JSON sample of an API response and want to quickly prototype TypeScript types without a full OpenAPI spec, an online converter is the fastest option. DevToolBox offers a free JSON to TypeScript converter that instantly generates TypeScript interfaces from any JSON object or array — useful for:

  • Bootstrapping types from a real API response before writing an OpenAPI spec
  • Quick prototyping when you only have sample JSON, not a formal schema
  • Validating what your generated OpenAPI types should look like
  • Teaching TypeScript shapes interactively

The workflow: paste a JSON response sample into the tool, copy the generated interfaces, then refine them by adding your OpenAPI spec constraints (nullable, optional, enums) by hand. This is much faster than writing interfaces from scratch.

Common Pitfalls in OpenAPI TypeScript Generation

1. allOf Merging Doesn't Work Like Expected

When you use allOf to combine schemas, the generated TypeScript intersection type (A & B) is structurally different from what you might expect. If both A andB have a property with the same name but different types, TypeScript will create an intersection of those types — which may be never if they are incompatible. Always ensure shared property names have compatible types in your allOf schemas.

2. Discriminated Unions Need a Discriminator

Without a discriminator in your OpenAPI spec,oneOf generates a plain union type that TypeScript cannot narrow automatically. Add a discriminator.propertyName pointing to a field (usually type) that has a uniqueenum value in each variant schema. This lets TypeScript narrow in if/switch statements.

3. Nullable vs Optional Confusion

A frequent mistake is using nullable: true when you mean "this field might not be present in the response". In OpenAPI,nullable only means the field's value can be null when present. To make a field absent-able, omit it from the required array. The two are independent and you may need both.

4. Missing $ref Resolution

If your spec uses $ref pointing to external files (e.g., ./schemas/user.yaml), the generator must be run from the correct directory and must have access to those files. Remote$ref URLs also need network access. If generation fails with "could not resolve $ref", check file paths and network access. You can useswagger-cli bundle to flatten all$refs into a single file first.

5. Stale Generated Types

Generated type files must be regenerated whenever the API spec changes. Forgetting to regenerate is the number one pitfall — you get false compile-time confidence while your actual API contract has changed. Automate regeneration in CI by running the generator and checking for diff. Consider treating the generated file as an artifact of the build, not a committed file, to prevent this.

Need to generate TypeScript types from JSON?

Use our free online JSON to TypeScript converter — paste any JSON object or array and get TypeScript interfaces instantly.

Try JSON to TypeScript Tool \u2192

Frequently Asked Questions

Q: What is openapi-typescript?

openapi-typescript is a CLI tool and Node.js library that reads an OpenAPI 3.x (or Swagger 2.0) YAML or JSON specification and outputs TypeScript type definitions. It generates readonly interfaces for request bodies, response bodies, path parameters, query parameters, and headers — all derived from your API schema automatically. Unlike code generators that produce full client code, openapi-typescript produces only types, letting you use them with any HTTP library.

Q: How do I run openapi-typescript?

Install it with npm install -D openapi-typescript typescript, then run: npx openapi-typescript schema.yaml -o src/api/types.ts. It reads YAML or JSON from a local file path or a remote URL (e.g., https://petstore3.swagger.io/api/v3/openapi.json). The output file contains all types under a namespace matching your spec paths. You can also import it programmatically: import openapiTS from "openapi-typescript"; and call it with a URL or parsed schema object.

Q: What is the difference between openapi-typescript and @hey-api/openapi-ts?

openapi-typescript generates only TypeScript type definitions — no runtime code. You use the types manually with fetch or axios. @hey-api/openapi-ts (formerly openapi-ts) generates a full API client: typed functions for every endpoint, typed request/response objects, and optional Zod validation. It is the modern replacement for the older swagger-typescript-api when you want a complete ready-to-use client rather than just types. Choose openapi-typescript for minimal footprint and maximum flexibility; choose @hey-api/openapi-ts when you want a batteries-included client.

Q: How does nullable work in OpenAPI TypeScript generation?

In OpenAPI 3.0, nullable: true on a schema creates a union with null in TypeScript — for example, type: string with nullable: true becomes string | null. In OpenAPI 3.1 (which aligns with JSON Schema), you use type: [string, null] or anyOf: [{type: string}, {type: null}] instead. The required array on an object controls whether a property gets the ? optional modifier. A property that is not in required and has nullable: true becomes string | null | undefined in practice, though openapi-typescript treats absence from required as optional (?) and nullable as | null independently.

Q: How do I use generated types with fetch?

After generating types with npx openapi-typescript schema.yaml -o types.ts, import the paths type and use createFetchClient or manual type assertions. For example: import type { paths } from "./types"; type UserResponse = paths["/users/{id}"]["get"]["responses"]["200"]["content"]["application/json"]; Then annotate your fetch wrapper: async function getUser(id: string): Promise<UserResponse> { const res = await fetch(`/users/${id}`); return res.json(); }. This gives full end-to-end type safety without a generated client.

Q: What is openapi-zod-client?

openapi-zod-client is a tool that generates Zod schemas from an OpenAPI specification instead of plain TypeScript interfaces. This gives you both runtime validation and static TypeScript types from the same source. Run: npx openapi-zod-client ./openapi.yaml -o ./src/api/client.ts. The output includes zodios or plain Zod schema objects you can use to parse API responses at runtime, catching contract violations immediately rather than at the TypeScript compiler level.

Q: How do I keep TypeScript types in sync with the backend?

Add a generate:api script to package.json: {"scripts": {"generate:api": "openapi-typescript https://api.example.com/openapi.json -o src/api/types.ts"}}. Run npm run generate:api whenever the backend API changes. In CI/CD, run this script and commit the diff — or configure the pipeline to fail if the generated file differs from the committed one. Tools like openapi-diff can detect breaking changes between versions. Some teams use a git hook to auto-regenerate before each commit.

Q: Can I generate TypeScript types from a Swagger 2.0 spec?

Yes, openapi-typescript supports Swagger 2.0 (.yaml or .json) though OpenAPI 3.x is strongly recommended for new projects. Swagger 2.0 lacks features like oneOf, anyOf, nullable in the modern sense, and $ref across files is more limited. If you have a Swagger 2.0 spec, consider converting it to OpenAPI 3.0 first using swagger2openapi (npx swagger2openapi swagger.json -o openapi3.json), then generating types from the converted spec for better TypeScript fidelity.

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

最新情報を受け取る

毎週の開発ヒントと新ツール情報。

スパムなし。いつでも解除可能。

Try These Related Tools

TSJSON to TypeScript{ }JSON FormatterJSON ValidatorGoJSON to Go Struct

Related Articles

JSONからTypeScriptインターフェース:ZodとType Guardの完全ガイド

JSONからTypeScriptインターフェースと型を生成する方法を学びます。interface vs type alias、optional、union型、zodを網羅。

JSONからGoの構造体へ:2026年完全変換ガイド

jsonタグ、ネストされた型、nullableポインタを使ってJSONをGo構造体に変換する方法を学びます。json.Unmarshal、json.Decoder、カスタムマーシャリングを網羅。