---
title: "Add TypeScript types"
description: "Generate types from your frontmatter so get(), list(), and query() know the shape of your pages."
canonical_url: "https://content.comark.dev/guide/typescript"
---
# Add TypeScript types

> Generate types from your frontmatter so get(), list(), and query() know the shape of your pages.

Out of the box, `page.data` is a `Record<string, any>`. That's enough to get going, but `page.data.titel` compiles, and nothing autocompletes. Comark Content can infer the shape of your frontmatter from your actual files and write it as a TypeScript declaration, so `get()`, `list()`, and `query()` return typed data.

This page is optional. Come back to it once you have a page rendering and want your editor to catch mistakes.

## Generate the types

::steps{level="3"}
### Run the command

From your project root, with a `content.ts` (or `server/content.ts`, `src/lib/content.ts`, and a few other [common locations](https://content.comark.dev/reference/cli#comark-content-prepare)) that exports your instance:

```bash [Terminal]
npx comark-content prepare
```

It reads every page's frontmatter, infers one schema per instance, and writes `comark-content.d.ts` next to your `package.json`. Pass `--config path/to/content.ts` if the file lives elsewhere, and `-o` to change the output path.

### Include the file

Make sure `tsconfig.json` picks it up. Most configs already include `**/*.ts`; add it explicitly if yours lists files:

```json [tsconfig.json]
{
  "include": ["comark-content.d.ts", "**/*.ts"]
}
```

### Ignore and regenerate

The file is derived from your content, so don't commit it. Add it to `.gitignore` and regenerate it where you run your other checks:

```json [package.json]
{
  "scripts": {
    "dev": "comark-content prepare && next dev",
    "build": "comark-content prepare && next build"
  }
}
```
::

## What you get

Paths and data both narrow, with no type parameter and no hand-written interface:

```ts
// Known path: autocompletes, and `data` has the shape of your frontmatter
const page = await content.get('/blog/hello')
page?.data.title // string (or string | undefined when some pages omit it)
page?.data.tags // string[] | undefined
page?.data.titel // Error: Property 'titel' does not exist

// Listings narrow the same way
const posts = await content.list()
posts[0].data.date // string | undefined

// A path built at runtime isn't known: pass the type yourself
const other = await content.get<DefaultData>(`/blog/${slug}`)
```

`DefaultData` is the generated interface for the unnamed (`default`) instance. A named instance gets a PascalCase name: `comarkContent('blog', …)` produces `BlogData`. With the [`sqlQuery` plugin](https://content.comark.dev/plugins/built-in/sql-query), the same schema types the columns you can filter on.

Edit a file, add a field, re-run `prepare`, and the types follow.

## Let Vite do it

With the [Vite plugin](https://content.comark.dev/integrations/vite), types are generated on dev-server start and at build. There is nothing to run:

```ts [vite.config.ts]
import { defineConfig } from 'vite'
import comark from 'comark-content/vite'
import { content } from './content'

export default defineConfig({
  plugins: [comark({ content })],
})
```

The `types` option is on by default. Pass `types: false` to turn it off, or `types: { outDir }` to write the file elsewhere. A failure to generate types only logs a warning; it never fails your build.

## Declare a schema instead

Inference reads what your files contain today. When you'd rather state the contract, pass a JSON Schema to the source:

```ts [content.ts]
export const content = comarkContent({
  source: fs('./content', {
    schema: {
      type: 'object',
      required: ['title'],
      properties: {
        title: { type: 'string' },
        date: { type: 'string', format: 'date' },
        draft: { type: 'boolean' },
      },
    },
  }),
})
```

`prepare` uses the declared schema for that instance, and the [`schemaValidation` plugin](https://content.comark.dev/plugins/built-in/schema-validation) can enforce it at load time.

## How it works

You can stop reading here. The rest of this page is for when you write scripts around type generation or hit a type error involving the instance itself.

Generated types work through **module augmentation**. `comark-content` exports two empty interfaces, and `comark-content.d.ts` fills them in:

- **`ContentRegistry`** maps each [instance name](https://content.comark.dev/guide/files-and-paths#internal-identifiers) to its `{ data, row }` shape.
- **`ContentPathsRegistry`** maps each instance name to the set of paths it contains, which is what makes `content.get('/blog/hello')` narrow without a type parameter. Paths are lowercased; differently cased literals narrow to the same type.

Each instance narrows against its own entry, so two instances can both contain `/index` without colliding:

```ts
const blog = comarkContent('blog', { source: fs('./content/blog') })
const docs = comarkContent('docs', { source: fs('./content/docs') })

const post = await blog.get('/hello') // data: BlogData
const guide = await docs.get('/hello') // data: DocsData
```

### `writeSourceTypes()`

The CLI and the Vite plugin both call one function, which you can call yourself from a build script:

```ts [scripts/generate-types.ts]
import { writeSourceTypes } from 'comark-content/build'
import { content } from '../content'

await writeSourceTypes(content, {
  outFile: 'types/comark-content.d.ts',
  moduleName: 'comark-content',
  paths: true,
})
```

- `content`: the instance to read the schema from. Pass an array of instances, or a [`contentHub()`](https://content.comark.dev/reference/content-hub), to declare them all in one file.
- `options?`: `outDir`/`fileName`/`outFile` (default `./comark-content.d.ts`), `moduleName` (default `comark-content`), and `paths` (default `true`).

It returns the generated file contents. `generateSourceTypes(content, options)`, exported from the main `comark-content` entry, is the same function without the file write, for when you want the string and will store it yourself.

### Derive the instance type, don't annotate it

The bare `ComarkContent` type can't hold a concrete instance. All three of these fail to typecheck:

```ts [wrong.ts]
// A lazy singleton
let content: ComarkContent | undefined
content = comarkContent({ source: fs('./content') })
// Type 'ComarkContent<…, "default">' is not assignable to type 'ComarkContent<…, string>'

// A registry of instances
const previews = new Map<string, Promise<ComarkContent>>()

// A helper that takes "any instance"
function buildSearch(content: ComarkContent) {}
buildSearch(comarkContent('blog', { source: fs('./content/blog') }))
```

The instance name is a type parameter that feeds the conditional types behind `get()` and `list()`; that's how `'blog'` narrows to `BlogData`. A type parameter used that way is invariant, so `ComarkContent<_, 'blog'>` isn't a subtype of `ComarkContent<_, string>`. Running `prepare` doesn't change this; the failure is about the name, not the registry.

Derive the type from the value instead:

```ts [right.ts]
export const content = comarkContent({ source: fs('./content') })
export type Content = typeof content

// When the instance comes from a factory
export function createContent(ref: string) {
  return comarkContent({ source: github({ repo: 'acme/docs', branch: ref }) })
}
export type Content = Awaited<ReturnType<typeof createContent>>

let content: Content | undefined
const previews = new Map<string, Promise<Content>>()
```

For a helper that accepts *any* instance, use `AnyComarkContent` from `comark-content`. It's `ComarkContent<any, any>` plus an open record for plugin methods, so inside the helper `get()` returns untyped data and every plugin method typechecks whether or not it exists:

```ts [any.ts]
import type { AnyComarkContent } from 'comark-content'

function buildSearch(content: AnyComarkContent) {}
```

### Plugin metadata

Metadata declared by Comark plugins is inferred from the instance configuration; it needs no generated declarations. For example, the `headings` plugin makes `meta.title` and `meta.description` available as `string | undefined` on `get()`, `list()`, and `stat()` results:

```ts [content.ts]
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import headings from 'comark/plugins/headings'

export const content = comarkContent({
  source: fs('./content'),
  markdown: {
    plugins: [headings()],
  },
})
```

The same inference works when you configure the [`markdown` plugin](https://content.comark.dev/plugins/built-in/markdown) explicitly:

```ts [content.ts]
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import markdown from 'comark-content/plugins/markdown'
import headings from 'comark/plugins/headings'

export const content = comarkContent({
  source: fs('./content'),
  plugins: [
    markdown({
      comark: {
        plugins: [headings()],
      },
    }),
  ],
})
```

### Semantic field types

Source schemas support vendor extensions on [`JsonSchema`](https://content.comark.dev/reference/types/content#option-and-method-types) via `x-content`. The [`markdown-fields`](https://content.comark.dev/plugins/built-in/markdown-fields) plugin parses string fields marked `{ type: 'markdown' }` into `MarkdownDocument` at runtime and adjusts generated data types when registered. Import `markdownField()` from `comark-content/plugins/markdown-fields`, not the main package entry.

See [Registry and query types](https://content.comark.dev/reference/types/content#registry-and-query-types) for every generated and derived type.


## Sitemap

See the full [sitemap](https://content.comark.dev/sitemap.md) for all pages.
