npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@archi-code/validation

v3.0.0-alpha.6

Published

String validators, type converters and sanitizers for TypeScript in a single package. The validators are a hardened port of [validator.js](https://github.com/validatorjs/validator.js). It has no runtime dependencies and ships CJS and ESM builds that also

Readme

@archi-code/validation

String validators, type converters and sanitizers for TypeScript in a single package. The validators are a hardened port of validator.js. It has no runtime dependencies and ships CJS and ESM builds that also work in the browser.

Installation

npm install @archi-code/validation

Requires Node.js 24 or later. In the browser, the browser field and the browser condition of exports point to the ESM build.

Import

Everything is a named export (there is no export default).

// ESM / TypeScript
import { isEmail, toInteger, canBeDate, sanitizer } from '@archi-code/validation';
import type { IsEmailOptions, Converted } from '@archi-code/validation';
// CommonJS
const { isEmail, toInteger, canBeDate, sanitizer } = require('@archi-code/validation');

Each validator is exported on its own and is exactly the same function as its member of the validator registry (isEmail === validator.isEmail). The package declares "sideEffects": false, so the bundler only includes what you import:

import { isEmail } from '@archi-code/validation'; // only isEmail and its helpers
import { validator } from '@archi-code/validation'; // every validator and locale table

Use validator only when you need dynamic access (validator[name]); otherwise, import by name. validator is frozen (Object.freeze): it cannot be patched.

Three tools

| Tool | Question it answers | Returns | | --------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------- | | isX(value, options?) (also validator.isX) | Is this value a valid X? (email, URL, IBAN, mobile number…) | boolean | | toX(value, options?) | Converts this value to type X. | { ok, value, error } | | canBeX(value) | Can this value be converted to type X? | boolean (always equal to toX(value).ok) |

The type rules live only in the to* converters; canBe* and the validators rely on them, so canBeX(v) === toX(v).ok always holds.

import {
  isEmail,
  isIBAN,
  isMobilePhone,
  toInteger,
  canBeDate,
  sanitizer,
} from '@archi-code/validation';

isEmail('[email protected]'); // true
isIBAN('ES91 2100 0418 4502 0005 1332'); // true
isMobilePhone('+34612345678', { locale: 'es-ES' }); // true
toInteger(' 42 '); // { ok: true, value: 42, error: null }
toInteger('abc'); // { ok: false, value: null, error: 'Value is not an integer' }
canBeDate('2024-01-31'); // true
sanitizer.escape('<a>'); // '&lt;a&gt;'

Contract

  • A validator returns only boolean and never throws because of the value: null, objects, Symbol, huge strings or lone surrogates return false.
  • An invalid configuration throws ValidationConfigError, and only that: unknown locales, country codes, algorithms or providers, a malformed decimal_digits, a pattern that is not a valid RegExp, options that are not an object… Never a raw TypeError or SyntaxError.
  • A single options object. Each validator takes the value and, at most, one options object. There are no positional parameters or shorthand forms: isLength(v, 2) or isIP(v, 4) throw ValidationConfigError; write isLength(v, { min: 2 }) and isIP(v, { version: 4 }).
  • null as options means "no options", just like undefined.
  • Non-string input. Validators read the value with toString: strings are used as they are, finite numbers are converted with String() and booleans become 'true'/'false'. Any other value gives false.
  • The to* converters never throw. They always return { ok: true, value, error: null } or { ok: false, value: null, error }. error is one of the fixed messages of ConvertMessages and never contains the value. A successful numeric result is always a finite number.
import { isInt, isAlpha, isEmail, isLength, isHash } from '@archi-code/validation';

isInt(5); // true
isAlpha(true); // true
isEmail(null); // false
isEmail('[email protected]', null); // true
isLength('x', 2); // throws ValidationConfigError: options must be an object, got 2
isHash('x', { algorithm: 'nope' }); // throws ValidationConfigError: Invalid hash algorithm 'nope'

is* validators

There are 91 validators. Each one has one of these three signatures:

isPort(value); // only the value
isAlpha(value, options?); // every option is optional
isVAT(value, options); // the options object has at least one required property

value is always unknown. options is typed by the <Validator>Options interface exported next to the validator (IsEmailOptions, IsVATOptions, MatchesOptions…): the editor autocompletes the options and flags unknown ones as errors. Locale, country and algorithm parameters suggest the known values and accept any string (an unknown value throws ValidationConfigError at runtime). An omitted option takes the validator's default value.

In the tables, bold = required property.

Text and content

| Signature | Options (default value) | Checks that the value… | | --------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | isAlpha(value, options?) | locale ('en-US', see isAlphaLocales), ignore (string or RegExp of characters to ignore) | has only letters of the locale | | isAlphanumeric(value, options?) | locale ('en-US', see isAlphanumericLocales), ignore | has only letters of the locale and digits | | isAscii(value) | — | has only ASCII characters | | isLowercase(value) | — | does not change when lowercased | | isUppercase(value) | — | does not change when uppercased | | isFullWidth(value) | — | contains full-width characters | | isHalfWidth(value) | — | contains half-width characters | | isVariableWidth(value) | — | mixes full-width and half-width characters | | isMultibyte(value) | — | contains multibyte characters | | isSurrogatePair(value) | — | contains UTF-16 surrogate pairs | | isSlug(value) | — | is a slug (my-article-1) | | isLength(value, options?) | min (0), max, discreteLengths (allowed lengths), graphemes (count graphemes) | has a length within the range | | isByteLength(value, options?) | min (0), max | takes a number of UTF-8 bytes within the range | | isEmpty(value, options?) | ignore_whitespace (false) | is the empty string | | isNotEmpty(value) | — | is a string with some non-whitespace character, or a finite number | | isRequired(value) | — | is neither null nor undefined | | contains(value, options) | elem, ignoreCase (false), minOccurrences (1) | contains elem at least minOccurrences times | | equals(value, options) | comparison | is equal to comparison | | matches(value, options) | pattern (RegExp or string), modifiers (flags when pattern is a string) | matches the pattern | | isIn(value, options) | values (array, object or string) | is in the array, is a key of the object or is a substring of the string | | isWhitelisted(value, options) | chars (string or array of characters) | has only characters from chars |

import {
  isAlpha,
  isLength,
  isNotEmpty,
  isRequired,
  contains,
  matches,
  isIn,
} from '@archi-code/validation';

isAlpha('año'); // false
isAlpha('año nuevo', { locale: 'es-ES', ignore: ' ' }); // true
isLength('hello', { min: 2, max: 10 }); // true
isLength('👍🏽', { max: 1 }); // false
isLength('👍🏽', { max: 1, graphemes: true }); // true
isNotEmpty('  '); // false
isNotEmpty(0); // true
isRequired(''); // true
isRequired(null); // false
contains('Hello World', { elem: 'world', ignoreCase: true }); // true
matches('ABC-123', { pattern: /^[A-Z]{3}-\d{3}$/ }); // true
matches('abc', { pattern: '^ABC$', modifiers: 'i' }); // true
isIn('b', { values: ['a', 'b'] }); // true
isIn('es', { values: { es: 1, en: 2 } }); // true

Numbers

| Signature | Options (default value) | Checks that the value… | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | isInt(value, options?) | min, max, lt, gt, allow_leading_zeroes (true) | is an integer (within the given limits) | | isFloat(value, options?) | min, max, lt, gt, locale (decimal separator of the locale, see isFloatLocales) | is a finite decimal number (within the given limits) | | isDecimal(value, options?) | force_decimal (false), decimal_digits ('1,': range of decimal places, e.g. '1,2'), locale ('en-US') | is a decimal number | | isNumeric(value, options?) | no_symbols (no sign or point), locale (decimal separator; . when not given) | has only digits (and sign/separator when allowed) | | isDivisibleBy(value, options) | num | is a number divisible by num | | isHexadecimal(value) | — | is a hexadecimal number | | isOctal(value) | — | is an octal number | | isPort(value) | — | is an integer between 0 and 65535 without leading zeros | | isBoolean(value, options?) | loose (false) | is exactly 'true', 'false', '1' or '0'; with loose, case-insensitive and also 'yes'/'no' |

import {
  isInt,
  isFloat,
  isDecimal,
  isNumeric,
  isDivisibleBy,
  isPort,
  isBoolean,
} from '@archi-code/validation';

isInt('42', { min: 50 }); // false
isInt('007', { allow_leading_zeroes: false }); // false
isFloat('3,14', { locale: 'es-ES' }); // true
isFloat('5.5', { gt: 5, lt: 6 }); // true
isFloat('1e400'); // false
isDecimal('10', { force_decimal: true }); // false
isDecimal('10.555', { decimal_digits: '1,2' }); // false
isNumeric('-12.5', { no_symbols: true }); // false
isDivisibleBy('21', { num: 7 }); // true
isPort('70000'); // false
isBoolean('TRUE'); // false
isBoolean('yes', { loose: true }); // true

Internet

| Signature | Options (default value) | Checks that the value… | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | isEmail(value, options?) | allow_display_name (false), require_display_name (false), allow_utf8_local_part (true), require_tld (true), allow_underscores (false), blacklisted_chars (''), ignore_max_length (false), host_blacklist ([]), host_whitelist ([]), allow_ip_domain, domain_specific_validation | is an email address | | isURL(value, options?) | protocols (['http', 'https', 'ftp']), require_tld (true), require_protocol (false), require_host (true), require_port (false), require_valid_protocol (true), allow_underscores (false), allow_trailing_dot (false), allow_protocol_relative_urls (false), allow_fragments (true), allow_query_components (true), validate_length (true), max_allowed_length (2084), disallow_auth, host_whitelist, host_blacklist, allow_wildcard, allow_numeric_tld, ignore_max_length | is a URL | | isFQDN(value, options?) | require_tld (true), allow_underscores (false), allow_trailing_dot (false), allow_numeric_tld (false), allow_wildcard (false), ignore_max_length (false) | is a fully qualified domain name | | isIP(value, options?) | version (4 or 6; when not given, either) | is an IP address | | isIPRange(value, options?) | version (4 or 6; when not given, either) | is a CIDR range | | isMACAddress(value, options?) | no_separators, no_colons, eui ('48' or '64'; when not given, either) | is a MAC address | | isDataURI(value) | — | is a data URI | | isMagnetURI(value) | — | is a magnet link | | isMailtoURI(value, options?) | the isEmail options, applied to each address | is a mailto: URI |

import { isEmail, isURL, isFQDN, isIP, isIPRange, isMACAddress } from '@archi-code/validation';

isEmail('Ana <[email protected]>'); // false
isEmail('Ana <[email protected]>', { allow_display_name: true }); // true
isEmail('ana@localhost', { require_tld: false }); // true
isEmail('[email protected]', { host_blacklist: ['spam.com'] }); // false
isURL('example.com'); // true
isURL('example.com', { require_protocol: true }); // false
isURL('ftp://example.com', { protocols: ['https'] }); // false
isFQDN('localhost'); // false
isIP('::1', { version: 4 }); // false
isIPRange('10.0.0.0/8'); // true
isMACAddress('0123456789ab', { no_separators: true }); // true

Dates and times

| Signature | Options (default value) | Checks that the value… | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | isDate(value, options?) | format ('YYYY/MM/DD'), delimiters (['/', '-']), strictMode (false), twoDigitYearPivot (current two-digit year) | is a calendar date in that format (or a valid Date) | | isTime(value, options?) | hourFormat ('hour24' or 'hour12'), mode ('default', 'withSeconds' or 'withOptionalSeconds') | is a time | | isISO8601(value, options?) | strict (also checks that the date exists), strictSeparator (requires T) | is an ISO 8601 date | | isRFC3339(value) | — | is an RFC 3339 date | | isAfter(value, options?) | comparisonDate (now, by default) | is a date after comparisonDate | | isBefore(value, options?) | comparisonDate (now, by default) | is a date before comparisonDate |

isAfter and isBefore parse dates deterministically, without depending on the machine's time zone: they accept ISO dates (also with reduced precision, such as '2024' or '2024-03') and the output of Date#toString() and Date#toUTCString(), but not engine-dependent formats such as '2030/01/01'.

import { isDate, isTime, isISO8601, isAfter, isBefore } from '@archi-code/validation';

isDate('2024-01-31'); // true
isDate('2024/02/30'); // false
isDate('31/01/2024', { format: 'DD/MM/YYYY' }); // true
isDate('01/01/24', { format: 'DD/MM/YY', twoDigitYearPivot: 50 }); // true
isTime('11:30 PM', { hourFormat: 'hour12' }); // true
isTime('23:59:59', { mode: 'withSeconds' }); // true
isISO8601('2024-02-30', { strict: true }); // false
isAfter('2024-06-01', { comparisonDate: '2024-01-01' }); // true
isBefore('2024-01-01', { comparisonDate: '2024-06-01' }); // true
isAfter('2030/01/01'); // false

Identifiers, hashes and encodings

| Signature | Options (default value) | Checks that the value… | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | isUUID(value, options?) | version ('all'; also 'loose', 'nil', 'max', 1…8) | is a UUID | | isULID(value) | — | is a ULID | | isMongoId(value) | — | is a MongoDB ObjectId | | isJWT(value) | — | has the shape of a JWT | | isMD5(value) | — | is an MD5 hash | | isHash(value, options) | algorithm: md5, md4, sha1, sha256, sha384, sha512, ripemd128, ripemd160, tiger128, tiger160, tiger192, crc32, crc32b | is a hexadecimal hash of that algorithm | | isBase32(value, options?) | crockford (false) | is Base32 | | isBase58(value) | — | is Base58 | | isBase64(value, options?) | urlSafe (false), padding (true by default, or false with urlSafe) | is Base64 | | isJSON(value, options?) | allow_primitives (false: also true, false and null), allow_any_value (false: any JSON) | is JSON that parses to an object or array | | isSemVer(value) | — | is a semantic version | | isMimeType(value) | — | is a MIME type |

import { isUUID, isHash, isBase64, isJSON } from '@archi-code/validation';

isUUID('550e8400-e29b-41d4-a716-446655440000', { version: 4 }); // true
isUUID('550e8400-e29b-11d4-a716-446655440000', { version: 4 }); // false
isUUID('00000000-0000-0000-0000-000000000000', { version: 'nil' }); // true
isHash('d41d8cd98f00b204e9800998ecf8427e', { algorithm: 'md5' }); // true
isBase64('SGVsbG8='); // true
isBase64('SGVsbG8', { urlSafe: true }); // true
isJSON('1'); // false
isJSON('true', { allow_primitives: true }); // true
isJSON('1', { allow_any_value: true }); // true

Colors

| Signature | Options (default value) | Checks that the value… | | ----------------------------- | -------------------------------------------------------- | ---------------------------- | | isHexColor(value, options?) | require_hashtag (false) | is a hexadecimal color | | isRgbColor(value, options?) | includePercentValues (true), allowSpaces (false) | is an rgb()/rgba() color | | isHSL(value) | — | is an hsl()/hsla() color |

import { isHexColor, isRgbColor, isHSL } from '@archi-code/validation';

isHexColor('ff0000', { require_hashtag: true }); // false
isRgbColor('rgb(255, 0, 0)'); // false
isRgbColor('rgb(255, 0, 0)', { allowSpaces: true }); // true
isHSL('hsl(120, 100%, 50%)'); // true

Finance and payments

| Signature | Options (default value) | Checks that the value… | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | isIBAN(value, options?) | whitelist, blacklist (country codes, see ibanLocales) | is an IBAN (format and check digits) | | isBIC(value) | — | is a BIC/SWIFT code | | isAbaRouting(value) | — | is an ABA routing number (US) | | isCreditCard(value, options?) | provider: amex, dinersclub, discover, jcb, mastercard, unionpay, visa (when not given, any) | is a card number | | isLuhnNumber(value) | — | passes the Luhn algorithm | | isCurrency(value, options?) | symbol ('$'), require_symbol (false), allow_space_after_symbol (false), symbol_after_digits (false), allow_negatives (true), parens_for_negatives (false), negative_sign_before_digits (false), negative_sign_after_digits (false), allow_negative_sign_placeholder (false), thousands_separator (','), decimal_separator ('.'), allow_decimal (true), require_decimal (false), digits_after_decimal ([2]), allow_space_after_digits (false) | is a monetary amount | | isVAT(value, options) | countryCode (e.g. 'ES', 'GB') | has the VAT number format of that country | | isISIN(value) | — | is an ISIN | | isISO4217(value) | — | is an ISO 4217 currency code | | isEthereumAddress(value) | — | is an Ethereum address | | isBtcAddress(value) | — | is a Bitcoin address |

import { isIBAN, isCreditCard, isCurrency, isVAT } from '@archi-code/validation';

isIBAN('DE89 3704 0044 0532 0130 00', { whitelist: ['ES'] }); // false
isCreditCard('4111111111111111'); // true
isCreditCard('4111111111111111', { provider: 'mastercard' }); // false
isCurrency('$1,000.00'); // true
isCurrency('1.000,00€', {
  symbol: '€',
  symbol_after_digits: true,
  thousands_separator: '.',
  decimal_separator: ',',
}); // true
isVAT('GB999 9999 00', { countryCode: 'GB' }); // true

People, countries and languages

| Signature | Options (default value) | Checks that the value… | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | isMobilePhone(value, options?) | locale (one, a list or 'any'; when not given, any; see isMobilePhoneLocales), strictMode (requires +) | is a mobile number of one of the locales | | isPostalCode(value, options) | locale (country code or 'any', see isPostalCodeLocales) | is a postal code of that country | | isIdentityCard(value, options?) | locale ('any') | is an identity document of that locale | | isPassportNumber(value, options) | countryCode (see passportNumberLocales) | has the passport format of that country | | isLicensePlate(value, options) | locale (e.g. 'de-DE', 'pt-BR', or 'any') | is a license plate of that locale | | isLatLong(value, options?) | checkDMS (false: degrees, minutes and seconds) | is a latitude,longitude pair | | isLocale(value) | — | is a locale identifier | | isISO6391(value) | — | is an ISO 639-1 language code | | isISO15924(value) | — | is an ISO 15924 script code | | isISO31661Alpha2(value, options?) | userAssignedCodes (extra accepted codes) | is an ISO 3166-1 alpha-2 country code | | isISO31661Alpha3(value, options?) | userAssignedCodes | is an ISO 3166-1 alpha-3 country code | | isISO31661Numeric(value) | — | is an ISO 3166-1 numeric country code |

import {
  isMobilePhone,
  isPostalCode,
  isIdentityCard,
  isLicensePlate,
  isISO31661Alpha2,
} from '@archi-code/validation';

isMobilePhone('612345678', { locale: 'es-ES', strictMode: true }); // false
isMobilePhone('612345678', { locale: ['es-ES', 'pt-PT'] }); // true
isMobilePhone('600', { locale: 'xx-XX' }); // throws ValidationConfigError: Invalid locale 'xx-XX'
isPostalCode('21001', { locale: 'ES' }); // true
isIdentityCard('12345678Z', { locale: 'ES' }); // true
isLicensePlate('B-AB 1234', { locale: 'de-DE' }); // true
isISO31661Alpha2('XK'); // false
isISO31661Alpha2('XK', { userAssignedCodes: ['XK'] }); // true

Products, publications and devices

| Signature | Options (default value) | Checks that the value… | | ----------------------------- | ------------------------------------------------ | ----------------------------- | | isISBN(value, options?) | version (10 or 13; when not given, either) | is an ISBN | | isISSN(value, options?) | case_sensitive, require_hyphen | is an ISSN | | isEAN(value) | — | is an EAN code | | isISRC(value) | — | is an ISRC | | isIMEI(value, options?) | allow_hyphens | is an IMEI | | isISO6346(value) | — | is an ISO 6346 container code | | isFreightContainerID(value) | — | alias of isISO6346 |

import { isISBN, isIMEI, isISO6346 } from '@archi-code/validation';

isISBN('978-3-16-148410-0'); // true
isISBN('0-306-40615-2', { version: 10 }); // true
isIMEI('49-015420-323751-8', { allow_hyphens: true }); // true
isISO6346('CSQU3054383'); // true

Passwords

| Signature | Options (default value) | Checks that the value… | | ----------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------- | | isStrongPassword(value, options?) | minLength (8), minLowercase (1), minUppercase (1), minNumbers (1), minSymbols (1) | meets every minimum |

See Passwords.

Locale lists

They are frozen arrays (readonly string[]) with the keys each validator accepts. They are also in the validator registry.

| Export | Locales / countries of | | ----------------------- | ---------------------------------------- | | isAlphaLocales | isAlpha | | isAlphanumericLocales | isAlphanumeric | | isFloatLocales | isFloat (and isDecimal, isNumeric) | | ibanLocales | isIBAN | | isMobilePhoneLocales | isMobilePhone | | isPostalCodeLocales | isPostalCode | | passportNumberLocales | isPassportNumber |

import { isAlphaLocales, isMobilePhoneLocales } from '@archi-code/validation';

isAlphaLocales.includes('es-ES'); // true
isMobilePhoneLocales.includes('es-ES'); // true

validator registry

validator is a frozen object with the 91 validators and the 7 locale lists. Its type is ValidatorRegistry. Use it to access validators by name:

import { validator } from '@archi-code/validation';

validator.isInt('5'); // true

to* converters

They convert a value to a type and never throw. They return Converted<T>:

type Converted<T> =
  | { readonly ok: true; readonly value: T; readonly error: null }
  | { readonly ok: false; readonly value: null; readonly error: ConvertMessage };

Checking ok narrows the type: inside if (r.ok), value is T.

| Signature | Default rule | Options | | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | toString(v): Converted<string> | Strings, booleans and finite numbers | — | | toBoolean(v, options?): Converted<boolean> | true/false, 1/0 and the strings 'true'/'false'/'1'/'0', trimmed and case-insensitive | mode: 'default', 'strict' (exact strings, no trimming or lowercasing) or 'loose' (case-insensitive, no trimming, adds 'yes'/'no') | | toInteger(v, options?): Converted<number> | Safe integers, as a number or as a trimmed -?\d+ string; outside ±2^53 it fails with INTEGER_OVERFLOW | syntax: 'validator' (the isInt rule: allows +), allowLeadingZeroes (with syntax: 'validator'; false rejects leading zeros) | | toFloat(v, options?): Converted<number> | Finite numbers or trimmed strings in decimal notation (not 0x/0b/0o) | syntax: 'validator' (the isFloat rule), decimalSeparator (with syntax: 'validator'; '.' by default) | | toDate(v, options?): Converted<Date> | YYYY/MM/DD with / or - as separator (result: that day at 00:00 UTC) or a valid Date | format, delimiters, strictMode, twoDigitYearPivot (as in isDate); iso: true (ISO date or date-time, optional zone); lax: true (reduced-precision ISO and the output of Date#toString()/toUTCString()) | | toJson(v): Converted<Record<string, unknown>> | A non-empty plain JSON object: JSON text, or an object that survives a JSON round trip (plain objects/arrays with strings, booleans, finite numbers or null) | — | | toJsonValue(v, options?): Converted<unknown> | JSON text that parses to an object or array | allowPrimitives (also true, false, null), allowAnyValue (any JSON) | | toArray(v): Converted<unknown[]> | Arrays, or JSON text that contains an array | — | | toEnum(v, options): Converted<T> | A string, number or boolean whose text form is in options (readonly T[]) | — |

import {
  toBoolean,
  toInteger,
  toFloat,
  toDate,
  toJson,
  toJsonValue,
  toArray,
  toEnum,
} from '@archi-code/validation';

toBoolean(' TRUE '); // { ok: true, value: true, error: null }
toBoolean('yes'); // { ok: false, value: null, error: 'Value cannot be converted to boolean' }
toBoolean('No', { mode: 'loose' }); // { ok: true, value: false, error: null }
toInteger('4.2'); // { ok: false, value: null, error: 'Value is not an integer' }
toInteger('+5', { syntax: 'validator' }); // { ok: true, value: 5, error: null }
toInteger('9007199254740993'); // { ok: false, value: null, error: 'Integer exceeds the safe integer limits (-9007199254740991 to 9007199254740991)' }
toFloat('0x10'); // { ok: false, value: null, error: 'Value is not a finite decimal number' }
toFloat('3,14', { syntax: 'validator', decimalSeparator: ',' }); // { ok: true, value: 3.14, error: null }
toDate('31/01/2024', { format: 'DD/MM/YYYY' }); // { ok: true, value: 2024-01-31T00:00:00.000Z, error: null }
toDate('2024-01-31T10:00:00+02:00', { iso: true }); // { ok: true, value: 2024-01-31T08:00:00.000Z, error: null }
toDate('2024-03', { lax: true }); // { ok: true, value: 2024-03-01T00:00:00.000Z, error: null }
toJson('{"a":1}'); // { ok: true, value: { a: 1 }, error: null }
toJson({ a: new Date(0) }); // { ok: false, value: null, error: 'Value is not a non-empty JSON object' }
toJsonValue('true', { allowPrimitives: true }); // { ok: true, value: true, error: null }
toArray('[1,2]'); // { ok: true, value: [ 1, 2 ], error: null }
toEnum(1, ['1', '2']); // { ok: true, value: '1', error: null }

With TypeScript, ok decides the type of value:

import { toInteger, toEnum } from '@archi-code/validation';

const quantity = toInteger(input);
if (quantity.ok) {
  quantity.value; // number
} else {
  quantity.error; // ConvertMessage
}

const role = toEnum(input, ['admin', 'user'] as const); // Converted<'admin' | 'user'>

ISO dates with a time need the ISO rule: canBeDate('2024-01-31T10:00:00Z') is false, while toDate('2024-01-31T10:00:00Z', { iso: true }) is ok.

ConvertMessages

Frozen object with the only possible error messages. Its value type is ConvertMessage.

| Key | Message | | ------------------ | --------------------------------------------------------------------------------- | | STRING | Value cannot be converted to string | | BOOLEAN | Value cannot be converted to boolean | | INTEGER | Value is not an integer | | INTEGER_OVERFLOW | Integer exceeds the safe integer limits (-9007199254740991 to 9007199254740991) | | FLOAT | Value is not a finite decimal number | | DATE | Value is not a valid date | | JSON | Value is not a non-empty JSON object | | JSON_VALUE | Value is not an accepted JSON value | | ARRAY | Value is not an array | | ENUM | Value is not one of the enum options |

import { toInteger, ConvertMessages } from '@archi-code/validation';

toInteger('x').error === ConvertMessages.INTEGER; // true

anyToString

anyToString(value: any): string describes any value in a readable way, for logs and error messages. It never throws.

import { anyToString } from '@archi-code/validation';

anyToString(null); // 'null'
anyToString({ a: 1 }); // '{"a":1}'
anyToString(new Date('2024-01-01T00:00:00Z')); // 'Date(2024-01-01T00:00:00.000Z)'
anyToString(new Map([['a', 1]])); // 'Map({a: 1})'
anyToString(new Error('boom')); // 'new Error(boom)'
anyToString(function foo() {}); // 'Function(foo)'

canBe* checks

They return boolean and are always equivalent to toX(value).ok with the default options.

| Signature | Equivalent to | | --------------------------------------------------- | ----------------------- | | canBeString(v): boolean | toString(v).ok | | canBeBoolean(v): boolean | toBoolean(v).ok | | canBeInteger(v): boolean | toInteger(v).ok | | canBeFloat(v): boolean | toFloat(v).ok | | canBeDate(v): boolean | toDate(v).ok | | canBeJson(v): boolean | toJson(v).ok | | canBeJsonObject(v): boolean | alias of canBeJson | | canBeArray(v): boolean | toArray(v).ok | | canBeEnum(v, options: readonly string[]): boolean | toEnum(v, options).ok |

import { canBeInteger, canBeBoolean, canBeJson, canBeEnum } from '@archi-code/validation';

canBeInteger('42'); // true
canBeInteger('4.2'); // false
canBeBoolean('yes'); // false
canBeJson('[]'); // false
canBeEnum('z', ['a', 'b']); // false

Sanitizers

They are grouped in the sanitizer namespace. Unlike the validators, they take a string and throw TypeError (Expected a string but received a …) when the first argument is not one.

| Signature | What it does | | ------------------------------------------------------------ | --------------------------------------------------------------------------- | | sanitizer.trim(str, chars?): string | Removes whitespace (or the characters in chars) at the start and the end | | sanitizer.ltrim(str, chars?): string | Same, only at the start | | sanitizer.rtrim(str, chars?): string | Same, only at the end | | sanitizer.escape(str): string | Escapes &, <, >, ", ', / and other characters as HTML entities | | sanitizer.unescape(str): string | Reverses escape | | sanitizer.blacklist(str, chars): string | Removes the characters in chars (literal, no ranges) | | sanitizer.whitelist(str, chars): string | Keeps only the characters in chars (literal, no ranges) | | sanitizer.stripLow(str, keepNewLines?): string | Removes ASCII control characters (with keepNewLines, keeps \n and \r) | | sanitizer.normalizeEmail(email, options?): string \| false | Normalizes an email; returns false when it has no @ |

normalizeEmail options (NormalizeEmailOptions), all true by default: all_lowercase, gmail_lowercase, gmail_remove_dots, gmail_remove_subaddress, gmail_convert_googlemaildotcom, outlookdotcom_lowercase, outlookdotcom_remove_subaddress, yahoo_lowercase, yahoo_remove_subaddress, yandex_lowercase, yandex_convert_yandexru, icloud_lowercase, icloud_remove_subaddress.

import { sanitizer } from '@archi-code/validation';

sanitizer.trim('xxhelloxx', 'x'); // 'hello'
sanitizer.escape('<b>"Hello"</b>'); // '&lt;b&gt;&quot;Hello&quot;&lt;&#x2F;b&gt;'
sanitizer.unescape('&lt;b&gt;'); // '<b>'
sanitizer.whitelist('abc123', 'abc'); // 'abc'
sanitizer.blacklist('abc123', '123'); // 'abc'
sanitizer.stripLow('a\x00b\nc', true); // 'ab\nc'
sanitizer.normalizeEmail('[email protected]'); // '[email protected]'
sanitizer.normalizeEmail('[email protected]', { gmail_remove_dots: false }); // '[email protected]'
sanitizer.normalizeEmail('not-an-email'); // false
sanitizer.trim(42); // throws TypeError: Expected a string but received a number

Passwords: isStrongPassword and scorePassword

isStrongPassword(value, options?) returns boolean: true when the password meets every minimum (minLength 8, minLowercase 1, minUppercase 1, minNumbers 1, minSymbols 1).

scorePassword(value, options?): number returns a numeric score (it is not a validator). It adds pointsPerUnique for each distinct character, pointsPerRepeat for each repeated one and a bonus for containing lowercase letters, uppercase letters, numbers and symbols. A value that cannot be turned into a string scores 0.

| ScorePasswordOptions option | Default | | ----------------------------- | ------- | | pointsPerUnique | 1 | | pointsPerRepeat | 0.5 | | pointsForContainingLower | 10 | | pointsForContainingUpper | 10 | | pointsForContainingNumber | 10 | | pointsForContainingSymbol | 10 |

import { isStrongPassword, scorePassword } from '@archi-code/validation';

isStrongPassword('Abcdef1!'); // true
isStrongPassword('abcdefgh', { minUppercase: 0, minNumbers: 0, minSymbols: 0 }); // true
scorePassword('Abcdef1!'); // 48
scorePassword('aaaa'); // 12.5

@Validations decorator and runValidations

@Validations(list) attaches a list of validations to a class. runValidations(Class, value) runs them (inherited ones included) against a value and returns a ValidationResult.

import { Validations, runValidations } from '@archi-code/validation';

@Validations([{ validator: 'isInt', properties: { min: 0 } }])
class Quantity {}

runValidations(Quantity, '-3');
// { ok: false, value: '-3', errors: [{ validator: 'isInt', message: 'Value does not satisfy isInt' }] }
runValidations(Quantity, '7');
// { ok: true, value: '7', errors: [] }

The decorator only stores metadata: it does not change the class, does not wrap its constructor and validates nothing by itself. Validation only happens when runValidations is called.

TypeScript configuration

It works with both decorator modes:

  • Legacy decorators ("experimentalDecorators": true).
  • Standard decorators (TypeScript 5+ without experimentalDecorators).

It needs neither emitDecoratorMetadata nor reflect-metadata.

Declaring validations

The decorator takes an array. Each element is one of these two kinds:

Built-in validation (BuiltInValidation): runs one of the package's validators. validator is its name and properties is its options object, passed as is as the second argument: { validator: 'isVAT', properties: { countryCode: 'ES' } } runs isVAT(value, { countryCode: 'ES' }). The type of properties comes from the validator's signature:

| Validator signature | properties | | ---------------------------------------------------- | ------------ | | With required options (isHash, isIn, matches…) | Required | | With only optional options (isInt, isEmail…) | Optional | | Takes only the value (isPort, isSlug…) | Not allowed |

Custom validation (CustomValidation): custom is its name (it appears in the errors) and fn: (value: unknown) => boolean decides; a truthy result passes and a falsy one fails.

Both accept message. Without it, the message is Value does not satisfy <name>.

import { Validations, runValidations } from '@archi-code/validation';

@Validations([
  { validator: 'isHash', properties: { algorithm: 'sha256' } },
  { validator: 'isInt', properties: { min: 1, max: 10 } },
  { validator: 'isPort' },
  { custom: 'isEven', fn: (v) => Number(v) % 2 === 0, message: 'Must be even' },
])
class Example {}

runValidations(Example, '5');
// { ok: false, value: '5', errors: [
//   { validator: 'isHash', message: 'Value does not satisfy isHash' },
//   { custom: 'isEven', message: 'Must be even' } ] }

Identity. Two validations are "the same" when they have the same kind and name: validator: 'isInt' is one identity and custom: 'isInt' is another; a built-in and a custom validation with the same name never replace each other. properties, fn and message do not count towards identity.

Missing values and isRequired

  • With a value (neither null nor undefined), every validation runs, whether isRequired was declared or not.
  • Without a value (null or undefined), only isRequired decides: if the class or any parent declares { validator: 'isRequired' }, only that one runs and it fails; otherwise, the value is optional, nothing runs and the result is ok.
import { Validations, runValidations } from '@archi-code/validation';

@Validations([{ validator: 'isRequired' }, { validator: 'isEmail', message: 'Invalid email' }])
class Email {}

runValidations(Email, undefined);
// { ok: false, value: undefined, errors: [{ validator: 'isRequired', message: 'Value does not satisfy isRequired' }] }
runValidations(Email, 'x');
// { ok: false, value: 'x', errors: [{ validator: 'isEmail', message: 'Invalid email' }] }

@Validations([{ validator: 'isInt', properties: { min: 0 } }])
class OptionalQuantity {}

runValidations(OptionalQuantity, null);
// { ok: true, value: null, errors: [] }

Inheritance and replacement

runValidations(Child, value) walks the chain from the root parent down to Child and combines each class's own validations:

  1. The root parent's come first, then those of each subclass, in declaration order.
  2. A subclass validation with the same identity as an inherited one replaces it and keeps its position.
  3. An intermediate class without @Validations passes its parent's on unchanged.
  4. Declaring a subclass never changes the parent.
import { Validations, runValidations } from '@archi-code/validation';

@Validations([{ validator: 'isInt', properties: { min: 0 } }, { validator: 'isEmail' }])
class Parent {}

@Validations([{ validator: 'isInt', properties: { min: 5 } }, { validator: 'isPort' }])
class Child extends Parent {}

runValidations(Child, 'x');
// runs, in order: isInt { min: 5 } (replaces and keeps first place), isEmail, isPort

Applying @Validations again to the same class follows the same rules. Several decorators on one class are applied from bottom to top, as always in TypeScript:

@Validations([{ validator: 'isEmail' }]) // applied second
@Validations([{ validator: 'isInt' }, { validator: 'isPort' }]) // applied first
class Stacked {}
// runValidations(Stacked, value) runs: isInt, isPort, isEmail

When each part runs

| Moment | What happens | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | The class is defined (usually, when its module is imported) | The decorator runs once: it checks the declaration, freezes a copy and stores it. A wrong declaration throws here and prevents the module from loading | | new MyClass() | Nothing. The decorator adds no code to the constructor | | runValidations(MyClass, value) | Walks the prototype chain, combines the lists and runs each validation in order. It is recomputed on every call; there is no cache |

Declaration checks

The decorator checks each element when it decorates the class. On the first error it throws ValidationConfigError and stores nothing for that class.

| Declaration