Files
ujsx/docs/research/typebox-module-valuepointer.md

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:

  1. Resolves Type.Ref("X") → dereferences to actual type at moduleProperties[X]
  2. Recursively processes: Type.Array(Type.Ref("Node"))Type.Array(ResolvedNode)
  3. Handles all TypeBox combinators: Object, Union, Intersect, Function, Record, etc.
  4. Adds $id to each definition via WithIdentifiers

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

  1. No separate type registry neededTModule IS the registry
  2. Schemas ARE types AND tool parameter schemas — one definition, triple duty
  3. Bi-directional transforms can be schema-drivenValue.Check(ruleSchema, node) instead of string matching
  4. Node definitions can be added at runtime — plugins can extend the IR via ValuePointer.Set()
  5. Import creates self-contained schemas — perfect for tool definitions or validation contexts
  6. Function components are first-class in schema — separate runtime vs serializable representations
  7. No Type.Recursive needed in Module — cycles handle through TRef + type-level inference