# TypeBox Module Research: UJSX Schema Implications ## Key Finding: `Type.Module` IS the Type Registry `Type.Module` creates a `TModule` object whose `$defs` property is a live `string → TSchema` map. This is exactly the "type registry" pattern needed for UJSX. ### Core Operations ```typescript 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")]), }) ``` #### Reading schemas by name ```typescript ValuePointer.Get(UJSX, "$defs/Props") // → TSchema with $id: "Props" ValuePointer.Get(UJSX, "$defs/Element") // → TSchema with $id: "Element" ``` #### Adding schemas at runtime ```typescript 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 can be referenced by Type.Ref("Heading") ``` #### Importing (resolves $ref, creates self-contained schema for validation) ```typescript const Element = UJSX.Import("Element") // Element = { [Kind]: 'Import', $defs: { Props: {...}, Element: {...}, Node: {...} }, $ref: "Element" } Value.Check(Element, someData) // Works! All $refs resolved via inlined $defs ``` #### Static type inference ```typescript type UE = Static // UE = { type: string; props: { id?: string; [k: string]: unknown }; children: UE[] } ``` ### How `Import` Works (from source) The `Import` method on `TModule` (module.ts:70-72): ```typescript public Import(key: Key, options?: SchemaOptions): TImport { const $defs = { ...this.$defs, [key]: CreateType(this.$defs[key], options) } return CreateType({ [Kind]: 'Import', $defs, $ref: key }) as never } ``` It creates a `TImport` schema with: - `[Kind]: 'Import'` — uniquely identifies this as a module import - `$defs` — ALL module definitions copied in (so $refs resolve) - `$ref: key` — which definition to use as the root ### How `ComputeModuleProperties` Works (from compute.ts) The constructor calls `ComputeModuleProperties` which walks every property and: 1. Resolves `Type.Ref("X")` → dereferences to the 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` This means you can use `Type.Ref()` inside `Type.Module()` and they get resolved to direct references. The $ref JSON pointer stays in the output for JSON Schema compatibility, but TypeBox's internal resolution handles them. ### `TInferFromModuleKey` — Type-Level Inference The `infer.ts` file provides `TInferFromModuleKey` which is the type-level path of resolving refs. This is what makes `Static` produce a proper recursive type: ```typescript type UE = Static // Resolves to: // { type: string; props: { id?: string; [k: string]: unknown }; children: (string | number | null | UE)[] } ``` This is a **recursive type** inferred from the module. No `Type.Recursive` needed because the Module's type-level inference handles cycles through `TRef`. ### Function Types in Modules `Type.Function([...params], returnType)` creates a JavaScript-extended schema type: ```typescript ComponentFn: Type.Function([Type.Ref("Props")], Type.Ref("Node")) ``` This produces a JSON Schema with `{ type: "Function", parameters: [...], returns: {...} }` which is NOT standard JSON Schema but IS valid TypeBox. `Value.Check` validates that the value is a function at runtime. This means we can represent component functions in the schema. ### Implications for UJSX v2 1. **No separate type registry needed.** `TModule` IS the registry. It's a map of `string → TSchema` with computed resolution and `$id` assignment built in. 2. **Schemas ARE types AND ARE tool parameter schemas.** One definition creates the TypeScript type, the runtime validator, and the JSON Schema for tool definitions. 3. **Bi-directional transforms can be schema-driven.** Transform rules can match on `Value.Check(ruleSchema, node)` instead of string tag matching. 4. **Node definitions can be added at runtime.** `ValuePointer.Set(module, "$defs/CustomNode", ...)` or simply defining new entries in the Module. This means plugins can extend the IR. 5. **Function components are first-class in the schema.** `Type.Function([Type.Ref("Props")], Type.Ref("Node"))` validates that `type` is a function at runtime. For serialization, we'd resolve function components to string tags before going to mdast/other targets. 6. **`Import` creates self-contained schemas.** Perfect for sending to tool definitions or validation contexts where the full module isn't available. 7. **The `ComponentFn` pattern means we can separate runtime and serializable representations.** At runtime, `type` can be a function. Before serialization (JSON, mdast), we resolve function components to string identifiers and emit `{ type: "Component", componentId: "TaskSection", ... }`.