--- status: stable last_updated: 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](schema.md). ## Construction Patterns ### Basic Pattern ```typescript const defs: Record = { 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` — it can be built incrementally, mutated, and extended before compilation: ```typescript const defs: Record = {} 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 ```typescript 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 ```typescript 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: ```typescript 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 ```typescript 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`: ```json { "$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 `$defs` entry has an `$id` matching its key - `Type.Ref` remains 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 ```typescript 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: - `INSERT` for new properties → `ALTER TABLE ADD COLUMN` - `DELETE` for removed properties → `ALTER TABLE DROP COLUMN` - `UPDATE` for 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. **Migration generation**: Leverage `drizzle-kit` for migrations. Since dbtype's primary output is Drizzle table definitions, `drizzle-kit generate` handles migration generation from those tables. The module's `Value.Diff` capability remains available as a foundation for future migration tooling, but is not used for production migrations in phase 1 or the foreseeable future. See [ADR-014](decisions/014-leverage-drizzle-kit-for-migrations.md). ## Cross-Module References `Module.Import()` embeds the source module's `$defs` in the resulting `TImport` schema. This enables referencing types from another module: ```typescript 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.Ref` outside a module has `static: unknown`** — always use `M.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.Ref` resolves within the module only** — no cross-module references without `Module.Import` - **`Module.Import` embeds 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 before `Value.Check` will enforce them - **Symbol properties are lost in `JSON.stringify`** — `[Kind]`, `[Hint]`, etc. are stripped. The serialized form is JSON Schema, not TypeBox schema. Roundtripping requires `FromSchema` or 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`