Beta You're reading the docs for Kubb v5, which is currently in beta. View the stable v4 docs
Skip to content

TypeScript

Kubb is built in TypeScript end to end, and that choice shapes how you work with it. The point is not that the source happens to be typed. It is that types travel with you: from the config you write, through the plugins and adapters that run, down to the AST nodes a generator visits. When the compiler already knows the shape of everything in the pipeline, IntelliSense can lead you through each choice and most mistakes surface before generation ever runs.

Every public surface takes a generic that pins down the same four things: the user-facing options, the resolved options after defaults, the plugin name, and the resolver shape. That generic threads through defineConfig, definePlugin, defineParser, createAdapter, defineGenerator, and the AST factories. Declare it once and the rest of the surface follows from it.

Inference starts at the config

You rarely write a type annotation. The kubb.config.ts entry point is where inference begins, and everything downstream reads from it. Pass a plugin to defineConfig and its options are checked against that plugin's own Options type, so a typo in pluginTs is a compiler error, not a silent no-op at runtime.

kubb.config.ts
typescript
import {  } from 'kubb'
import {  } from '@kubb/plugin-ts'

export default ({
  : { : './petStore.yaml' },
  : { : './src/gen', : true },
  : [
    ({
      : { : 'models' },
      // Hover any option to see the inferred type.
    }),
  ],
})

Hovering an option shows the type the compiler resolved for it. That feedback loop is the whole idea: the config is the source of truth, and the editor reflects back what Kubb will actually do with it.

Why strict mode matters

Kubb assumes TypeScript strict mode, and a few of its APIs only behave the way you expect with it on. All exported types compile cleanly under "strict": true. The AST node guards and resolvers in particular rely on strictNullChecks, because that is what lets the compiler treat a possibly-absent value as possibly-absent and narrow it once you check.

tsconfig.json
json
{
  "compilerOptions": {
    "strict": true,
    "moduleResolution": "bundler",
    "module": "ESNext",
    "target": "ES2022"
  }
}

IMPORTANT

If you cannot enable full strict, enable at least strictNullChecks. Without it, RefSchemaNode.ref and resolver helpers return widened types and you cast manually.

How a plugin's types thread through the pipeline

A plugin is described by a single generic, PluginFactoryOptions. It is the contract between the options a user passes and the values a plugin works with at runtime, and it carries four pieces of information through the plugin lifecycle:

Generic Purpose
TName The plugin's name literal (e.g. 'plugin-ts'). Used by dependencies lookups.
TOptions The user-facing options accepted by the factory.
TResolvedOptions The shape of options after defaults are applied (what runs at runtime).
TResolver The plugin's Resolver extension (pluginName, naming helpers).

The split between TOptions and TResolvedOptions is the part worth understanding. TOptions is what a user is allowed to leave out, and TResolvedOptions is what the plugin sees once defaults are filled in. Declare the alias once and both halves stay in sync across the factory and its hooks.

plugin-example.ts
typescript
import {  } from '@kubb/core'
import type { ,  } from '@kubb/core'

type  = { ?: string }
type  = <>
type  = <'plugin-example', , , >

export const  = <>(() => {
  const :  = { : . ?? '.ts' }

  return {
    : 'plugin-example',
    : ,
    : {
      'kubb:plugin:end'({  }) {
        // `files` is FileNode[]; no cast required.
        .(`${.} files emitted with suffix ${.}`)
      },
    },
  }
})

TIP

Inside hooks, ctx.options is typed as TResolvedOptions and ctx.config is the fully resolved Config. No casts required.

Adapters extend the same idea

Adapters follow the same pattern with AdapterFactoryOptions, with one extra slot. Alongside TName, TOptions, and TResolvedOptions, an adapter also pins TDocument, the shape of the parsed spec it produces. That is the type every downstream plugin reads from, so getting it right here is what keeps the rest of the pipeline honest.

adapter-example.ts
typescript
import { ,  } from '@kubb/core'
import type {  } from '@kubb/core'

type  = { ?: boolean }
type  = <>
type  = { : <string, unknown> }
type  = <'adapter-example', , , >

export const  = <>(() => ({
  : 'adapter-example',
  : { : . ?? false },
  : null,
  async () {
    return ..()
  },
  () {
    return []
  },
  async () {
    // Throw or call ctx.error here when the spec is invalid.
  },
}))

The same alias flows into Adapter<AdapterExample>, so consumers that import the adapter type get the options and document for free, without redeclaring anything.

Parsers carry their own metadata

Parsers see the files a plugin produced, and each FileNode<TMeta> keeps the metadata its plugin attached. The TMeta generic is how that survives the trip: type the parse parameter and file.meta comes back as your own shape rather than unknown.

parser-typed.ts
typescript
import { ,  } from '@kubb/core'

type  = { : 'ts' | 'tsx' }

export const  = ({
  : 'parser-typed',
  : ['.ts'],
  (: .<>) {
    const  = . // typed as Meta
    return `// ${?. ?? 'unknown'}\n`
  },
  () {
    return ''
  },
})

How the AST narrows

The AST is a set of discriminated unions, which is what makes it safe to walk. The SchemaNode union shares one kind: 'Schema' discriminator and uses node.type to tell variants apart, so once you check a node's type, the compiler knows exactly which fields exist. Two helpers cover the cases the discriminants alone do not: narrowSchema narrows a SchemaNode to a specific variant, and isHttpOperationNode narrows an OperationNode to an HttpOperationNode.

narrow.ts
typescript
import {  } from '@kubb/core'

declare const : .

const  = .(, 'ref')
if (?.) {
  const : string = .
  .()
}

declare const : .
if (.()) {
  // op is now HttpOperationNode. method and path are non-nullable
  const : . = .
}

These are the only two guards @kubb/ast exports. Everything else narrows through the kind and type discriminants directly.

See also

  • Plugins: definePlugin, PluginFactoryOptions, resolvers, generators.
  • Adapters: createAdapter and AdapterFactoryOptions.
  • AST: node types, visitors, guards.
  • Configuration: top-level defineConfig shape.