Data Types
Overview
A data type is the single object that answers everything the DMS needs to know about one value: which input component edits it, which Zod schema validates it, which comparison modes its filter offers, and whether the field it sits on needs extra decorators. FormField.type and ColumnOptions.type both take one, which is why a form field and a table column of the same type behave identically.
import {
CreateDataType,
DataType,
DefaultDataCompareTypes,
DefaultDataTypes,
RegisterDataType,
serializeType,
} from "@antelopejs/interface-dms/base/data-types";
The data-types subpath re-exports the built-in catalogues, so DefaultDataTypes, DefaultDataCompareTypes and StatusType are all reachable from it. They also have their own modules — data-types/default-types, data-types/compare-types and data-types/status-type — which is how much of the DMS imports them.
Note that the data types are not re-exported from the base barrel — always import them from the data-types subpath.
Use a Data Type
Instantiate the type with its options and hand the instance to the field or the column.
import { Form } from "@antelopejs/interface-dms/base";
import { DefaultDataTypes } from "@antelopejs/interface-dms/base/data-types";
const form = Form({
fields: [
{
id: "name",
label: "Name",
type: new DefaultDataTypes.StringType({ maxLength: 120 }),
required: true,
},
{
id: "status",
label: "Status",
type: new DefaultDataTypes.SelectType({
items: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
],
}),
},
],
submitUrl: "/api/articles/new",
});
An instance is reusable: the same object can back a form field and a table column, since it holds only its configuration.
The DataType Contract
abstract class DataType {
constructor(
readonly compareModes: Class<DataCompareMode>[] = [],
readonly defaultCompareMode?: Class<DataCompareMode>,
readonly options?: Record<string, unknown>,
);
protected abstract defaultInputComponent(): ComponentInfoSerialized;
abstract getValidation(): z.ZodType;
filter(context, proxy, key, value, mode, row): ValueProxyOrValue<boolean>;
filterComponents(): Record<string, ComponentInfoSerialized> & { default: ComponentInfoSerialized };
inputComponent(): ComponentInfoSerialized;
decorateField?(target: unknown, key: PropertyKey, descriptor?: PropertyDescriptor): void;
}
| Member | Role |
|---|---|
compareModes | Compare-mode classes this type offers in the filter dropdown. |
defaultCompareMode | The mode selected when the viewer adds a filter on this field. |
options | The type's own configuration, kept on the instance and readable by other code. |
defaultInputComponent() | Abstract. Returns the input component the type renders. Implemented by every concrete type. |
getValidation() | Abstract. Returns the Zod schema for one value of this type, before the field-level adaptation described in Forms. |
inputComponent() | The form input. Defaults to defaultInputComponent(). |
filterComponents() | The filter inputs, keyed by compare-mode id with a mandatory default. Defaults to { default: defaultInputComponent() }. |
filter() | Applies a filter for one compare mode. The base implementation looks the mode up in the compare-mode registry and delegates to it; a type overrides it when a mode needs different semantics. |
decorateField() | Optional. Called by the @Column decorator so the type can add further decorators to the field — this is how the relation types install their join. |
Write a Custom Type
StatusType is the smallest complete example: a boolean rendered as an online/offline indicator whose labels and colours are configurable.
import { z } from "zod";
import { FormComponents } from "@antelopejs/interface-dms/base/form";
import {
DataType,
DefaultDataCompareTypes,
RegisterDataType,
} from "@antelopejs/interface-dms/base/data-types";
@RegisterDataType("status")
export class StatusType extends DataType {
constructor(
public readonly options: {
onlineLabel?: string;
offlineLabel?: string;
onlineColor?: string;
offlineColor?: string;
} = {
onlineLabel: "$common.status.online",
offlineLabel: "$common.status.offline",
onlineColor: "primary",
offlineColor: "neutral",
},
) {
super(
[DefaultDataCompareTypes.Is, DefaultDataCompareTypes.IsNot],
DefaultDataCompareTypes.Is,
options as Record<string, unknown>,
);
}
protected defaultInputComponent() {
const component = FormComponents.InputCheckbox();
component.options = this.options;
return component;
}
getValidation() {
return z.boolean();
}
}
Three pieces make it a type: the @RegisterDataType("status") decorator puts it in the registry under an id, the super() call declares the compare modes and stores the options, and the two abstract members supply the input and the schema. StatusType itself is exported by the data-types subpath and is one of the three column types a kanban board can group by — see Tables.
The Registry
RegisterDataType
const RegisterDataType: (id: string) => ClassDecorator<Class<DataType>>;
Registers the decorated class under id, in both directions: CreateDataType looks up the class by id, and getDataTypeId recovers the id from an instance. An id must be unique — registering twice under the same string replaces the earlier class.
RegisterDataCompareMode
const RegisterDataCompareMode: (
id: string,
noInput?: boolean,
) => ClassDecorator<Class<DataCompareMode>>;
Registers a compare mode. The decorator instantiates the class immediately and keeps that single instance, so a compare mode is stateless. noInput marks a mode that needs no value from the viewer — serializeType replaces its filter component with the literal "noInput".
DataCompareMode is the interface a mode implements:
interface DataCompareMode {
filter(
context: RequestContext,
proxy: ValueProxy<unknown>,
key: string,
value: unknown,
row: ValueProxy<Record<string, unknown>>,
): ValueProxyOrValue<boolean>;
}
proxy is the database value proxy for the field being filtered and row the proxy for the whole row, both from @antelopejs/interface-database, so a mode builds a database-side predicate rather than filtering in memory.
CreateDataType
function CreateDataType(id: string, options?: unknown): DataType;
Instantiates a registered type by id, forwarding options to its constructor. It asserts on an unknown id with the message Unknown type <id>. Use it when the type is chosen at runtime — from configuration, or from a description produced by another module — rather than written in the source.
getDataTypeId
function getDataTypeId(instance: DataType): string | undefined;
Returns the id an instance's class was registered under, or undefined when the class is not registered. It resolves the class from the instance's own prototype, so a subclass of a registered type returns its own id, not its parent's.
Serialization
function serializeType(instance?: DataType): DataTypeSerialized | undefined;
serializeType is what turns a type instance into the description shipped to the frontend. It returns undefined when there is no instance, and also when the instance's class is not registered.
DataTypeSerialized:
| Field | Type | Description |
|---|---|---|
id | string | The registered id. |
compareModes | string[] | Ids of the type's compare modes; unregistered ones are dropped. |
defaultCompareMode | string | Id of the default mode. |
filterComponents | Record<string, FilterComponent> | Filter inputs by mode id, from filterComponents(), with every noInput mode's entry replaced by the literal "noInput". |
inputComponent | ComponentInfoSerialized | The form input, from inputComponent(). |
FilterComponent is ComponentInfoSerialized | "noInput". The value-level counterpart, ComponentOrNoInput, is described in Shared Types.
Compare Modes
DefaultDataCompareTypes holds the built-in modes. Each entry is a class; you reference the class in a type's compareModes, and the string id is what appears in a serialized type and in a filter query.
| Class | Id | Input |
|---|---|---|
Is | is | value |
IsNot | is_not | value |
GreaterThan | greater_than | value |
GreaterThanOrEqualTo | greater_than_or_equal_to | value |
LessThan | less_than | value |
LessThanOrEqualTo | less_than_or_equal_to | value |
IsBetween | is_between | two values |
IsEmpty | is_empty | none |
IsNotEmpty | is_not_empty | none |
Include | include | value |
Exclude | exclude | value |
Contains | contains | value |
NotContains | not_contains | value |
ArrayContainsString | array_contains_string | value |
IsEmpty and IsNotEmpty are the two registered with noInput. The namespace also exports escapeRegex(str: string): string, the helper the substring modes use to make a viewer-supplied value safe inside a pattern.
The Default Types
DefaultDataTypes is the built-in catalogue. Every class takes its options object as its only constructor argument, and most of them accept a fallback string used in place of an empty value.
| Class | Id | Compare modes (default first) | Input |
|---|---|---|---|
StringType | string | contains, is, is_not, not_contains, is_empty, is_not_empty | text or textarea |
NumberType | number | is, is_not, greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, is_empty, is_not_empty | number |
PriceType | price | as NumberType | number |
PercentageType | percentage | as NumberType | percentage |
DateType | date | is, less_than, greater_than, is_between, is_empty, is_not_empty | date picker |
BooleanType | boolean | is, is_not, is_empty, is_not_empty | switch |
SelectType | select | include/exclude when multiple, otherwise is/is_not; plus is_empty, is_not_empty | select |
EmailType | email | contains, is, is_not, not_contains, is_empty, is_not_empty | |
UrlType | url | is, is_not, is_empty, is_not_empty | text |
PhoneType | phone | contains, is, is_not, not_contains, is_empty, is_not_empty | phone |
ColorType | color | is, is_not, is_empty, is_not_empty | colour |
PasswordType | password | none | password |
RichTextType | rich_text | none | rich text |
StringTimeType | string_time | as NumberType | time |
RelationType | relation | is, is_not, is_empty, is_not_empty | relation picker |
CascaderRelationType | cascader_relation | is, is_not, is_empty, is_not_empty | cascader |
AddressType | address | is_empty, is_not_empty | address |
FileType | file | none | file upload |
ImageType | image | none | image upload |
Scalar Types
| Options interface | Fields |
|---|---|
StringTypeOptions | placeholder?, maxLength?, minLength?, showCount?, textarea?, rows?, fallback? |
NumberTypeOptions | min?, max?, step?, placeholder?, suffix?, fallback? |
DateTypeOptions | range?, multiple?, minDate?, maxDate?, fallback? |
BooleanTypeOptions | fallback? |
SelectTypeOptions | items: FormComponents.SelectOption[], placeholder?, multiple?, deselectable?, display?, columns? (cards per row of the cards display, 2 by default), fallback? |
EmailTypeOptions, ColorTypeOptions, UrlTypeOptions, RichTextTypeOptions | placeholder?, fallback? |
PasswordTypeOptions | placeholder?, minLength?, confirmPassword?, confirmPlaceholder?, fallback? |
PhoneTypeOptions | placeholder?, requiredPrefix?, fallback? |
StringTimeTypeOptions | placeholder?, min?, max?, fallback? |
Notes on the ones whose behaviour is not obvious from the options:
StringTyperenders a textarea instead of a single-line input whentextareais set, and passesrowsto it.StringTypewithshowCountshows the characters typed, out ofmaxLengthwhen set ("4 / 60"), in its input or under its textarea.NumberTypehandssuffix(an i18n key with$, or a literal) to its input, which writes it after the field; the value stays a number.PriceTypeinherits it;PercentageTypekeeps its own%.DateTypeoverridesfiltersois,is_notandis_betweencompare whole UTC days rather than instants, and adds a range picker as the filter component foris_between. Its schema accepts aDateor an ISO-shaped string and transforms it to aDate; withmultiplethe value becomes an array, and withrangeit becomes{ start, end },endon or afterstart(a pair[start, end]is still read and validates to the same object).BooleanTypeuses a two-option select as its filter component, so a viewer picks "checked"/"unchecked" rather than typing a value.SelectTypevalidates against thevalues of itsitems, as an enum or an array of that enum whenmultipleis set.PriceTypeis aNumberTypewhose options default tomin: 0andstep: 0.01.PercentageTypeis aNumberTypewhose options default tomin: 0,max: 1andstep: 0.01; the stored value is a fraction while the input works in whole percent.PhoneTypeonly validates the E.164 pattern whenrequiredPrefixis set; otherwise any string passes.ColorTypevalidates a six-digit#RRGGBBstring.StringTimeTypevalidates a number — a time of day expressed as a number, bounded byminandmax.
Relations
RelationType<T> points a field at another data controller. Its input is a searchable picker whose searchUrl is the target controller's select endpoint, and — unless filterOnly is set — its decorateField installs a data-api Foreign join so list rows carry the resolved related row instead of the raw foreign key.
import { DefaultDataTypes } from "@antelopejs/interface-dms/base/data-types";
import { Column } from "@antelopejs/interface-dms/base/table-view";
@Column({
name: "Customer",
type: new DefaultDataTypes.RelationType({
dataApiController: customerDataAPI,
keyMapping: { label: "name", value: "_id" },
}),
filterable: true,
})
declare customerId: string;
| Option | Type | Description |
|---|---|---|
dataApiController | T extends ControllerClass | The target data controller. Required. |
keyMapping | { label?, value?, avatar?, disabled? } | Which of the target's fields fill each role in the picker. Keys are checked against the target's own field names. |
index | string | Index the join reads, when it is not the primary key. |
placeholder | string | Picker placeholder. |
multiple | boolean | Allow more than one related row. |
deselectable | boolean | Allow clearing the selection. |
filterOnly | boolean | Skip the join entirely: the raw scalar id stays in the list output and the funnel filter runs equality on it. |
addForm | { displayName?: string; description?: string } | Title and description of the drawer the picker's "add" entry opens ("New customer"). $-prefixed: i18n keys. Default to "New entry". |
fallback | string | Text used in place of an empty value. |
When the target controller exposes a new endpoint and has form fields, the picker also carries an inline "add" form built from the target's new fields, plus the permission id of the target's add action.
CascaderRelationType<T> is the same idea for a self-referencing table: the target's select endpoint returns a flat list, and the widget rebuilds the tree client-side. Its keyMapping is required and adds parent — the field pointing at the parent row, which must itself carry @Select() — alongside the required label and value. It adds maxDepth and leafOnly to the relation options, and supports the same filterOnly behaviour. In a table cell it shows the path of the picked node ("Electronics / Audio"); a multiple one shows a badge per picked node, named by its own label with its path on hover, and folds those past three into +N.
Address
AddressType<T> stores a structured postal address. Its Address shape follows ISO 19160-1 and maps directly onto the UBL cac:PostalAddress used by EN 16931 / Peppol BIS Billing 3.0.
| Field | Required | Description |
|---|---|---|
streetName | yes | Street or thoroughfare name. |
postalCode | yes | Postal or ZIP code. |
city | yes | City or locality. |
countryCode | yes | ISO 3166-1 alpha-2 country code. |
houseNumber | no | House or building number. |
boxNumber | no | Box, unit or apartment number. |
addressLine2 | no | Supplementary delivery information. |
countrySubdivision | no | Region, state or province. |
The type's options are placeholder (one string per address component), autocomplete, an optional dataApiController with a keyMapping of label / value for picking an existing address, and fallback. AddressAutocomplete configures the geocoder:
| Option | Type | Description |
|---|---|---|
enabled | boolean | Enable suggestions on the street field. Defaults to false. |
url | string | Geocoder endpoint. Defaults to "https://photon.komoot.io/api". |
lang | string | Preferred result language, ISO 639-1. Falls back to the UI locale. |
limit | number | Maximum number of suggestions. Defaults to 5. |
placeholder | string | Search field placeholder. |
Files and Images
FileTypeOptions has multiple?, constraints?: UploadConstraints, path?, storage? and fallback?. ImageTypeOptions has multiple?, max?, constraints?, path?, storage? and resize?, the FormComponents.ImageResizeOptions the input applies client-side before uploading (Forms). Both accept attachmentField? for a stable native field identity and visibility?: "private" | "public", defaulting to private. constraints is the UploadConstraints type from @antelopejs/interface-file-storage; storage names the storage backend and path the prefix uploads land under.
A file value is the resource key as a string. Metadata-based validation checks configured constraints, matching allowedMimetypes patterns including the type/* form. A native field's save integration also validates upload provenance and canonical metadata before persistence. Existing unclassified legacy values may be retained, but cannot be introduced as new native attachments or read through the native metadata endpoint. A storage outage propagates as a server error rather than becoming a field validation error. An image value is an ImageValue — { key: string; alt?: string; principal?: boolean } — and ImageType defaults allowedMimetypes to ["image/*"], caps the array at max when multiple is set, and rejects a list with more than one principal image.
Both types enable the table view's native file handling: authorized saves promote staging keys, and successful edits or deletes clean removed native references. Failed saves retain promoted objects; cleanup is not reference-counted across records. See File storage for authorization, visibility, retries, and custom save routes, and Helpers for comparing references within records.
Next Steps
- Helpers - Search, export jobs, file references and the archive-mode defaults.
Layout and Containers
Grid, stacks, tabs, guard, placeholder and the page layouts that hold every other component.
Helpers
A few small surfaces sit beside the components and support them: the `@Searchable` decorator that tells a list route which columns free-text search covers, the export-job runner behind every long download, the…