Skip to content

Generating TypeScript types — cratebase typegen

Cratebase ships TypeScript typegen as a built-in feature of the server binary — no separate package, no Node/Bun toolchain required. It turns your collections into a single .d.ts file with one <Name>Record type per non-system collection (plus <Name>Create/<Name>Update input types), and a Schema/SchemaCreate/SchemaUpdate export shaped for @cratebase/client’s createClient<Schema>(). Three surfaces all call the same generator, so they can never disagree:

  • cratebase typegen — a one-shot CLI command, reading the local data directory’s collections the same way cratebase schema pull does;
  • GET /api/typegen — a superuser-only REST endpoint, and a “Download TypeScript types” button in the dashboard’s API docs tab that calls it;
  • the --dev watch — when the server runs with --dev and CB_TYPEGEN_OUT is set, the file at that path is rewritten every time a collection is created, updated or deleted.
Terminal window
cratebase typegen # writes ./cratebase-types.d.ts
cratebase typegen -o src/cratebase-types.d.ts
cratebase typegen -o - # write to stdout instead of a file
cratebase typegen --dir ./pb_data -o types.d.ts

See the CLI reference for the full flag list.

Superuser-only, returns the generated .d.ts as application/typescript with a Content-Disposition: attachment filename, so a plain authenticated fetch (or the dashboard’s download button) can save it directly:

Terminal window
curl -H "Authorization: Bearer <superuser-jwt>" \
http://localhost:8090/api/typegen -o cratebase-types.d.ts

Set CB_TYPEGEN_OUT (in .env or the environment) and run with --dev, and Cratebase rewrites that path after every collection create/update/delete — including through cratebase schema push and POST /api/schema/apply, since they apply changes through the same code path. A write failure only logs a warning; it never fails the schema change itself.

.env
CB_DEV=true
CB_TYPEGEN_OUT=./src/cratebase-types.d.ts

Given a posts collection with a required title, a multi-select tags, and a relation to authors:

collections (as GET /api/collections returns them)
{
"name": "posts",
"type": "base",
"fields": [
{ "name": "title", "type": "text", "required": true },
{ "name": "tags", "type": "select", "values": ["news", "release"], "maxSelect": 5 },
{ "name": "author", "type": "relation", "collectionId": "<authors id>", "maxSelect": 1, "required": true }
]
}

cratebase typegen writes:

cratebase-types.d.ts
export type PostsRecord = {
id: string;
title: string;
tags?: ("news" | "release")[];
author: string;
expand?: {
author?: AuthorsRecord;
};
readonly created?: string;
readonly updated?: string;
};
export type PostsCreate = {
title: string;
tags?: ("news" | "release")[];
author: string;
};
export type PostsUpdate = Partial<PostsCreate>;
  • select fields become a string literal union ("news" | "release"), an array of the union when maxSelect > 1.
  • relation fields are the related id(s) (a string or string[] by maxSelect), plus an expand? field typed to the target collection’s own <Name>Record — falling back to Record<string, unknown> if the target collection isn’t in the generated set (e.g. a relation to a system collection).
  • file fields are string/string[] (the stored filename(s)); json is unknown unless a narrower type is more useful to hand-edit in; geoPoint is { lon: number; lat: number }; vector is number[]; autodate fields (created/updated) are marked readonly.
  • password fields are excluded from the record type entirely — they never round-trip out of a record read.
  • Every other kind maps straight to its TypeScript scalar (number → number, bool → boolean, …).

<Name>Create and <Name>Update exclude system and autodate fields (the server fills those in); Create respects each field’s required flag, and Update is Partial<Create>. An auth collection’s Create type also gets password/passwordConfirm (required strings), and its record type includes email/emailVisibility/verified. A view collection only gets a record type — it has no Create/Update, since it can’t be written to.

Schema, SchemaCreate, SchemaUpdate — for createClient<Schema>()

Section titled “Schema, SchemaCreate, SchemaUpdate — for createClient<Schema>()”

The generated file also exports Schema (every non-system collection, keyed by name, typed to its own <Name>Record), type Collections = keyof Schema, and SchemaCreate/SchemaUpdate (the same keys, typed to each collection’s <Name>Create/<Name>Update; a view collection maps to never):

cratebase-types.d.ts
export type Schema = {
posts: PostsRecord;
};
export type Collections = keyof Schema;
export type SchemaCreate = {
posts: PostsCreate;
};
export type SchemaUpdate = {
posts: PostsUpdate;
};

Pass Schema alone to createClient<Schema>() for a fully typed client.collection(name) — an unknown collection name is a compile error, and reads resolve to the right <Name>Record with no per-call type argument needed. Pass SchemaCreate/SchemaUpdate too for create()/update() typed to each collection’s actual input shape (required fields respected) instead of Partial<Record>:

import { createClient } from "@cratebase/client";
import type { Schema, SchemaCreate, SchemaUpdate } from "./cratebase-types.js";
const cb = createClient<Schema, SchemaCreate, SchemaUpdate>("http://localhost:8090");
const posts = await cb.collection("posts").list(); // typed PostsRecord[]
await cb.collection("posts").create({ title: "hello", author: "abc123" }); // title/author required

The standalone @cratebase/schema-codegen package

Section titled “The standalone @cratebase/schema-codegen package”

Before cratebase typegen existed, tools/schema-codegen was a separate, Node/Bun-based CLI that did the same job from a checked-in schema-as-code JSON file. It’s superseded by cratebase typegen for any new project, but still works and isn’t going away — useful if you want typegen as a pure-JS build step with no Cratebase binary on hand. See its own README for usage.