OpenAPI to TypeScript: Auto-Generate Types from Your API Spec
July 7, 2026 · 9 min read
OpenAPI to TypeScript: Auto-Generate Types from Your API Spec
Every team that ships a REST API eventually faces the same maintenance trap: TypeScript interfaces defined by hand, slowly drifting out of sync with the actual API contract. A backend engineer renames a field, the spec gets updated, and the frontend keeps compiling — right up until runtime, when user.userName blows up because the field is now user.username. The fix is not better discipline. The fix is eliminating the manual step entirely by generating TypeScript types straight from your OpenAPI 3.0 spec. This guide walks through exactly how to do that, keep the output in sync automatically, and handle the edge cases that trip up most teams.
Why Hand-Written API Types Are a Liability
Writing TypeScript interfaces manually feels productive the first time. You model the /users response, add the Order type, wire up the CreateProductRequest body — it takes an hour and everything type-checks cleanly. The problem surfaces over the next six months.
APIs change. Fields get added, renamed, or made optional. A nullable string | null becomes string | undefined because someone switched ORMs. A nested object gets extracted into a shared $ref. None of these changes break your hand-written types at compile time; they just silently let incorrect assumptions propagate into production.
The real cost shows up in code reviews and incident postmortems. Teams spend time chasing "why is this field undefined?" bugs that should be impossible in a typed language. Junior engineers copy-paste outdated interfaces because they don't know which file is the authoritative source. The TypeScript promise — catch errors at compile time — breaks down the moment your type definitions are not the spec.
The numbers bear this out: in a codebase with 50 API endpoints, a conservative estimate of 3–5 hand-maintained type files covering request bodies, response shapes, and path parameters means 150–300 individual field definitions. Any of them can drift. Generating them from a single OpenAPI document reduces that to zero definitions to maintain manually.
What OpenAPI 3.0 Gives You
OpenAPI 3.0 is a JSON or YAML document that describes every endpoint in your API: the path, the HTTP method, the request body schema, the response schemas for each status code, reusable component schemas, and security requirements. It is machine-readable by design.
A minimal schema component looks like this:
components:
schemas:
User:
type: object
required:
- id
- email
properties:
id:
type: integer
email:
type: string
format: email
displayName:
type: string
That schema carries enough information to produce a TypeScript interface with the correct required/optional split. id and email are required; displayName is optional. The format: email hint can be preserved as a branded type. Enums, oneOf, allOf, anyOf, $ref references — all of it maps to TypeScript constructs.
Most backend frameworks can emit this spec automatically. FastAPI, Spring Boot, NestJS, Rails with rswag, Laravel with L5-Swagger — they all derive the spec from your route definitions and annotations. That means the spec is already being generated. The only gap is the last mile: turning that spec into TypeScript types your frontend can import.
If you already have JSON schemas for your data models but no OpenAPI spec yet, use the JSON to OpenAPI Schema converter to generate a starting point, then layer in the path definitions.
Setting Up openapi-typescript
The most widely used tool for this job is openapi-typescript, maintained by Drizzle co-creator Drew Powers. It requires Node 18+, has zero runtime dependencies, and supports both local spec files and remote URLs.
Install it as a dev dependency:
npm install --save-dev openapi-typescript typescript
Add a generation script to package.json:
{
"scripts": {
"gen:types": "openapi-typescript ./openapi.yaml -o ./src/types/api.d.ts"
}
}
Run npm run gen:types and the output file contains a single paths interface with every endpoint and its request/response shapes, plus a components namespace with every schema from components/schemas.
The generated output for the User schema above looks like this:
export interface components {
schemas: {
User: {
id: number;
email: string;
displayName?: string;
};
};
}
No external dependencies are imported. No runtime validation is generated. The output is pure TypeScript type declarations — it adds zero bytes to your bundle. For teams that want runtime validation too, the types compose cleanly with Zod or Valibot schemas, but that is a separate concern.
Using Generated Types in Your Codebase
The generated paths interface is the main entry point for typed API calls. Instead of writing a custom User interface in three different files, you reference the canonical generated type everywhere:
import type { components, paths } from '@/types/api';
type User = components['schemas']['User'];
type GetUsersResponse =
paths['/users']['get']['responses'][200]['content']['application/json'];
This looks verbose at first. In practice, most teams create thin re-export aliases in a types/index.ts file:
export type { components } from './api';
export type User = components['schemas']['User'];
export type CreateUserBody =
paths['/users']['post']['requestBody']['content']['application/json'];
Now the rest of the codebase imports from @/types and never needs to know where the definitions originated. You get the ergonomics of hand-written types with zero maintenance overhead.
For fetch wrappers, you can type the response inline using the path types:
async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json() as User;
}
Teams using openapi-fetch — the companion library — get end-to-end type safety on the request and response without any casting, which is worth exploring once the generation workflow is stable. Our OpenAPI to TypeScript converter lets you paste a spec and inspect the output before committing to the toolchain.
Keeping Types in Sync Automatically
The biggest risk after setting up generation is forgetting to re-run it. The spec changes, someone merges the PR, and the generated file is now two weeks stale. The solution is to make generation part of your CI pipeline and fail the build if the output is out of date.
Add a check step to your GitHub Actions workflow:
- name: Generate API types
run: npm run gen:types
- name: Check for uncommitted type changes
run: |
if ! git diff --quiet src/types/api.d.ts; then
echo "Generated types are out of sync with the OpenAPI spec."
echo "Run 'npm run gen:types' and commit the result."
exit 1
fi
This runs generation from scratch and then checks whether the output matches what is committed. If a backend engineer updates the spec without regenerating, CI catches it. The fix is a single command, not a debugging session.
For monorepos where the OpenAPI spec lives in the backend package and the types need to land in the frontend package, point the generation script at the backend output path:
{
"scripts": {
"gen:types": "openapi-typescript ../backend/dist/openapi.json -o ./src/types/api.d.ts"
}
}
Teams that version their API spec separately can point directly at a URL: openapi-typescript https://api.example.com/openapi.json. This works in CI too as long as the environment can reach the spec endpoint.
Handling Complex Schemas
Real API specs are rarely just flat objects. allOf, oneOf, and anyOf compositors map to TypeScript intersections and unions, which is where most generation tools diverge in quality.
A polymorphic response using oneOf:
components:
schemas:
PaymentMethod:
oneOf:
- $ref: '#/components/schemas/CreditCard'
- $ref: '#/components/schemas/BankTransfer'
discriminator:
propertyName: type
openapi-typescript emits this as:
PaymentMethod: components['schemas']['CreditCard'] | components['schemas']['BankTransfer'];
The discriminator field — type in this case — is preserved in each branch's schema, so TypeScript's narrowing works correctly:
function processPayment(method: PaymentMethod) {
if (method.type === 'credit_card') {
// TypeScript knows this is CreditCard here
console.log(method.last4);
}
}
Nullable fields deserve attention. OpenAPI 3.0 uses nullable: true on a property; OpenAPI 3.1 uses type: ['string', 'null']. Both map to string | null in the TypeScript output, but only if you are using a tool that handles both spec versions. Check which version your backend emits — the openapi field at the top of the document says "3.0.x" or "3.1.x" — and confirm your tooling supports it before you commit to a pipeline.
For specs that start as raw JSON rather than YAML, you can use the JSON to TypeScript converter to verify your schema structure before piping it into the full OpenAPI workflow.
Integrating with Editors and Type Checking
The generation workflow only pays off fully if your editor resolves the generated types without friction. Two things matter: the tsconfig.json must include the output directory, and the generated file must not be excluded by .gitignore.
A minimal tsconfig.json setup:
{
"compilerOptions": {
"strict": true,
"paths": {
"@/types": ["./src/types/index.ts"]
}
},
"include": ["src"]
}
With strict: true enabled, TypeScript will flag every case where you access a property that might not exist on a generated type — exactly the class of bug that hand-written types silently permit. Optional chaining and nullish coalescing become compile-time requirements rather than defensive habits.
One gotcha: if verbatimModuleSyntax is enabled (required in some Next.js 13+ configurations), import generated types with import type rather than import. The generated .d.ts file contains only type declarations, and mixing value and type imports in that context triggers a compiler error.
The openapi-typescript CLI also supports a watch mode via --watch for local development:
npx openapi-typescript ./openapi.yaml -o ./src/types/api.d.ts --watch
This regenerates on every spec change, giving you near-instant feedback when a backend engineer modifies a schema while you are actively building against it.
Conclusion
Auto-generating TypeScript types from your OpenAPI spec is one of the highest-leverage tooling decisions a frontend or full-stack team can make. The initial setup takes under an hour. After that, the feedback loop from API change to type error drops from days to seconds, the number of hand-maintained type definitions drops to zero, and the class of bugs caused by stale interfaces disappears from your backlog.
The workflow is straightforward: generate from a single spec file using openapi-typescript, commit the output, and add a CI check that fails if the generated file drifts. Complex schemas — polymorphic types, nullable fields, nested $ref chains — are handled automatically. The types compose with any fetch library and require no runtime dependency.
Use the OpenAPI to TypeScript converter to try generation directly in the browser before wiring up your pipeline. If you are starting from raw JSON schemas, the JSON to OpenAPI Schema tool helps you scaffold the spec, and JSON to TypeScript covers the simpler case where you only need interfaces without the full OpenAPI contract. Pick the entry point that matches where your data lives today — the destination is the same either way.
Try these free tools
Free & private — all tools run in your browser, nothing uploaded.