# Field types (/docs/guides/build-a-plugin/field-types)

---
title: Field types
description: Put your own type on the f proxy. One file in fields/ gives you a column, a Zod schema and a where operator set. A second file gives the admin its control, and f.color() reads the same as f.text().
kind: guide
package: questpie
---

You want `f.color()` beside `f.text()`. This page writes one, wires it into the
generator, and gives it a control in the admin.

## Try `.drizzle()` first

If you only need different column behaviour, keep the nearest built-in type and
tweak the column. You keep its validation, its admin control, its localization
and its operators.

```ts title="src/questpie/server/collections/products.ts"
import { sql } from "questpie/builders";
import { collection } from "#questpie/factories";

export const products = collection("products").fields(({ f }) => ({
	sku: f
		.text(64)
		.required()
		.drizzle((column) => column.$type<`sku_${string}`>()),
	createdAt: f.datetime().drizzle((column) => column.default(sql`now()`)),
}));
```

`.drizzle()` replaces the column and leaves the field's value type alone. Reach
for `.zod()` when the validation has to change too.

Write a new type when you need a different column, a different operator set, or
a different admin control.

## Write the type

`fieldType(name, config)` is the factory. `create` returns the field's runtime
state, and that state is the whole definition.

```ts title="src/questpie/server/fields/color.ts"
import { fieldType, selectSingleOps } from "questpie/builders";
import { varchar } from "questpie/drizzle-pg-core";
import { z } from "zod";

export const colorFieldType = fieldType("color", {
	create: () => ({
		type: "color",
		columnFactory: (name: string) => varchar(name, { length: 9 }),
		schemaFactory: () => z.string().regex(/^#[0-9a-fA-F]{6}$/),
		operatorSet: selectSingleOps,
		notNull: false,
		hasDefault: false,
		localized: false,
		virtual: false,
		input: true,
		output: true,
		isArray: false,
	}),
});
```

Every key above is required. Three of them do the real work.

| Key             | What it decides                               |
| --------------- | --------------------------------------------- |
| `columnFactory` | The Drizzle column, from the field name       |
| `schemaFactory` | The Zod schema the value is validated against |
| `operatorSet`   | Which `where` operators the field accepts     |

The rest are the starting state of the chain. `.required()` flips `notNull`,
`.default()` flips `hasDefault`, `.localized()` flips `localized`, and so on.

Operator sets ship with the framework, so you rarely write one. `stringOps`,
`numberOps`, `booleanOps`, `dateOps`, `selectSingleOps`, `selectMultiOps`,
`objectOps` and `basicOps` all come from `questpie/builders`. Reuse the one that
matches how people will filter your field.

## Add chain methods

A bare field type gets the common methods only. Pass `methods` to add your own.

```ts title="src/questpie/server/fields/slug.ts"
import { type Field, fieldType, stringOps } from "questpie/builders";
import { varchar } from "questpie/drizzle-pg-core";
import { z } from "zod";

export const slugFieldType = fieldType("slug", {
	create: (maxLength = 255) => ({
		type: "slug",
		columnFactory: (name: string) => varchar(name, { length: maxLength }),
		schemaFactory: () =>
			z
				.string()
				.regex(/^[a-z0-9-]+$/)
				.max(maxLength),
		operatorSet: stringOps,
		// the rest of the state, as above
	}),
	methods: {
		maxLen: (field: Field<any>, n: number) => field.derive({ maxLength: n }),
	},
});
```

Each method takes the field plus its arguments and returns a field. Use
`.derive()` to change state. Its parameter type omits the identity keys. A
method cannot swap `type`, `columnFactory`, `schemaFactory`, `operatorSet`,
`innerField`, `isArray` or `virtual` out from under the field.

Declaring any method wraps the factory in a proxy that re-wraps after every
call. That is what keeps `f.slug().maxLen(80).required().label("Slug")` working
in any order.

## Make it appear on `f`

In an app, put the file in `fields/` and generate.

```bash
questpie generate
```

```ts
export const paints = collection("paints").fields(({ f }) => ({
	hex: f.color().required(),
}));
```

The generator scans `fields/` for `fieldType()` calls. It unwraps each one to
its `factory` and merges it into the field definitions the builders are handed.
The name you passed is the key, so `fieldType("color", …)` gives `f.color()`.

In a package it takes two steps, because a consuming app never scans your source.

| Step                                                          | What it gives the consumer |
| ------------------------------------------------------------- | -------------------------- |
| A `fields.ts` in the module, default-exporting your factories | The type on `f`            |
| `factoryImports` on the `fieldTypes` category in your plugin  | The factory at runtime     |

```ts title="your plugin's server target"
categories: {
	fieldTypes: {
		dirs: ["fields"],
		prefix: "ftype",
		factoryImports: [{ name: "acmeFields", from: "@acme/thing/fields" }],
	},
},
```

That is exactly how `@questpie/admin` ships `richText` and `blocks`. Miss the
second step and the type resolves while the call throws at runtime.

## Give it a control

The admin needs a React component under the same name. Put it in the admin
target's `fields/` directory.

```tsx title="src/questpie/admin/fields/color.tsx"
import { field, type FieldComponentProps } from "@questpie/admin/client";

function ColorField({ value, onChange }: FieldComponentProps<string>) {
	return (
		<input
			type="color"
			value={value ?? "#000000"}
			onChange={(e) => onChange?.(e.target.value)}
		/>
	);
}

export default field("color", { component: ColorField });
```

The name is the only link between the two files. `questpie add field color`
scaffolds this side for you.

## Where each topic lives

| Topic                              | Page                                                           |
| ---------------------------------- | -------------------------------------------------------------- |
| Every built-in type and its column | [Fields](/docs/schema/fields)                                  |
| Adding a method to `collection()`  | [Builder methods](/docs/guides/build-a-plugin/builder-methods) |
| Shipping the type in a package     | [Publishing](/docs/code/modules/publishing)                    |
| The `fieldTypes` category in full  | [Plugins](/docs/code/codegen/plugins)                          |

## Next

**[Builder methods](/docs/guides/build-a-plugin/builder-methods)** is the other
half of the same seam. A field type adds a value. A builder method adds a place
to put one.
