Skip to content

@absoluteui/helpers/validate

Format validators (email, IBAN, card, phone, postal code, and more) and a small typed schema validator.

import { ... } from '@absoluteui/helpers/validate';

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.' }

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']

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'); // => true
isIban('DE89 3704 0044 0532 0130 01'); // => false // checksum
isBic('COBADEFFXXX'); // => true
isPostalCode('560001', 'IN'); // => true
isPhoneE164('+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.' }

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']

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; // => false
isJwtShape('eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiJ9.'); // => true // an unsigned alg:none token still has the right shape

v

namespace v {
string
number
boolean
date
literal
enumOf
array
object
optional
nullable
union
refine
transform
}

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') // => -1

isBic

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') // => true

isCidr

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') // => true

isCreditCard

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') // => true

isEan13

EAN-13 / GTIN-13 barcode with a valid check digit.

function isEan13(value: unknown): value is string;

Example:

isEan13('4006381333931') // => true

isEmail

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') // => true

Example:

isEmail('a..b@example.com') // => false

isHexColor

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') // => true

isHttpUrl

http: or https: URL with a host and no credentials.

function isHttpUrl(value: unknown): value is string;

Example:

isHttpUrl('https://example.com') // => true

isIban

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') // => true

isIpv4

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') // => true

isIpv6

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') // => true

isIsbn

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') // => true

Example:

isIsbn('0-306-40615-2', 10) // => true

isIsoDate

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') // => true

Example:

isIsoDate('2023-02-29') // => false

isIsoDateTime

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') // => true

isJwtShape

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') // => true

isLatitude

Latitude in decimal degrees, -90…90 (number or plain decimal string).

function isLatitude(value: unknown): boolean;

Example:

isLatitude(51.5074) // => true

isLongitude

Longitude in decimal degrees, -180…180 (number or plain decimal string).

function isLongitude(value: unknown): boolean;

Example:

isLongitude('-0.1278') // => true

isMacAddress

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') // => true

isPhone

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') // => true

isPhoneE164

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') // => true

isPostalCode

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') // => true

Example:

isPostalCode('90210-1234', 'US') // => true

isSemver

Strict SemVer 2.0.0 check.

function isSemver(value: unknown): value is string;

Example:

isSemver('1.0.0-alpha.1') // => true

Example:

isSemver('01.0.0') // => false

isSlug

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') // => true

isStrongPassword

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') // => true

isUrl

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') // => true

Example:

isUrl('javascript:alert(1)') // => false

isUuid

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) // => true

luhnCheck

Luhn (mod 10) checksum over a digit string. Non-digits make it false.

function luhnCheck(digits: string): boolean;

Example:

luhnCheck('79927398713') // => true

parseSemver

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'] }

IBAN_COUNTRIES

Countries with a known IBAN length.

const IBAN_COUNTRIES: readonly string[];

Example:

IBAN_COUNTRIES.includes('DE') // => true

POSTAL_CODE_COUNTRIES

ISO 3166-1 alpha-2 codes supported by {@link isPostalCode}.

const POSTAL_CODE_COUNTRIES: readonly string[];

Example:

POSTAL_CODE_COUNTRIES.includes('GB') // => true

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';

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.' }

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.'

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;
};