Skip to content

Generated types

Your schema already describes every field, its type and whether it is required. trokky generate-types turns that description into TypeScript interfaces, so your frontend’s compiler knows the same things your CMS does.

Terminal window
trokky generate-types -o ./src/types/trokky

The command talks to a running instance. It calls GET /collections for the list of collections, then GET /schemas/<name> for each one, and renders the answers into a single file.

That has two consequences. The server must be running and reachable. And the types describe the schemas that server has loaded — not the schema files in your working tree. If you edited a schema and did not restart the CMS, you will generate the old types and everything will typecheck.

Both endpoints are authenticated, so the command needs credentials: a trokky login, a stored config, --url and --token flags, or the TROKKY_URL and TROKKY_TOKEN environment variables. The CLI covers all four.

--output is a directory, not a file. The command creates it if needed and writes index.ts inside it. Pass -o src/types/trokky.ts and you get a directory called trokky.ts containing index.ts. With no flag the default is ./src/types/trokky.

The file imports nothing and depends on nothing, so it compiles on its own. It opens with the shared types:

// Generated by trokky CLI
// Source: https://cms.example.com/api
// Do not edit manually.
export interface BaseDocument {
_id: string
_type: string
_createdAt?: string
_updatedAt?: string
_version?: number
_status?: 'draft' | 'published'
}

then MediaAssetReference, MediaFieldValue, RichTextValue and Reference<T>, then one interface per collection:

/**
* Post
* Type: document
*/
export interface PostDocument extends BaseDocument {
_type: 'post'
title: string
slug?: string
cover?: MediaFieldValue | null
author?: Reference<'author'> | AuthorDocument
tags?: string[]
}

The interface name is the schema name in PascalCase plus Document, so blog-post becomes BlogPostDocument. A field is optional unless the schema marks it required: true. Fields whose names are not valid TypeScript identifiers are emitted quoted.

The file ends with four helpers derived from the collection list:

export type DocumentType = 'post' | 'author'
export type AnyDocument = PostDocument | AuthorDocument
export interface DocumentTypeMap {
'post': PostDocument
'author': AuthorDocument
}
export type DocumentOf<T extends DocumentType> = DocumentTypeMap[T]

DocumentOf is useful when a component is generic over collections.

Schema field typeTypeScript
string, text, slug, email, url, password, colorstring
number, integer, floatnumber
booleanboolean
date, datetimestring
richtextRichTextValue
portableText, blockContentunknown[]
jsonunknown
media, image, audio, video, fileMediaFieldValue | null
referenceReference<'target'>, plus the target’s interface when that collection exists
objecta generated interface named <Document><Field>
arraythe item type, []-suffixed; arrays of objects get <Document><Field>Item
anything elseunknown

RichTextValue is wrong for most projects. The generator hardcodes it as { type: 'doc'; content?: unknown[] } — a ProseMirror document — but a richtext field defaults to outputFormat: 'html' and stores a plain HTML string. So the field you render with set:html={post.body}, which is the form that works, does not typecheck against the type the generator gave it. Until the generator reads the option, narrow it at the edge: const body = post.body as unknown as string. If you changed outputFormat away from the default, check what your field actually stores before trusting either shape.

Dates are strings because that is what the API returns. A custom field type you registered yourself becomes unknown, which is honest — the generator has no way to know its shape. See Field types.

BaseDocument declares _id: string, required. The Postgres adapter returns _id on every document. The filesystem adapter returns the key as id and never sets _id.

So on a filesystem-backed instance post._id typechecks and is undefined at runtime. Address documents by slug rather than by id — you want that anyway, because restores regenerate ids — and if you genuinely need the identifier on the filesystem adapter, read id.

Every time a schema changes. Adding a field, making one required, changing a reference target, renaming a collection — each of those changes the generated file, and nothing detects the drift for you. A stale file is worse than no file, because it typechecks.

The point of regenerating is the errors it produces. Rename title to heading in a schema, regenerate, and every page reading post.title stops compiling. That is the failure arriving in your terminal instead of on your site.

The output is deterministic, so it is reasonable to commit it and refresh it deliberately:

{
"scripts": {
"types": "trokky generate-types -o ./src/types/trokky",
"build": "astro build"
}
}

Committing the file means your build does not need the CMS running, which matters in CI and matters more when the CMS is behind a VPN. The cost is remembering to run npm run types — put it in the same pull request as the schema change and review the diff.

Generating during the build instead removes the stale-file risk and adds a hard dependency on the CMS being up and the token being valid at build time:

{
"scripts": {
"build": "trokky generate-types -o ./src/types/trokky && astro build"
}
}

Pick one. Doing both means the checked-in file is rewritten on every build and shows up in every diff.

Either way, do not edit the generated file. It is overwritten, and the schema is the definition — see Schemas.

The SDK’s generator is a different thing

Section titled “The SDK’s generator is a different thing”

@trokky/client also exports a TypeGenerator class. It predates the Go CLI, writes one file per schema, and names its interfaces postDocument rather than PostDocument. Types generated by the two tools do not interchange.

trokky generate-types is the supported path. If you have a project on the older output, the names are the thing that will break when you switch.