@contextual-io/typegen
v0.1.1
Published
Types for a Contextual application's contextual.yaml: a TypeScript 7.1 content mapper and generated declarations for earlier compilers.
Readme
@contextual-io/typegen
@contextual-io/typegen is an optional companion to @contextual-io/sdk. It provides TypeScript types for an application's contextual.yaml: typed Object Type handles and records, route parameters, handler signatures, and event destinations. The core SDK is filesystem-free and accepts explicit configuration. It has no typegen, YAML, or TypeScript beta dependency, and works without these files or tools.
With typegen, the application owns contextual.yaml, which selects Object Types, routes, and events. An optional contextual.lock.yaml holds the Registry definitions that give records their shape.
Typegen reads the handler manifest shown below, with app, runtime, objects, http, and events. The CLI's Application deployment manifest and @contextual-io/app's intentVersion: 1 format have their own schemas.
Choose one of two ways to get types. Runtime value imports use a separate, explicit loader:
| Path | Supported compiler or runtime | Output |
| --- | --- | --- |
| Generated declarations | TypeScript 5.9.3 | contextual-typegen writes contextual.d.yaml.ts |
| Opt-in content mapper | TypeScript 7.1.0-beta | Virtual declarations for contextual.yaml, with diagnostics and definitions mapped into the YAML |
| @contextual-io/typegen/register | Node.js 24 | The objects catalog for value imports |
These are the exact tested compiler versions, not a compatibility claim for other TypeScript versions or historical SDK releases. Both static paths emit identical declaration text. Use one at a time, following Switching modes.
Install
The Contextual packages are not published yet. The registry installation commands below apply after publication.
Use Node.js 24 for installation, codegen, the mapper, and the runtime loader. In your application's directory, choose npm, pnpm, or Yarn Classic 1 with a node_modules installation:
# npm
npm install @contextual-io/sdk @contextual-io/api-types @contextual-io/typegen
npm install --save-dev --save-exact [email protected] @types/[email protected]
# pnpm
pnpm add @contextual-io/sdk @contextual-io/api-types @contextual-io/typegen
pnpm add --save-dev --save-exact [email protected] @types/[email protected]
# Yarn Classic 1
yarn add @contextual-io/sdk @contextual-io/api-types @contextual-io/typegen
yarn add --dev --exact [email protected] @types/[email protected]With Yarn Classic 1, use yarn run for the package scripts below. Run the source and compiled clients with the direct node --import ... commands shown; Node resolves their dependencies from node_modules.
The declarations import types from @contextual-io/sdk, so install it explicitly as shown above. Each package has its own version. The api-types version must satisfy the caret ranges in the packed package manifests; ^0.0.1 accepts only 0.0.1. Typegen depends on @contextual-io/api-types, yaml, and zod, which the package manager installs automatically.
Compatibility is checked against the SDK packed alongside typegen by the packed consumer and content mapper acceptance checks. Typegen declares no SDK dependency or peer range. Equal version numbers do not establish compatibility; SDK API changes need acceptance checks against the generated declarations and runtime loader before using a new combination.
Before registry publication, install the required package tarballs together in your application directory:
api_types_version=0.0.1 # set each version from its tarball file name
sdk_version=0.0.1
typegen_version=0.0.1
npm install "./contextual-io-api-types-$api_types_version.tgz" "./contextual-io-sdk-$sdk_version.tgz" "./contextual-io-typegen-$typegen_version.tgz"For core SDK usage alone, omit @contextual-io/typegen and the typegen setup below. For typegen used only during development and builds, you can put it in devDependencies; keep it in runtime dependencies if you use @contextual-io/typegen/register in production.
Quick start
The following setup uses generated declarations with TypeScript 5.9.3. All paths belong to your application:
inventory/
package.json
tsconfig.json
contextual.yaml
contextual.lock.yaml # optional
contextual.d.yaml.ts # generated
src/
items.ts
main.tsCreate contextual.yaml at the application root:
manifestVersion: 1
app:
id: inventory
name: Inventory
runtime: nodejs24
objects:
inventory-item:
operations: [read, create]
http:
auth: none
routes:
getItem:
handler: { file: src/items.ts, export: getItem }
method: GET
path: /items/:idcontextual.lock.yaml, beside it, is optional. Each entry is an Object Type definition captured from Registry, keeping only id, type, objectType, name, display, defaultListStyle and schema:
lockVersion: 1
objects:
inventory-item:
definition:
id: inventory-item
type: custom
objectType: internal
name: Inventory Item
display: default
defaultListStyle: table
schema:
type: object
primaryKey: id
required: [id, title]
properties:
id: { type: string }
title: { type: string }
quantity: { type: integer, minimum: 0 }The route's handler, src/items.ts:
import { HttpResponse } from "@contextual-io/sdk"
import type { Handlers } from "../contextual.yaml"
export const getItem: Handlers["getItem"] = async ({ params }, ctx) => {
const { data } = await ctx.object("inventory-item").get(params.id) // params from the route path
return HttpResponse.json(200, { title: data.title })
}A standalone client, src/main.ts:
import { Contextual } from "@contextual-io/sdk"
import { connectionFromEnv } from "@contextual-io/sdk/env"
import { objects } from "../contextual.yaml"
const client = new Contextual({ ...connectionFromEnv(process.env), objects })
const { data: item } = await client.object("inventory-item").get("one") // the read type from the lock
console.log(item.title, item.quantity)Here, ../contextual.yaml resolves from src/ to your own application root. It does not refer to a file shipped by the SDK. Nested sources can use another parent segment as needed, while staying within that root.
Add the generated-declaration configuration, then run npm run build, pnpm run build, or yarn run build. To run the standalone client from source with Node.js 24:
node --import @contextual-io/typegen/register src/main.tsThe client reads credentials and endpoints from environment input you supply through connectionFromEnv(process.env). The manifest supplies the Object Type catalog, not credentials. A handler such as getItem needs an application-owned host or server to invoke it; typegen does not start one.
What the declarations provide
objectsdeclares each selected Object Type with its operations and primary key. A configured client rejects unselected IDs; handler contexts accept them untyped, as the SDK defines.Recordsholds the read and insert types: a record result'sdatahas the read type, andcreateandreplacetake the insert type. Defaulted and generated fields are present on reads and optional on inserts, and every object level stays open to tenant-added fields. A top-level_metaDataproperty is left out, since the SDK returns it as the result'smetadata. Without a captured definition the record isRecord<string, unknown>. Values in the manifest never become record types.RoutesandHandlerstype route parameters and handler signatures.HandlerChecksverifies each referenced export against its handler type.- The module augments the SDK's
Registerwith the catalog and event destinations for its TypeScript program.
Typegen checks every manifest field but types only objects, http.routes and events. Record types need an internal Object Type whose schema is an object with a declared string primary key. The record compiler supports a bounded JSON Schema subset and refuses the rest with the schema path, rather than approximating it. It does not resolve $ref. Within the subset, some rules have no TypeScript form: value constraints such as minLength, pattern and minimum keep the field's base type, and oneOf becomes a union, so a value that matches more than one alternative still type-checks. The server validates writes against the full schema.
Generated declarations
Use the pinned TypeScript 5.9.3 installed above. Merge these fields into your application's package.json:
{
"type": "module",
"scripts": {
"codegen": "contextual-typegen",
"codegen:check": "contextual-typegen --check",
"build": "contextual-typegen && tsc -p tsconfig.json"
}
}Put this tsconfig.json beside contextual.yaml:
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"allowArbitraryExtensions": true,
"types": ["node"],
"rootDir": ".",
"outDir": "dist"
},
"include": ["src/**/*.ts", "contextual.d.yaml.ts"]
}Package scripts resolve the installed contextual-typegen and tsc executables. Run npm run codegen, pnpm run codegen, or yarn run codegen to write contextual.d.yaml.ts without building. Regenerate after every manifest or lock change; the build script does this before compiling. Handler conformance errors appear in contextual.d.yaml.ts.
Commit the generated declaration if you want CI to detect stale types. Before regenerating in CI, run npm run codegen:check, pnpm run codegen:check, or yarn run codegen:check. It exits 1 if the declaration is missing or out of date. For projects that generate during every build instead, ensure codegen runs before compilation and before pruning development dependencies.
The CLI reads contextual.yaml and the optional lock from the current directory. To target another application directory, pass it as the positional argument, for example contextual-typegen ./inventory or contextual-typegen --check ./inventory from its parent.
Content mapper
The content mapper is an opt-in alternative for TypeScript 7.1.0-beta. Replace the stable compiler with that exact beta using your package manager:
npm install --save-dev --save-exact [email protected]
# or
pnpm add --save-dev --save-exact [email protected]
# or, with Yarn Classic 1
yarn add --dev --exact [email protected]Remove contextual.d.yaml.ts and use this tsconfig.json beside contextual.yaml for type checking:
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"types": ["node"]
},
"contentMappers": [{ "package": "@contextual-io/typegen", "extensions": [".yaml"] }],
"include": ["src/**/*.ts", "contextual.yaml"]
}- Add a package script such as
"typecheck": "tsc --noEmit --runExternalCode", then run it withnpm run typecheck,pnpm run typecheck, oryarn run typecheck. Without--runExternalCode, TypeScript reports TS18068 and does not run the mapper. - Use an editor integration that supports the TypeScript
7.1.0-betalanguage server and content mappers. Select that compiler for the workspace and trust the workspace to permit external mapper code. The editor must pass the equivalentrunExternalCodeinitialization option; installing the package alone does not enable editor support. - The mapper starts with
nodefrom the compiler'sPATH. Ensure that resolves to Node.js 24. The mapper reads the application's YAML and optional lock and emits no JavaScript. - The mapper reads
contextual.lock.yamlfrom besidecontextual.yamlwhen the project opens, reports it as a watched file and fingerprints it, so lock changes refresh--incrementalbuilds and editor projects that deliver watched-file changes.
This configuration only checks types. Keep your application's JavaScript build separate. Content mapping does not turn YAML value imports into deployable JavaScript.
Switching modes
- To enable the mapper, pin TypeScript
7.1.0-beta, removecontextual.d.yaml.tsso it cannot shadow the mapper, remove codegen steps and generated-file checks from your scripts, and use the mapper configuration and editor opt-in above. - To return to generated declarations, pin TypeScript
5.9.3, removecontentMappersand--runExternalCode, restore the generated-declaration configuration and scripts, and run codegen before compiling.
Your YAML imports can stay the same. Neither switch changes the runtime loader requirements.
Runtime loader
Type-only imports of contextual.yaml are erased and need nothing at runtime. A value import, such as objects, needs the loader:
node --import @contextual-io/typegen/register src/main.tsThe loader reads the imported contextual.yaml and its optional sibling contextual.lock.yaml. It does not read environment variables or credentials. Generating contextual.d.yaml.ts supplies types only; it does not replace the loader or create a JavaScript catalog.
For deployment, keep @contextual-io/typegen in runtime dependencies, preload @contextual-io/typegen/register, and ship the application-owned YAML and optional lock at the paths the running imports resolve to. TypeScript does not copy these files into its output directory. With the generated-declaration rootDir and outDir above, the compiled client is dist/src/main.js, so copy contextual.yaml and any contextual.lock.yaml to dist/ and start it with:
node --import @contextual-io/typegen/register dist/src/main.jsShipping only dist/src/ will leave that value import without its manifest. If your deployment cannot use YAML or the Node hook, have your own build provide a plain JavaScript catalog and pass it through the SDK's objects option. Typegen's CLI generates declarations only; it has no JavaScript catalog generation command. Applications with only erased type imports have no YAML or loader requirement at runtime.
Troubleshooting
contextual-typegen prints manifest errors as contextual.yaml:line:column - message and lock errors as contextual.lock.yaml: message, then exits 1. The content mapper reports the same messages as diagnostics in contextual.yaml.
| Message | Cause and fix |
| --- | --- |
| contextual.d.yaml.ts is out of date; run contextual-typegen | --check found the file missing or different from what contextual.yaml and contextual.lock.yaml now produce. Run contextual-typegen to rewrite it. |
| YAML aliases, anchors and explicit tags are unsupported | Write the value out in place of the alias, anchor or tag such as !!str. |
| Prototype-sensitive keys are unsupported | Neither file accepts __proto__, constructor or prototype as a key, because converting one to JavaScript can replace an object's prototype or drop the entry. Rename the Object Type, route or event. A captured schema property with one of these names can't be typed; remove that Object Type's lock entry to type its records as Record<string, unknown>. |
Known limits with TypeScript 7.1.0-beta
- Go to definition reaches
contextual.yamlfrom a route parameter such asparams.idand from a handler type such asHandlers["getItem"]. A string argument such asctx.object("inventory-item")does not resolve to a definition. - Record fields come from the lock file, which the protocol cannot map; their definitions stay in the virtual file.
- After
contextual.yamlchanges on disk, for example after a branch checkout, an editor can keep showing old errors or fail to show new ones in importing TypeScript files. The beta language server does not push the diagnostic refresh (microsoft/TypeScript#64943). Restart the workspace's TypeScript language server or reload the editor project to refresh it, and run the typecheck script for a fresh compiler result. Diagnostics requested after an unsaved YAML edit or a delivered lock-file change can already be current even while other displayed errors are stale. - A YAML or schema error is a mapper diagnostic, which TypeScript treats like a syntax error: other semantic diagnostics wait until it is fixed.
- Use Node.js 24. With Node.js 22.16, the beta's
tsclauncher can start the compiler with an empty environment, leaving the mapper unable to findnode.
