3.8 KiB
TypeBox Module & ValuePointer: Core Patterns for UJSX
Source: /workspace/@alkdev/typebox (npm: @alkdev/typebox)
TModule: The Schema Registry
Type.Module() creates a TModule whose $defs property is a live string → TSchema map. This IS the type registry UJSX needs — no separate registry required.
Core Operations
const UJSX = Type.Module({
Props: Type.Object({ id: Type.Optional(Type.String()) }, { additionalProperties: Type.Unknown() }),
Element: Type.Object({
type: Type.String(),
props: Type.Ref("Props"),
children: Type.Array(Type.Ref("Node")),
}),
Node: Type.Union([Type.String(), Type.Number(), Type.Null(), Type.Ref("Element")]),
})
Read schemas by name
import { ValuePointer } from "@alkdev/typebox/value";
ValuePointer.Get(UJSX, "$defs/Props") // TSchema with $id: "Props"
ValuePointer.Get(UJSX, "$defs/Element") // TSchema with $id: "Element"
Add schemas at runtime
ValuePointer.Set(UJSX, "$defs/Heading", Type.Object({
type: Type.Literal("heading"),
depth: Type.Number(),
children: Type.Array(Type.Ref("Node")),
}))
// Now UJSX.$defs.Heading exists and Type.Ref("Heading") resolves
Import: Resolved schemas for validation
const Element = UJSX.Import("Element")
// Creates TImport with: { [Kind]: 'Import', $defs: { all module defs inlined }, $ref: "Element" }
Value.Check(Element, someData) // Works! All $refs resolved via inlined $defs
Static type inference
type UE = Static<typeof Element>
// UE = { type: string; props: { id?: string; [k: string]: unknown }; children: UE[] }
The Module's type-level inference handles cycles through TRef — no Type.Recursive needed.
How Import Works (from source)
Module source: /workspace/@alkdev/typebox/src/type/module/index.ts
public Import<Key>(key: Key, options?: SchemaOptions): TImport {
const $defs = { ...this.$defs, [key]: CreateType(this.$defs[key], options) }
return CreateType({ [Kind]: 'Import', $defs, $ref: key })
}
Creates a TImport schema with:
[Kind]: 'Import'— uniquely identifies as module import$defs— ALL module definitions copied in (so $refs resolve)$ref: key— which definition to use as root
ComputeModuleProperties
Called during Module construction. Walks every property and:
- Resolves
Type.Ref("X")→ dereferences to actual type atmoduleProperties[X] - Recursively processes:
Type.Array(Type.Ref("Node"))→Type.Array(ResolvedNode) - Handles all TypeBox combinators: Object, Union, Intersect, Function, Record, etc.
- Adds
$idto each definition viaWithIdentifiers
Function Types in Modules
Type.Function([Type.Ref("Props")], Type.Ref("Node"))
Creates a JavaScript-extended schema type { type: "Function", parameters: [...], returns: {...} }. Not standard JSON Schema but valid TypeBox. Value.Check validates that the value is a function at runtime.
This means component functions CAN be represented in the schema. For serialization, resolve function components to string identifiers before going to mdast/other targets.
Implications for UJSX v2
- No separate type registry needed —
TModuleIS the registry - Schemas ARE types AND tool parameter schemas — one definition, triple duty
- Bi-directional transforms can be schema-driven —
Value.Check(ruleSchema, node)instead of string matching - Node definitions can be added at runtime — plugins can extend the IR via
ValuePointer.Set() Importcreates self-contained schemas — perfect for tool definitions or validation contexts- Function components are first-class in schema — separate runtime vs serializable representations
- No
Type.Recursiveneeded in Module — cycles handle throughTRef+ type-level inference