TL;DR
使用 openapi-typescript (CLI)、@hey-api/openapi-ts (完整 API 客户端) 或 openapi-zod-client (Zod 模式) 从 OpenAPI 规范生成 TypeScript 类型。结合 fetch 或 React Query 实现类型安全的 API 调用。 立即使用免费的 JSON 转 TypeScript 工具 →
核心要点
openapi-typescriptgenerates TypeScript interfaces from OpenAPI 3.x YAML/JSON specs with zero runtime overhead.npx openapi-typescript schema.yaml -o types.tscreates a complete type file instantly from a local file or remote URL.@hey-api/openapi-tsgenerates full API clients with typed request/response methods for batteries-included usage.openapi-zod-clientcreates Zod schemas for runtime validation plus TypeScript types from the same OpenAPI source.nullable: truein OpenAPI 3.0 becomesT | null; absent fromrequiredbecomes optional (?).oneOfmaps to TypeScript union (A | B),allOfto intersection (A & B),anyOfto union.- Generated types stay in sync with backend — regenerate whenever the API spec changes by running the
generate:apiscript.
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.
| Section | Purpose | TypeScript Impact |
|---|---|---|
| info | API title, version, description | None (metadata only) |
| paths | All endpoints with methods, params, bodies | Main source of request/response types |
| components/schemas | Reusable data models ($ref targets) | Generates named interfaces |
| components/parameters | Reusable query/path/header params | Generates param types |
| components/responses | Reusable response shapes | Generates response types |
| components/securitySchemes | Auth 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: trueFrom 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.tsThe 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 schemassrc/api/services.gen.ts— Typed async functions for every operationsrc/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 | undefinedThe 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.
| Tool | Output | Runtime Code | Best For |
|---|---|---|---|
| openapi-typescript | Types only | No | Maximum flexibility, use any HTTP lib |
| @hey-api/openapi-ts | Types + client | Yes | Batteries-included, opinionated projects |
| swagger-typescript-api | Types + client | Yes | Custom templates, Axios projects |
| openapi-zod-client | Zod schemas + types | Yes | Runtime 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.1 | Meaning | TypeScript Output |
|---|---|---|
| type: string (in required) | Required, cannot be null | name: string |
| type: string (not in required) | Optional, can be absent | name?: string |
| type: string, nullable: true (3.0) | Required but can be null | name: string | null |
| type: [string, null] (3.1) | Required but can be null | name: string | null |
| type: string, nullable: true (not in required) | Optional and can be null | name?: 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: numberThe 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
| Approach | Compile-Time Check | Runtime Check | Bundle Size |
|---|---|---|---|
| openapi-typescript + fetch | Yes | No | Minimal |
| openapi-typescript + openapi-fetch | Yes | No | Tiny (~2KB) |
| openapi-zod-client | Yes | Yes | Zod (~12KB) |
| @hey-api/openapi-ts | Yes | Optional | Medium |
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常见问题
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.