Skip to main content

Built-in validators

This page documents every built-in validator, including every supported call style and the values each rule intentionally does not reject.

Most format and constraint validators accept an empty value so they compose cleanly with required:

field('', [required, email]);

Every built-in failure includes a default English message. A static message can be passed directly as the last argument. Use the options object when the message is reactive or when another option is needed:

min(18, 'You must be an adult.');
min(18, { message: () => translations().minimumAge });
minDate('2026-08-24', { parseAs: 'local', message: 'Choose a later date.' });

Returning undefined from a message function continues through form, provider, global, and built-in message fallbacks. See Validator messages and i18n.

Constraints passed as functions are reactive. Signals read by them are tracked, and returning undefined temporarily disables constraints that support optional sources.

Every standard built-in factory also accepts a reactive when predicate in its options object. requiredIf() uses its condition argument for the same purpose instead. The predicate receives the complete validator context. While it returns false, the validator contributes neither an error nor constraint metadata:

const businessAccount = signal(false);

const myForm = form({
companyName: field('', [
required({ when: () => businessAccount() })
]),
});

Calls that pass a message string directly remain unchanged. Use the options object when when is needed.

🚨 Custom errors​

Every built-in validator accepts an error option that replaces its standard error when the rule fails. Supply one error, an array of errors, or a function receiving the validator context. The function is reactive and runs only while the rule fails. An empty array, null, or undefined suppresses the failure. Because the replacement owns its complete shape, error and message cannot be used together.

built-in-validator-error.example.ts
import { signal } from '@angular/core';
import { field, min } from '../../src/public-api';

const errorKind = signal('minimumAge');
const age = field(16, [
min(18, { error: ({ value }) => ({ kind: errorKind(), actual: value(), minimum: 18 }) })
]);

if (age.errors()[0]?.kind !== 'minimumAge') {
throw new Error('The custom error should replace the built-in minimum error.');
}

errorKind.set('minimumEmploymentAge');
if (age.errors()[0]?.kind !== 'minimumEmploymentAge') {
throw new Error('Signals read by the error function should remain reactive.');
}

const optionalMinimum = field(16, [min(18, { error: () => [] })]);
if (!optionalMinimum.valid()) {
throw new Error('An empty custom error list should suppress the failed rule.');
}

The validator's constraint metadata remains active even when the replacement function suppresses the error. A false when predicate disables both the rule and its metadata before error is evaluated.

βœ… Validator map​

NeedValidators
Presencerequired, requiredIf
Numeric limits and shapemin, max, between, integer
Text or collection sizeminLength, maxLength, lengthBetween
Word countminWords, maxWords
Text formatpattern, email, url
Date limitsminDate, maxDate, dateBetween
Allowed or matching valuesoneOf, equalTo
Collection uniquenessuniqueItems
Native/custom-control constraint hintsConstraint metadata
Bundle behaviorTree shaking

βœ… required​

Requires a value to be present. It can be passed directly or called with message options:

const myForm = form({
name: field('', [required]),
surname: field('', [required('Enter your surname.')]),
});

It rejects null, undefined, '', and NaN. It does not reject empty arrays, sets, maps, or objects. Use minLength(1) when a collection must contain an item:

const myForm = form({
roles: array(field(''), {
initialValue: [],
validators: [required, minLength(1)],
}),
});

A failure is { kind: 'required', message }. The validator contributes required() === true metadata to its node.

Presence and acceptance​

ValuerequiredrequiredTruenotNil
null, undefinedInvalidInvalidInvalid
falseValidInvalidValid
trueValidValidValid
'', NaNInvalidInvalidValid
0, whitespace, empty collectionsValidInvalidValid

requiredIf follows required while its condition is true. For a nullable boolean question, required accepts both answers. For terms and consent, use requiredTrue. See the complete example.

βœ… requiredTrue​

requiredTrue requires exactly true and reports { kind: 'requiredTrue', message }. It supports direct use, a message string, or options with message, error, and when. It contributes required metadata and native checkbox acceptance.

βœ… notNil​

notNil rejects only null and undefined, reporting { kind: 'notNil', message }. It supports the same call styles and options. It contributes no required metadata or native required attribute, because empty values remain valid.

βœ… requiredIf​

Requires a value only while a reactive condition is true:

const businessAccount = signal(false);

const myForm = form({
companyName: field('', [requiredIf(() => businessAccount())]),
});

Signals read by the condition are tracked. While it returns false, the rule contributes neither an error nor required metadata. While it returns true, requiredIf() has the same empty-value, message, and { kind: 'required' } behavior as required.

βœ… min​

Requires a number greater than or equal to an inclusive minimum:

const myForm = form({
age: field(16, [min(18)]),
dependentAge: field(16, [min(() => minimumAge())]),
employeeAge: field(16, [min(18, 'You must be an adult.')]),
});

null and NaN pass. A reactive minimum returning undefined or NaN disables the constraint temporarily. A failure is { kind: 'min', min, actual, message }. The resolved limit contributes to min() metadata.

βœ… max​

Requires a number less than or equal to an inclusive maximum:

const myForm = form({
age: field(130, [max(120)]),
dependentAge: field(130, [max(() => maximumAge())]),
employeeAge: field(130, [max(120, 'Enter a realistic age.')]),
});

null and NaN pass. A reactive maximum returning undefined or NaN disables the constraint temporarily. A failure is { kind: 'max', max, actual, message }. The resolved limit contributes to max() metadata.

βœ… between​

Requires a number within an inclusive range:

const myForm = form({
age: field(17, [between(18, 65)]),
dependentAge: field(70, [
between(
() => allowedAge().minimum,
() => allowedAge().maximum,
),
]),
employeeAge: field(70, [
between(18, 65, 'Enter an age within the supported range.'),
]),
});

null and NaN pass. If either boundary resolves to undefined or NaN, the complete range is temporarily disabled. A failure is { kind: 'between', min, max, actual, message }. Both boundaries contribute to min() and max() metadata.

βœ… integer​

Requires a JavaScript safe integer. It can be passed directly or called with message options:

const myForm = form({
quantity: field(1.5, [integer]),
attempts: field(1.5, [
integer('Enter a whole number.'),
]),
});

It uses Number.isSafeInteger(), rejecting decimals, NaN, infinities, and integers outside JavaScript's exactly representable safe range. null passes. A failure is { kind: 'integer', actual, message }.

βœ… minLength​

Requires a numeric length or size to meet a minimum:

const myForm = form({
password: field('', [required, minLength(12)]),
dependentPassword: field('', [minLength(() => minimumPasswordLength())]),
tags: array(field(''), {
initialValue: [],
validators: [minLength(2, 'Choose more tags.')],
}),
});

It supports strings, arrays, sets, maps, and other values with numeric length or size. null and undefined pass; empty strings and collections have length zero and fail a positive minimum. A reactive minimum returning undefined disables the constraint. A failure is { kind: 'minLength', minLength, actual, message }. The resolved limit contributes to minLength() metadata without marking the node as required.

Unlike Angular Reactive Forms and Signal Forms v22.1.6, empty strings are measured. Use when for optional empty text, or combine with required to also reject nullish values. An empty string then produces both error kinds.

βœ… maxLength​

Requires a numeric length or size not to exceed a maximum:

const myForm = form({
biography: field('', [maxLength(500)]),
dependentBiography: field('', [maxLength(() => biographyLimit())]),
tags: array(field(''), {
initialValue: ['angular'],
validators: [maxLength(10, 'Choose at most 10 tags.')],
}),
});

It supports the same length and size values as minLength. null and '' pass, while empty collections are measured normally. A reactive maximum returning undefined disables the constraint. A failure is { kind: 'maxLength', maxLength, actual, message }. The resolved limit contributes to maxLength() metadata.

βœ… lengthBetween​

Combines inclusive minimum and maximum text or collection lengths:

lengthBetween(1, 5)
lengthBetween(() => minimumLength(), () => maximumLength())
lengthBetween(1, 5, 'Use between one and five characters.')

It supports the same values as minLength, including nullish values, which pass. Empty strings and collections fail a positive minimum. Failures retain the minLength or maxLength kind, parameters, and message fallbacks. Each bound contributes its corresponding constraint metadata; returning undefined or NaN disables only that bound. A false when disables both bounds. See lengthBetween() for executable form and array examples, custom errors, and edge cases.

βœ… minWords​

Requires a non-empty string to contain at least a number of words:

const myForm = form({
summary: field('', [required, minWords(3)]),
description: field('', [minWords(() => minimumWords())]),
biography: field('', [minWords(3, 'Add more detail.')]),
});

null and '' pass. A reactive minimum returning undefined or NaN disables the constraint. A word is a Unicode letter-or-number sequence that may contain internal apostrophes or hyphens, so L'Γ©tΓ© and well-known each count as one word. A failure is { kind: 'minWords', minWords, actual, message }, where actual is the observed word count.

βœ… maxWords​

Requires a non-empty string to contain no more than a number of words:

const myForm = form({
biography: field('', [maxWords(100)]),
description: field('', [maxWords(() => maximumWords())]),
summary: field('', [maxWords(100, 'Keep it concise.')]),
});

null and '' pass. A reactive maximum returning undefined or NaN disables the constraint. It uses the same Unicode word definition as minWords. A failure is { kind: 'maxWords', maxWords, actual, message }.

βœ… pattern​

Requires a non-empty string to match a regular expression:

const myForm = form({
code: field('', [pattern(/^[A-Z]{3}-\d{4}$/)]),
username: field('', [pattern(() => configuredPattern())]),
countryCode: field('', [pattern(/^[A-Z]+$/, 'Use letters only.')]),
});

null and '' pass. A reactive expression returning undefined disables the constraint. The expression's lastIndex is reset before every check, so global and sticky regular expressions do not reuse stale match state. A failure is { kind: 'pattern', pattern, actual, message }. Every active expression appears in pattern() metadata.

βœ… email​

Validates Angular's standard email-address format. It can be passed directly or called with message options:

const myForm = form({
email: field('', [required, email]),
workEmail: field('', [
email('Enter a valid work email.'),
]),
});

null and '' pass. Use required when the address must be present. The format includes local-part, domain-label, and total-length restrictions. A failure is { kind: 'email', message }; the rejected address is intentionally omitted from the error.

βœ… url​

Validates an absolute WHATWG URL. It can be passed directly or called with message options:

const myForm = form({
website: field('', [required, url]),
documentationUrl: field('', [
url('Enter a complete URL.'),
]),
});

null and '' pass. The validator uses new URL(value) without a base URL. It accepts absolute URLs with any valid schemeβ€”including https:, mailto:, and custom schemesβ€”but rejects relative references such as /account. A failure is { kind: 'url', message }; the rejected URL is intentionally omitted.

βœ… minDate​

Requires a valid date on or after an inclusive minimum:

const myForm = form({
appointment: field<Date>(null, [minDate('2026-08-24')]),
sameDayAppointment: field<Date>(null, [minDate('today')]),
reactiveTodayAppointment: field<Date>(null, [minDate(() => 'today')]),
earliestAppointment: field<Date>(null, [minDate(() => bookingWindowStart())]),
localAppointment: field<Date>(null, [
minDate('2026-08-24', {
parseAs: 'local',
}),
]),
messagedAppointment: field<Date>(null, [
minDate('2026-08-24', 'Choose a later date.'),
]),
momentAppointment: field<Date>(null, [
minDate(moment('2026-08-24').toDate()),
]),
});

The limit accepts a Date, an ISO calendar-date string (YYYY-MM-DD), the relative shortcut 'today', or a reactive function returning any of them. Strings and the shortcut use UTC midnight by default; parseAs: 'local' selects local midnight. The shortcut is resolved when validation runs, so minDate('today') does not permanently capture its declaration date. The library does not create a midnight timer; after the day changes, the boundary updates on the next value or reactive dependency change. null and invalid current dates pass. An absent or invalid resolved limit disables the constraint. A failure is { kind: 'minDate', minDate, actual, message }. The normalized Date contributes to min() metadata.

βœ… maxDate​

Requires a valid date on or before an inclusive maximum:

const myForm = form({
appointment: field<Date>(null, [maxDate('2026-12-31')]),
todayOnly: field<Date>(null, [maxDate('today')]),
reactiveAppointment: field<Date>(null, [maxDate(() => bookingWindowEnd())]),
localAppointment: field<Date>(null, [
maxDate('2026-12-31', { parseAs: 'local' }),
]),
messagedAppointment: field<Date>(null, [
maxDate('2026-12-31', 'Choose an earlier date.'),
]),
momentAppointment: field<Date>(null, [
maxDate(moment('2026-12-31').toDate()),
]),
});

It accepts the same absolute dates, relative shortcuts, reactive sources, and parsing modes as minDate. null and invalid current dates pass. An absent or invalid resolved limit disables the constraint. A failure is { kind: 'maxDate', maxDate, actual, message }. The normalized Date contributes to max() metadata.

βœ… dateBetween​

Requires a valid date within an inclusive range:

const myForm = form({
immediateBookingDate: field<Date>(null, [
dateBetween('today', '2026-12-31'),
]),
campaignDate: field<Date>(null, [
dateBetween('2026-08-24', '2026-09-30'),
]),
bookingDate: field<Date>(null, [
dateBetween(
() => bookingWindow().start,
() => bookingWindow().end,
),
]),
localDate: field<Date>(null, [
dateBetween('2026-08-24', '2026-09-30', { parseAs: 'local' }),
]),
messagedDate: field<Date>(null, [
dateBetween('2026-08-24', '2026-09-30', {
message: 'Choose a date within the booking window.',
}),
]),
});

Both boundaries accept a Date, YYYY-MM-DD, 'today', or a reactive source. parseAs applies to both string limits and the shortcut. null and invalid current dates pass. If either limit is absent or invalid, the range and both metadata constraints are disabled together. A failure is { kind: 'dateBetween', minDate, maxDate, actual, message }. The normalized boundaries contribute to min() and max() metadata.

βœ… oneOf​

Requires a non-empty value to equal one of the allowed values:

const myForm = form({
status: field('draft', [oneOf(['draft', 'published'])]),
reactiveStatus: field('draft', [oneOf(() => availableStatuses())]),
publicationStatus: field('draft', [
oneOf(['draft', 'published'], 'Choose an available status.'),
]),
});

null, undefined, and '' pass. A reactive source returning undefined disables the constraint. Membership uses Array.prototype.includes: NaN matches NaN, while objects compare by reference. A failure is { kind: 'oneOf', options, actual, message }.

βœ… equalTo​

Requires a value to equal a static or reactive expected value using Object.is():

const password = field('');
const myForm = form({
termsAccepted: field(false, [equalTo(true)]),
password,
confirmation: field('', [equalTo(() => password())]),
confirmedTerms: field(false, [
equalTo(true, 'You must accept the terms.'),
]),
});

Unlike optional format validators, null and undefined are compared as real values rather than skipped. Signals read by the expected-value source are tracked. A failure is { kind: 'equalTo', message }; both compared values are deliberately omitted so confirmation errors do not expose secrets.

βœ… uniqueItems​

Requires every array itemβ€”or a selected item keyβ€”to be unique. It supports direct use, an options factory, a property key selector, and a key-selector function:

const myForm = form({
roles: array(field(''), {
initialValue: ['admin', 'admin'],
validators: [uniqueItems],
}),
labelledRoles: array(field(''), {
initialValue: ['admin', 'admin'],
validators: [uniqueItems({ message: 'Roles must be unique.' })],
}),
explicitRoles: array(field(''), {
initialValue: ['admin', 'admin'],
validators: [uniqueItems()],
}),
contacts: array({
email: field(''),
name: field(''),
}, {
initialValue: [
{ email: 'same@example.com', name: 'First' },
{ email: 'same@example.com', name: 'Second' },
],
validators: [uniqueItems('email')],
}),
products: array(productTemplate, {
initialValue: initialProducts,
validators: [
uniqueItems<Product>(product => `${tenantId()}:${product.sku}`),
],
}),
});

Calling uniqueItems() without arguments is equivalent to direct [uniqueItems]. Without a key selector, comparison uses SameValueZero like Set: NaN matches NaN, 0 matches -0, and objects compare by reference. Property and function key selectors compare their derived keys; key-selector functions may read signals reactively.

null and undefined pass as empty arrays. Empty and one-item arrays pass. A failure is { kind: 'uniqueItems', duplicateIndexes, message }. It belongs to the array node and reports every participating index in ascending order while deliberately omitting duplicate values.

βœ… Constraint metadata​

Built-in constraints expose state even when the current value is valid:

age.min();
age.max();
password.minLength();
password.maxLength();
code.pattern();
name.required();

Multiple minimum constraints expose the strictest, largest minimum; multiple maximum constraints expose the strictest, smallest maximum. pattern() contains every active expression. Conditionally composed validators contribute metadata only while their branch is active. [formNode] forwards applicable metadata to native and compatible custom controls.

🌳 Tree shaking​

Validators are independent, side-effect-free exports. Consumer bundlers can remove validators and default messages that an application does not import; every validator remains available in the published package itself.

For authoring application-specific rules, see the validator() reference and asyncValidator() reference.