OfficeGest
Form Builder/Field Types

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.

typeValue committed to form stateSummaryReference
textstringSingle-line text input.Text inputs
textareastringMulti-line text input (rows?).Text inputs
numbernumberNumeric input with stepper (min/max/step).Text inputs
currencynumberLocale-formatted money input (currency?, min/max/step).Text inputs
arrayobject[] (one entry per row)Repeatable fieldset bound to z.array(z.object(...)).Text inputs
selectstring | numberCompact dropdown (Radix Select, no search).Choice fields
comboboxstring | numberSearchable single-select (Popover + Command).Choice fields
asyncComboboxstring | numberServer-driven single-select with debounced query.Choice fields
radiostring | numberSingle choice from a visible option list.Choice fields
checkboxGroup(string | number)[]Multi-select from a visible option list.Choice fields
checkboxbooleanSingle boolean checkbox.Choice fields
switchbooleanSingle boolean toggle.Choice fields
datestring ("YYYY-MM-DD")Calendar date picker (min/max).Date and time
datetimestring ("YYYY-MM-DDTHH:mm")Date + time picker.Date and time
dateRange{ from?: string; to?: string }Two-month range picker.Date and time
timestringTime picker.Date and time
fileFile | File[]File picker / dropzone (accept, multiple, maxSize, maxFiles).File and hidden
hiddenpassthrough (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 new type valid in FormFields<T> at compile time.
  • A slot contribution at FORM_SLOTS.FIELD_TYPES supplies the renderer, which resolveFieldType finds via the slotMap after 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.