Architecture review session resolving all high-priority open questions and filling documentation gaps identified during review: Decisions resolved: - OQ-04: Flat props with inner escape hatch for column validation (ADR-007) - OQ-05: PG enum pre-declaration returns enums and tables (ADR-008) - OQ-06: Render results accumulate in root.ctx (resolved in hosts.md) - Column references vs fk: references is shorthand, explicit fk takes precedence (ADR-006) - ADR-001, 002, 003 promoted from Proposed to Accepted (probe-validated) Documentation improvements: - Complete DbColumnType mapping tables for all 14 types across 3 dialects - Define ColumnMeta, TableMeta, IndexMeta, FkMeta types in elements.md - Document inner prop, mode prop, and default prop semantics - Add PgRootCtx, SqliteRootCtx, MySqlRootCtx context types - Consolidate schema.md and module.md (remove duplication) - Add end-to-end pipeline walkthrough to README - Add glossary with 13 terms - Add error handling strategy - Remove duplicate content from hosts.md (cross-ref elements.md)
6.6 KiB
status, last_updated
| status | last_updated |
|---|---|
| stable | 2026-05-23 |
Module: Type.Module Mechanical Reference
How Type.Module works mechanically — construction, validation, serialization, migration diffing, cross-module references, and constraints. For the domain-specific content (what goes into a dbtype module, schema derivation semantics, relations), see schema.md.
Construction Patterns
Basic Pattern
const defs: Record<string, any> = {
Users: Type.Object({ id: Type.String({ format: 'uuid' }), name: Type.String() }),
Tasks: Type.Object({ id: Type.String({ format: 'uuid' }), title: Type.String() }),
}
const M = Type.Module(defs)
const Users = M.Import('Users')
Incremental Construction
The defs map is a plain Record<string, TSchema> — it can be built incrementally, mutated, and extended before compilation:
const defs: Record<string, any> = {}
defs.Users = extractTableSchema(UsersElement)
defs.Tasks = extractTableSchema(TasksElement)
// Add a column later
defs.Users = Type.Object({ ...defs.Users.properties, role: Type.String() })
// Add relations
defs.UsersRelations = Type.Object({ tasks: Type.Array(Type.Ref('Tasks')) })
// Compile when ready
const M = Type.Module(defs)
Once compiled, mutations to the original defs map don't affect the compiled module.
With Relations
defs.UsersRelations = Type.Object({ tasks: Type.Array(Type.Ref('Tasks')) })
defs.TasksRelations = Type.Object({ user: Type.Ref('Users') })
Type.Ref('Users') within the module resolves to the Users schema. No circular import issues.
With Derived Schemas
defs.InsertUsers = Type.Object({ name: Type.String(), email: Type.String() }) // manual
defs.UpdateUsers = Type.Partial(Type.Ref('Users')) // computed
Validation
Format Registration Required
TypeBox treats format as an annotation by default. To enforce format validation, register custom formats:
import { FormatRegistry } from '@alkdev/typebox'
FormatRegistry.Set('uuid', (value) => /^[0-9a-f]{8}-...$/i.test(value))
FormatRegistry.Set('email', (value) => /^[^@]+@[^@]+\.[^@]+$/.test(value))
After registration, Value.Check enforces these formats.
Validation Pattern
const M = Type.Module(defs)
const Users = M.Import('Users')
// Valid
Value.Check(Users, { id: '550e8400-e29b-41d4-a716-446655440000', name: 'alice', email: 'a@b.com', ... })
// Invalid — Value.Check returns false, Value.Errors provides details
Value.Check(Users, { id: 'bad-uuid', ... }) // false
for (const err of Value.Errors(Users, badData)) { ... }
Serialization
JSON.stringify(M.Import(key)) produces JSON Schema with $defs:
{
"$defs": {
"Users": { "$id": "Users", "type": "object", "properties": { ... }, "required": [...] },
"Tasks": { "$id": "Tasks", "type": "object", "properties": { ... }, "required": [...] },
"UsersRelations": { "$id": "UsersRelations", "type": "object", "properties": { "tasks": { "type": "array", "items": { "$ref": "Tasks" } } } }
},
"$ref": "Users"
}
Key properties:
- Each
$defsentry has an$idmatching its key Type.Refremains as{ "$ref": "Key" }— not inlined; consumers must resolve them- The entire structure is valid JSON Schema
- All entries in the module are present in
$defs(even if only one was imported)
Roundtrip
The serialized form can be parsed back into a schema-like structure. Value.Diff works on these serialized objects to produce structural edit lists. Note that Symbol properties ([Kind], [Hint], etc.) are stripped by JSON.stringify — the serialized form is JSON Schema, not TypeBox schema. Roundtripping requires FromSchema or reconstructed TypeBox objects.
Migration Diffing
const v1 = JSON.parse(JSON.stringify(M.Import('Users')))
// Modify schema
defs.Users = Type.Object({ ...defs.Users.properties, role: Type.String() })
const M2 = Type.Module(defs)
const v2 = JSON.parse(JSON.stringify(M2.Import('Users')))
const edits = Value.Diff(v1, v2)
// [
// { type: 'insert', path: '/$defs/Users/properties/role', value: { type: 'string' } },
// { type: 'update', path: '/$defs/Users/required/3', value: 'role' },
// ]
Edits use JSON Pointer paths. A migration generator can translate these to:
INSERTfor new properties →ALTER TABLE ADD COLUMNDELETEfor removed properties →ALTER TABLE DROP COLUMNUPDATEfor type changes →ALTER TABLE ALTER COLUMN TYPE
This is structural diffing, not semantic — it doesn't understand that changing Type.String() to Type.String({ maxLength: 255 }) is a constraint addition, not a type change. Semantic diffing is a future concern.
Cross-Module References
Module.Import() embeds the source module's $defs in the resulting TImport schema. This enables referencing types from another module:
const CommonM = Type.Module({ Uuid: Type.String({ format: 'uuid' }) })
const CommonUuid = CommonM.Import('Uuid')
const AppM = Type.Module({
User: Type.Object({ id: CommonUuid, name: Type.String() }),
})
However, this nests $defs within $defs (the User's $defs contains CommonUuid's $defs), which increases payload size. For dbtype's use case, keeping everything in a single module is simpler and avoids nesting.
Constraints
- Module entries are computed at construction time —
Type.Partial(Type.Ref('Users'))is resolved when the module is built, producing a concrete optional-property object Type.Refoutside a module hasstatic: unknown— always useM.Import(key)for proper type inference- Module keys are a flat namespace — no nested paths like
"tables/Users". Table names must be unique within the module. Type.Refresolves within the module only — no cross-module references withoutModule.ImportModule.Importembeds all$defs— every import carries the full module. This is correct for validation but increases JSON Schema size.- Defs map is mutable until compiled — once passed to
Type.Module, mutations to the original map don't affect the compiled module - Format validation requires
FormatRegistry.Set—uuid,email, and other custom formats must be registered beforeValue.Checkwill enforce them - Symbol properties are lost in
JSON.stringify—[Kind],[Hint], etc. are stripped. The serialized form is JSON Schema, not TypeBox schema. Roundtripping requiresFromSchemaor reconstructed TypeBox objects.
References
- TypeBox Module:
@alkdev/typebox/src/type/module/module.ts - TypeBox Format:
@alkdev/typebox/src/type/registry/format.ts - Probe:
scripts/probe-e2e.ts