@absoluteui/helpers/validate
Format validators (email, IBAN, card, phone, postal code, and more) and a small typed schema validator.
import { ... } from '@absoluteui/helpers/validate';How to use
Section titled “How to use”Typed validation for forms and API input, plus format checks (email, IBAN, BIC, phone, postal code, and more). A v.* schema gives you the parsed, typed value on success, and field-keyed, user-readable messages on failure, ready for a form. The Helpers and services guide shows how fieldErrors from the error module fits in, and the Forms guide shows the messages in fields.
How do I validate a form and show a message per field?
Section titled “How do I validate a form and show a message per field?”Describe the record once; the type follows with Infer. An empty required field reports required. Form values arrive as strings, so use coerce for numbers and let v.date parse YYYY-MM-DD into a PlainDate. Unknown keys are stripped by default.
import { v, validate, type Infer } from '@absoluteui/helpers/validate';
const newHire = v.object({ name: v.string({ min: 1, max: 80, trim: true }), email: v.string({ email: true, trim: true, lowercase: true }), dept: v.enumOf(['finance', 'hr', 'engineering']), salary: v.number({ min: 0, integer: true, coerce: true }), startDate: v.date({ min: '2026-01-01' }), manager: v.optional(v.string()),});type NewHire = Infer<typeof newHire>;
const good = validate(newHire, { name: ' Asha Rao ', email: 'Asha.Rao@Example.com', dept: 'hr', salary: '98000', startDate: '2026-10-05', extra: 'x' });const hire: NewHire | undefined = good.ok ? good.value : undefined;hire?.email; // => 'asha.rao@example.com'hire?.salary; // => 98000'extra' in (hire ?? {}); // => false
const bad = validate(newHire, { name: '', email: 'asha@', dept: 'sales', salary: '-5', startDate: '2025-12-31' });bad.ok ? {} : bad.error.fields; // => { name: 'Enter a value for this field.', email: 'Enter an email address in the format name@example.com.', dept: 'Choose one of: finance, hr, engineering.', salary: 'Enter a number that is 0 or more.', startDate: 'Enter a date on or after 2026-01-01.' }How do I check one field against another?
Section titled “How do I check one field against another?”Wrap the object in v.refine with your own code. An issue on the whole object is reported under _form (change it with formKey); nested fields use paths such as items[2].qty.
import { v, validate } from '@absoluteui/helpers/validate';import { formatPlainDate } from '@absoluteui/helpers/date';
const leave = v.refine( v.object({ from: v.date(), to: v.date(), days: v.array(v.object({ half: v.boolean() }), { max: 30 }) }), (r) => formatPlainDate(r.to) >= formatPlainDate(r.from), 'leave.endBeforeStart',);const messages = (code: string) => (code === 'leave.endBeforeStart' ? 'The last day must be on or after the first day.' : `Check ${code}`);const r = validate(leave, { from: '2026-10-09', to: '2026-10-05', days: [] }, { messages });r.ok ? {} : r.error.fields; // => { _form: 'The last day must be on or after the first day.' }const nested = validate(leave, { from: '2026-10-05', to: '2026-10-06', days: [{ half: true }, { half: 'maybe' }] }, { messages });nested.ok ? {} : Object.keys(nested.error.fields ?? {}); // => ['days[1].half']How do I translate the messages?
Section titled “How do I translate the messages?”A message catalog is a function from code and parameters to text. Bind yours once with createValidator and fall back to defaultMessages for codes you have not translated. Parameters hold schema limits, never the user’s input.
import { createValidator, defaultMessages, v } from '@absoluteui/helpers/validate';
const german = (code: string, params: Readonly<Record<string, unknown>>) => code === 'number.min' ? `Geben Sie mindestens ${String(params.min)} ein.` : defaultMessages(code, params);const check = createValidator(german);const r = check(v.object({ hours: v.number({ min: 1 }), note: v.string({ max: 5 }) }), { hours: 0, note: 'too long' });r.ok ? {} : r.error.fields; // => { hours: 'Geben Sie mindestens 1 ein.', note: 'Enter no more than 5 characters.' }How do I check bank details and contact data?
Section titled “How do I check bank details and contact data?”Format checks are type guards that accept unknown, so they work on raw input. isIban verifies the country length and checksum; isPostalCode checks the format for a country (not that the address exists). Use them in a schema with v.refine.
import { isBic, isIban, isPhoneE164, isPostalCode, v, validate } from '@absoluteui/helpers/validate';
isIban('DE89 3704 0044 0532 0130 00'); // => trueisIban('DE89 3704 0044 0532 0130 01'); // => false // checksumisBic('COBADEFFXXX'); // => trueisPostalCode('560001', 'IN'); // => trueisPhoneE164('+918055501234'); // => true
const payee = v.object({ iban: v.refine(v.string({ trim: true }), isIban, 'iban') });const r = validate(payee, { iban: 'GB00 WEST 1234 5698 7654 32' }, { messages: (code) => (code === 'iban' ? 'Enter a valid IBAN.' : code) });r.ok ? {} : r.error.fields; // => { iban: 'Enter a valid IBAN.' }How do I enforce a password policy?
Section titled “How do I enforce a password policy?”isStrongPassword returns every failed rule as a code, so the UI can list what to fix. Defaults follow a strict policy (12 characters, four classes, no common passwords); NIST prefers length, so relax the class rules for passphrases.
import { isStrongPassword } from '@absoluteui/helpers/validate';
isStrongPassword('summer2026').failures; // => ['too-short', 'missing-uppercase', 'missing-symbol', 'common']const passphrase = { minLength: 16, requireUppercase: false, requireDigit: false, requireSymbol: false, allowSpaces: true };isStrongPassword('correct horse battery staple', passphrase); // => { valid: true, failures: [] }isStrongPassword('ExampleCorp2026!', { extraCommon: ['examplecorp2026!'] }).failures; // => ['common']What goes wrong?
Section titled “What goes wrong?”Validation never throws for bad input; limits (depth 32, 100 000 values, 1 000 000-character strings) become issues too. unknownKeys: 'error' rejects extra fields instead of stripping them. Format checks are not proof: isJwtShape checks structure only, so verify tokens on the server.
import { isJwtShape, v, validate } from '@absoluteui/helpers/validate';
const strict = v.object({ id: v.string() }, { unknownKeys: 'error' });const r = validate(strict, { id: 'E-1', isAdmin: true });r.ok ? {} : r.error.fields; // => { isAdmin: 'Remove this field; it is not recognized.' }r.ok ? undefined : r.error.code; // => 'HLP_VALIDATE_FAILED'validate(v.string(), 'x'.repeat(20), { maxStringLength: 10 }).ok; // => falseisJwtShape('eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiJ9.'); // => true // an unsigned alg:none token still has the right shapeAPI reference
Section titled “API reference”General
Section titled “General”Namespaces
Section titled “Namespaces”v
namespace v { string number boolean date literal enumOf array object optional nullable union refine transform}Formats
Section titled “Formats”Functions
Section titled “Functions”cardBrand
Card network by IIN range and length (no Luhn check; combine with {@link isCreditCard}).
Returns undefined when no network matches.
function cardBrand(value: unknown): CardBrand | undefined;Example:
cardBrand('4111111111111111') // => 'visa'Example:
cardBrand('378282246310005') // => 'amex'compareSemver
SemVer 2.0.0 precedence (-1, 0, 1); build metadata is ignored. Throws a
ValidationError with code HLP_VALIDATE_SEMVER for invalid input.
function compareSemver(a: string, b: string): -1 | 0 | 1;Example:
compareSemver('1.0.0-alpha', '1.0.0') // => -1isBic
ISO 9362 BIC/SWIFT code: 4-letter institution, 2-letter country, 2 location characters, optional 3-character branch. Case-insensitive.
function isBic(value: unknown): value is string;Example:
isBic('DEUTDEFF500') // => trueisCidr
CIDR block: IPv4 with /0-/32 or IPv6 with /0-/128.
function isCidr(value: unknown, version?: 4 | 6): value is string;Example:
isCidr('10.0.0.0/8') // => trueisCreditCard
Payment card number: 12-19 digits (spaces and dashes allowed) with a valid Luhn checksum.
function isCreditCard(value: unknown): value is string;Example:
isCreditCard('4111 1111 1111 1111') // => trueisEan13
EAN-13 / GTIN-13 barcode with a valid check digit.
function isEan13(value: unknown): value is string;Example:
isEan13('4006381333931') // => trueisEmail
Practical RFC 5322 addr-spec subset: dot-atom local part (no quoted strings, comments or
IP literals), host-name domain; at most 254 characters overall and 64 bytes before the @.
function isEmail(value: unknown, options?: EmailOptions): value is string;Example:
isEmail('ada@example.com') // => trueExample:
isEmail('a..b@example.com') // => falseisHexColor
CSS hex colour #rgb, #rrggbb, and (unless alpha: false) #rgba, #rrggbbaa.
function isHexColor(value: unknown, options?: { readonly alpha?: boolean; readonly requireHash?: boolean;}): value is string;Example:
isHexColor('#1a2b3c') // => trueisHttpUrl
http: or https: URL with a host and no credentials.
function isHttpUrl(value: unknown): value is string;Example:
isHttpUrl('https://example.com') // => trueisIban
ISO 13616 IBAN: country length table and ISO 7064 mod-97 checksum. Spaces are ignored; letters are case-insensitive.
function isIban(value: unknown): value is string;Example:
isIban('GB82 WEST 1234 5698 7654 32') // => trueisIpv4
Dotted-quad IPv4 with no leading zeros (01.2.3.4 is rejected as ambiguous octal).
function isIpv4(value: unknown): value is string;Example:
isIpv4('192.168.0.1') // => trueisIpv6
IPv6 in any RFC 4291 text form, including :: compression and an embedded IPv4 tail.
Zone identifiers (%eth0) are rejected.
function isIpv6(value: unknown): value is string;Example:
isIpv6('2001:db8::ff00:42:8329') // => trueisIsbn
ISBN-10 or ISBN-13 (hyphens and spaces ignored) with a valid check digit. Pass 10 or 13
to require one form.
function isIsbn(value: unknown, version?: 10 | 13): value is string;Example:
isIsbn('978-0-306-40615-7') // => trueExample:
isIsbn('0-306-40615-2', 10) // => trueisIsoDate
Strict calendar date YYYY-MM-DD (4-digit year, real day of month).
function isIsoDate(value: unknown): value is string;Example:
isIsoDate('2024-02-29') // => trueExample:
isIsoDate('2023-02-29') // => falseisIsoDateTime
Strict RFC 3339 date-time: YYYY-MM-DDTHH:mm[:ss[.fraction]] plus Z or +/-HH:MM
(required unless requireOffset: false). Uppercase T/Z only; no leap seconds.
function isIsoDateTime(value: unknown, options?: { readonly requireOffset?: boolean; readonly requireSeconds?: boolean;}): value is string;Example:
isIsoDateTime('2026-09-29T14:30:00.123Z') // => trueisJwtShape
JWS compact serialization shape: three base64url segments whose header is a JSON object with
a string alg and whose payload is a JSON object. Structure only: this never means the token
is valid, trusted or unexpired; verify signatures on the server.
function isJwtShape(value: unknown, options?: { readonly maxLength?: number;}): value is string;Example:
isJwtShape('eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln') // => trueisLatitude
Latitude in decimal degrees, -90…90 (number or plain decimal string).
function isLatitude(value: unknown): boolean;Example:
isLatitude(51.5074) // => trueisLongitude
Longitude in decimal degrees, -180…180 (number or plain decimal string).
function isLongitude(value: unknown): boolean;Example:
isLongitude('-0.1278') // => trueisMacAddress
MAC-48 address as 01:23:45:67:89:ab, 01-23-45-67-89-ab or 0123.4567.89ab.
function isMacAddress(value: unknown): value is string;Example:
isMacAddress('01:23:45:67:89:AB') // => trueisPhone
Loose phone check for user input: digits, spaces, (), -, ., an optional leading +,
and 7-15 digits.
function isPhone(value: unknown): value is string;Example:
isPhone('+1 (555) 010-9999') // => trueisPhoneE164
E.164: +, a non-zero country code digit, 7-15 digits in total, nothing else.
function isPhoneE164(value: unknown): value is string;Example:
isPhoneE164('+442079460958') // => trueisPostalCode
Postal code format for a country in {@link POSTAL_CODE_COUNTRIES} (format only; not an address lookup). Unsupported countries return false.
function isPostalCode(value: unknown, country: string): value is string;Example:
isPostalCode('SW1A 1AA', 'GB') // => trueExample:
isPostalCode('90210-1234', 'US') // => trueisSemver
Strict SemVer 2.0.0 check.
function isSemver(value: unknown): value is string;Example:
isSemver('1.0.0-alpha.1') // => trueExample:
isSemver('01.0.0') // => falseisSlug
Lowercase URL slug: ASCII letters and digits in hyphen-separated groups (max 200 chars).
function isSlug(value: unknown): value is string;Example:
isSlug('hello-world-2') // => trueisStrongPassword
Checks a password against a policy and returns every failed rule as a code the UI can translate. Defaults: 12-128 characters, all four classes, no spaces, not common. NIST SP 800-63B favours length over composition; relax the class flags for passphrase policies.
function isStrongPassword(value: string, policy?: PasswordPolicy): PasswordCheck;Example:
isStrongPassword('Tr1cky-Horse-42') // => { valid: true, failures: [] }Example:
isStrongPassword('Password123!').failures // => ['common']isUlid
ULID: 26 Crockford base32 characters, first character 0-7 (48-bit timestamp).
function isUlid(value: unknown): value is string;Example:
isUlid('01ARZ3NDEKTSV4RRFFQ69G5FAV') // => trueisUrl
Absolute URL check via the platform URL parser plus a scheme allow-list. Rejects raw whitespace/control characters (which the parser would silently strip) and, by default, embedded credentials.
function isUrl(value: unknown, options?: UrlOptions): value is string;Example:
isUrl('https://example.com/a?b=1') // => trueExample:
isUrl('javascript:alert(1)') // => falseisUuid
RFC 9562 UUID (versions 1-8, variant 10xx). Without version, the nil and max UUIDs are
accepted too.
function isUuid(value: unknown, version?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8): value is string;Example:
isUuid('0190f2a8-1b2c-7d3e-8f40-123456789abc', 7) // => trueluhnCheck
Luhn (mod 10) checksum over a digit string. Non-digits make it false.
function luhnCheck(digits: string): boolean;Example:
luhnCheck('79927398713') // => trueparseSemver
Parses a strict SemVer 2.0.0 string (no v prefix, no leading zeros, safe-integer parts).
function parseSemver(value: unknown): SemVer | undefined;Example:
parseSemver('1.2.3-rc.1+build.5') // => { major: 1, minor: 2, patch: 3, prerelease: ['rc', '1'], build: ['build', '5'] }Constants
Section titled “Constants”IBAN_COUNTRIES
Countries with a known IBAN length.
const IBAN_COUNTRIES: readonly string[];Example:
IBAN_COUNTRIES.includes('DE') // => truePOSTAL_CODE_COUNTRIES
ISO 3166-1 alpha-2 codes supported by {@link isPostalCode}.
const POSTAL_CODE_COUNTRIES: readonly string[];Example:
POSTAL_CODE_COUNTRIES.includes('GB') // => trueInterfaces
Section titled “Interfaces”EmailOptions
interface EmailOptions { readonly allowIdn?: boolean; readonly requireTld?: boolean;}PasswordCheck
interface PasswordCheck { readonly valid: boolean; readonly failures: readonly PasswordFailure[];}PasswordPolicy
interface PasswordPolicy { readonly minLength?: number; readonly maxLength?: number; readonly requireLowercase?: boolean; readonly requireUppercase?: boolean; readonly requireDigit?: boolean; readonly requireSymbol?: boolean; readonly minClasses?: number; readonly allowSpaces?: boolean; readonly rejectCommon?: boolean; readonly extraCommon?: readonly string[];}SemVer
Parsed Semantic Versioning 2.0.0 value.
interface SemVer { readonly major: number; readonly minor: number; readonly patch: number; readonly prerelease: readonly string[]; readonly build: readonly string[];}UrlOptions
interface UrlOptions { readonly schemes?: readonly string[]; readonly requireHost?: boolean; readonly allowCredentials?: boolean; readonly maxLength?: number;}CardBrand
Card networks recognized by {@link cardBrand}.
type CardBrand = 'visa' | 'mastercard' | 'amex' | 'discover' | 'diners' | 'jcb' | 'unionpay' | 'maestro';PasswordFailure
Failure codes returned by {@link isStrongPassword}.
type PasswordFailure = 'too-short' | 'too-long' | 'missing-lowercase' | 'missing-uppercase' | 'missing-digit' | 'missing-symbol' | 'too-few-classes' | 'contains-space' | 'common';Schema
Section titled “Schema”Functions
Section titled “Functions”createValidator
Binds a message catalog (for example a localized one) to {@link validate}.
function createValidator(messages: MessageCatalog, defaults?: Omit<ValidateOptions, 'messages'>): <T>(schema: Schema<T>, input: unknown, options?: Omit<ValidateOptions, "messages">) => Result<T, ValidationError>;Example:
const check = createValidator((code) => `validation.${code}`);const r = check(v.object({ name: v.string({ min: 1 }) }), { name: 42 });r.ok ? {} : r.error.fields; // => { name: 'validation.type.string' }validate
Validates input. On failure returns a ValidationError (HLP_VALIDATE_FAILED) whose
fields map each path to its first message and whose details.issues list every issue.
function validate<T>(schema: Schema<T>, input: unknown, options?: ValidateOptions): Result<T, ValidationError>;Example:
const r = validate(v.object({ email: v.string({ email: true }) }), { email: 'x' });r.ok ? {} : r.error.fields; // => { email: 'Enter an email address in the format name@example.com.' }Constants
Section titled “Constants”defaultMessages
English default catalog. Unknown codes get a generic, still actionable message.
const defaultMessages: MessageCatalog;Example:
defaultMessages('string.min', { min: 3 }) // => 'Enter at least 3 characters.'Interfaces
Section titled “Interfaces”DateOptions
interface DateOptions { readonly min?: DateInput; readonly max?: DateInput; readonly timeZone?: string;}Issue
One validation failure. params hold schema-defined values (limits, options), never input.
interface Issue { readonly path: string; readonly code: string; readonly params: Readonly<Record<string, unknown>>;}NumberOptions
interface NumberOptions { readonly min?: number; readonly max?: number; readonly integer?: boolean; readonly coerce?: boolean;}ObjectOptions
interface ObjectOptions { readonly unknownKeys?: 'strip' | 'error' | 'keep';}OptionalSchema
Schema produced by v.optional.
interface OptionalSchema<T> extends Schema<T | undefined> { readonly optional: true;}ParseContext
Internal traversal state (exported for declaration output only).
interface ParseContext { readonly issues: Issue[]; depth: number; nodes: number; stopped: boolean; readonly maxDepth: number; readonly maxNodes: number; readonly maxIssues: number; readonly maxStringLength: number;}Schema
A validator for values of type T. Build with the v.* functions.
interface Schema<T> { readonly kind: string; readonly optional: boolean; readonly run: (input: unknown, ctx: ParseContext, path: string) => Outcome<T>;}StringOptions
interface StringOptions { readonly min?: number; readonly max?: number; readonly pattern?: RegExp; readonly patternCode?: string; readonly trim?: boolean; readonly lowercase?: boolean; readonly email?: boolean | EmailOptions; readonly url?: boolean | UrlOptions;}ValidateOptions
interface ValidateOptions { readonly messages?: MessageCatalog; readonly maxDepth?: number; readonly maxNodes?: number; readonly maxIssues?: number; readonly maxStringLength?: number; readonly formKey?: string;}Infer
Output type of a schema.
type Infer<S> = S extends Schema<infer T> ? T : never;Example:
const userSchema = v.object({ email: v.string({ email: true }), age: v.optional(v.number()) });type User = Infer<typeof userSchema>; // { email: string; age?: number | undefined }InferShape
Object type inferred from a shape; v.optional properties become optional.
type InferShape<S extends Shape> = Simplify<{ -readonly [K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>;} & { -readonly [K in OptionalKeys<S>]?: Infer<S[K]>;}>;MessageCatalog
Resolves a message code to text. Replace it to localize.
type MessageCatalog = (code: string, params: Readonly<Record<string, unknown>>) => string;Outcome
Internal parse outcome.
type Outcome<T> = { readonly ok: true; readonly value: T;} | { readonly ok: false;};