Field Types
Catalog of the 17 built-in field types and how the type discriminator resolves to a renderer.
Every field in an OGMF form is described by a plain config object whose type property names a built-in renderer. The form builder ships a fixed catalog of built-in field types, and modules can extend the catalog with their own types through the slot system. This page is the index for that catalog: it lists every built-in type with the value it produces, explains how the type discriminator is resolved to a React renderer at runtime, and points at the detailed reference pages for each family.
A form's field map is keyed by schema field name, and each entry is a FieldConfig:
const fields: FormFields<typeof schema> = {
title: { type: "text", label: "Title", placeholder: "A short headline…" },
priority: {
type: "select",
label: "Priority",
options: [
{ label: "Low", value: "low" },
{ label: "Normal", value: "normal" },
{ label: "High", value: "high" },
],
},
accepted: { type: "checkbox", label: "I accept the terms" },
};The type string is the only thing the builder needs to pick a renderer. Everything else in the object is that renderer's typed config.
The built-in catalog
These are the built-in field types, exactly the keys registered in BUILTIN_RENDERERS. The Value column is the type the field commits to form state (what you write the Zod schema against); the Reference column links to the family page that documents each type's full config.
type | Value committed to form state | Summary | Reference |
|---|---|---|---|
text | string | Single-line text input. | Text inputs |
textarea | string | Multi-line text input (rows?). | Text inputs |
number | number | Numeric input with stepper (min/max/step). | Text inputs |
currency | number | Locale-formatted money input (currency?, min/max/step). | Text inputs |
array | object[] (one entry per row) | Repeatable fieldset bound to z.array(z.object(...)). | Text inputs |
select | string | number | Compact dropdown (Radix Select, no search). | Choice fields |
combobox | string | number | Searchable single-select (Popover + Command). | Choice fields |
asyncCombobox | string | number | Server-driven single-select with debounced query. | Choice fields |
radio | string | number | Single choice from a visible option list. | Choice fields |
checkboxGroup | (string | number)[] | Multi-select from a visible option list. | Choice fields |
checkbox | boolean | Single boolean checkbox. | Choice fields |
switch | boolean | Single boolean toggle. | Choice fields |
date | string ("YYYY-MM-DD") | Calendar date picker (min/max). | Date and time |
datetime | string ("YYYY-MM-DDTHH:mm") | Date + time picker. | Date and time |
dateRange | { from?: string; to?: string } | Two-month range picker. | Date and time |
time | string | Time picker. | Date and time |
file | File | File[] | File picker / dropzone (accept, multiple, maxSize, maxFiles). | File and hidden |
hidden | passthrough (schema-defined) | Invisible field that participates in state and submission. | File and hidden |
The four single-choice types (select, combobox, asyncCombobox, radio) commit string | number, mirroring SelectOption.value. checkboxGroup commits an array of those. Write your schema to match — e.g. z.enum([...]) for a string-valued select, or z.array(z.string()) for a checkbox group.
hidden has no value type of its own. Its config is { type: "hidden"; label?: never } — it has no label by definition. The renderer mirrors whatever the schema already stores for that field into an <input type="hidden">, stringified for the DOM. The value's type is whatever your Zod schema declares.
FieldConfig is a discriminated union
FieldConfig is a discriminated union over the type literal. It is derived from the FieldTypeRegistry interface, one entry per built-in type:
type FieldConfig = FieldTypeRegistry[keyof FieldTypeRegistry];Each registry entry pins the props that type accepts. For example textarea adds rows?, number adds min/max/step, and the choice types add options. Most entries extend a shared FieldConfigBase that carries label, description, placeholder, colSpan, the runtime predicates (disabled, required, visible), and the field-level validators (validate, validateAsync).
Because the union is discriminated on type, TypeScript narrows the config the moment you set type. Setting type: "textarea" makes rows valid; the same key on a text field is a compile error. And because FormFields<T> requires an entry for every top-level field in the Zod schema, omitting a field or adding an unknown one also fails at compile time.
FieldTypeRegistry is a TypeScript interface, not a type alias, on purpose: modules add custom types by declaration-merging new entries into it. A merged entry instantly becomes a valid type value across FieldConfig and FormFields<T>. See Custom field types.
How a type resolves to a renderer
At render time, each field's type string is mapped to a React component by resolveFieldType:
function resolveFieldType(
type: string,
slotMap?: ReadonlyMap<string, FieldRenderer>,
): FieldRenderer;The resolution order is:
Built-ins first. The type is looked up in BUILTIN_RENDERERS. If found, that renderer wins — built-ins always take precedence over slot contributions on a name collision.
Slot contributions next. If a slotMap was supplied and the type isn't a built-in, the map is consulted for a module-contributed renderer.
Otherwise, throw. An unknown type raises a descriptive error listing the built-ins it knows and any slot contributions it saw, so the misspelled or unregistered type is obvious from the message alone.
The thrown error reads:
Unknown field type: "<type>". Known built-ins: [array, asyncCombobox, …].
Slot contributions: [(none)]. If this is a custom type, register it via the
module's module.ts using slotContribution(FORM_SLOTS.FIELD_TYPES, …).A renderer is a FieldRenderer<TConfig> — a React component that receives { name, config, field }. The config it sees is a ResolveConfig<TConfig>: predicates have already been evaluated upstream, so config.disabled and config.required are resolved to boolean | undefined and config.visible is gone entirely (a field whose visible predicate is false is unmounted before its renderer ever runs). The registry's storage map (BuiltinRendererMap) is a mapped type keyed by FieldTypeRegistry, so each built-in is compile-time pinned to its own config shape — registering the text renderer under the checkbox key would be a type error.
Extending the catalog
The catalog is open: a module can register a new field type without the form builder knowing about it ahead of time. Two pieces make a custom type real:
- Declaration merging adds an entry to
FieldTypeRegistry, which makes the newtypevalid inFormFields<T>at compile time. - A slot contribution at
FORM_SLOTS.FIELD_TYPESsupplies the renderer, whichresolveFieldTypefinds via theslotMapafter the built-ins.
declare module "@/components/forms" {
interface FieldTypeRegistry {
licensePlate: { type: "licensePlate"; label: string; country?: "PT" | "ES" };
}
}See Custom field types for the end-to-end registration walkthrough, including the _development module's licensePlate worked example.