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

@kreiseck/validator

v0.10.0

Published

Validate, normalize and format email, phone, URL, host, IBAN, credit-card, license-plate, IMEI, ICCID, MAC, VIN and postal-code input. Zero dependencies, DACH-aware. By Kreiseck.

Readme

@kreiseck/validator

Validate, normalize and format the input every app collects — email, phone, URL, host, IBAN, credit-card, license plate, IMEI, ICCID, MAC address, VIN and postal code — in a few lines of TypeScript. Zero dependencies, DACH-aware.

This is the TypeScript/JavaScript port of the kreiseck_validator Dart package. Every type follows the same four-operation API — isValid, validate, normalize, format — so once you learn one, you know them all. Behavior is kept in lockstep with the Dart package via a shared, language-independent set of test vectors.

Install

npm i @kreiseck/validator

Works in Node, browser and edge runtimes — zero runtime dependencies, ships as ESM + CommonJS with full TypeScript types.

Input fields

Every validator also exposes fieldDescriptor(...) and formatPartial(...), taking the same options object as its validate. fieldDescriptor describes the input field a value needs — which keyboard to raise, whether an autofill hint applies, how to case typed text, how long the formatted value can get, an example value, and which characters are allowed — as a platform-neutral FieldDescriptor. The package has no DOM and no framework dependency: the mapping tables below are what you write in your own component.

const d = PostalCode.fieldDescriptor({ country: 'NL' });

input.inputMode = d.keyboard === 'digits' ? 'numeric' : 'text';
input.autocomplete = d.autofill === 'postalCode' ? 'postal-code' : 'off';
input.maxLength = d.maxLength ?? 524288;
input.placeholder = d.example ?? '';
input.addEventListener('input', () => {
  input.value = PostalCode.formatPartial(input.value, { country: 'NL' });
});

| KeyboardType | Flutter | HTML | | --- | --- | --- | | text | TextInputType.text | inputmode="text" | | digits | TextInputType.number | inputmode="numeric" | | phone | TextInputType.phone | inputmode="tel" | | email | TextInputType.emailAddress | inputmode="email" | | url | TextInputType.url | inputmode="url" |

| Capitalization | Flutter | HTML | | --- | --- | --- | | none | TextCapitalization.none | autocapitalize="off" | | characters | TextCapitalization.characters | autocapitalize="characters" | | words | TextCapitalization.words | autocapitalize="words" | | sentences | TextCapitalization.sentences | autocapitalize="sentences" |

| AutofillHint | Flutter | HTML | | --- | --- | --- | | email | AutofillHints.email | autocomplete="email" | | telephoneNumber | AutofillHints.telephoneNumber | autocomplete="tel" | | postalCode | AutofillHints.postalCode | autocomplete="postal-code" | | creditCardNumber | AutofillHints.creditCardNumber | autocomplete="cc-number" | | url | AutofillHints.url | autocomplete="url" |

IBAN, VIN, IMEI, ICCID, MAC address and license plate have no standard autofill category on either platform, so their autofill is null — nothing is invented.

formatPartial never throws: it accepts empty, half-typed or garbage input and always returns a string safe to put straight back into the field. For the nine grouping types — Iban, CreditCard, MacAddress, PostalCode, Phone, LicensePlate, Imei, Iccid, Vin — it produces exactly what format produces once the value is valid; it is the same grouping, just also defined on incomplete input. Email, Url and Host are the exception: formatPartial returns their text essentially untouched, because Url.format is a display transform that strips the scheme and www., Host.format canonicalises and lower-cases, and Email has no format at all — applying any of those while the user is still typing would delete what they just typed.

A few FieldDescriptor fields are honestly absent rather than filled with a plausible guess: PostalCode.fieldDescriptor({ country }).example is null for the 28 of 51 countries with no verified real postal code on hand (maxLength, unlike example, is still mechanically derived for all 51). Phone.fieldDescriptor(...).example is null for every country, always — there is no bundled table of a single verified real phone number per country, only synthetic ones, and an invented example is worse than none. LicensePlate.fieldDescriptor(...).maxLength is null for every country by design — the implemented Austrian grammar has an unbounded serial, so any cap could reject a plate validate accepts.

Quick start

Email

import { Email } from '@kreiseck/validator';

Email.isValid('[email protected]');          // true
Email.normalize(' [email protected] ');      // '[email protected]'

const result = Email.validate('[email protected]');
if (result.ok) {
  result.normalized;               // '[email protected]'
  result.suggestions[0]?.value;    // '[email protected]'  (offline typo hint)
} else {
  result.issues[0].code;           // e.g. 'emailMissingAt'
}

Phone

import { Phone } from '@kreiseck/validator';

Phone.isValid('+43 660 1234567');                        // true
Phone.normalize('0660 1234567', { country: 'AT' });      // '+436601234567'
Phone.format('06601234567', { country: 'AT' });          // '+43 660 1234567'
Phone.format('+436601234567', { international: false }); // '0660 1234567'

National input (no +, no country code) requires the country option; without it, validate returns an issue with code phoneAmbiguousCountry. Validation and national/international formatting cover every country, driven by libphonenumber-derived metadata (see the root NOTICE). Phone.type/Phone.parse additionally classify Austrian numbers into mobile/landline/voip/etc. — that classification is Austria-only; every other country resolves to 'unknown'.

URL

import { Url } from '@kreiseck/validator';

Url.isValid('example.com');               // true
Url.isValid('example.com:8080');          // true
Url.normalize('Example.com/path/');       // 'https://example.com/path'
Url.format('https://www.example.com/');   // 'example.com'

Host

import { Host } from '@kreiseck/validator/host';

Host.isValid('example.com:8080');            // true
Host.isValid('[2001:db8::1]:443');           // true

const h = Host.parse('[::1]:8080')!;
h.type; // 'ipv6'
h.port; // 8080

Host classifies a bare host (no scheme) as a hostname, IPv4 or IPv6 address, trying IPv4 then IPv6 then hostname in that order. A port is only recognised for IPv6 in the bracketed form ([::1]:8080) — a bare ::1 parses as IPv6 with no port, since a plain trailing :port would be ambiguous with the address's own colons.

IBAN

import { Iban, IbanCountry } from '@kreiseck/validator';

Iban.isValid('AT61 1904 3002 3457 3201');    // true
Iban.normalize('at611904300234573201');      // 'AT611904300234573201'
Iban.format('AT611904300234573201');         // 'AT61 1904 3002 3457 3201'

const info = Iban.parse('AT72 1200 0002 3457 3201')!;
info.bankCode;  // '12000'
info.bankName;  // 'UniCredit Bank Austria AG'
info.bic;       // 'BKAUATWW'

const at = IbanCountry.of('AT')!;
at.length;          // 20
at.bankCodeLength;  // 5
at.hasBranchCode;   // false
at.example;         // 'AT61 1904 3002 3457 3201'

Iban.parse's structural fields (bankCode, branchCode, accountNumber) are filled for every country with a known BBAN layout; bankName/bic lookup is bundled for Austria, Germany and Switzerland.

Credit card

import { CreditCard } from '@kreiseck/validator';

CreditCard.isValid('4111 1111 1111 1111');   // true
CreditCard.normalize('4111-1111-1111-1111'); // '4111111111111111'
CreditCard.format('378282246310005');        // '3782 822463 10005'  (Amex 4-6-5)
CreditCard.network('4111111111111111');      // 'visa'

License plate

import { LicensePlate } from '@kreiseck/validator/license-plate';

LicensePlate.isValid('W-12345A', { country: 'AT' });  // true
LicensePlate.format('m ab1234', { country: 'DE' });   // 'M-AB 1234'

const info = LicensePlate.parse('W-12345A', { country: 'AT' })!;
info.districtCode; // 'W'
info.region;       // 'Wien'
info.type;         // 'standard'

Covers Austria, Germany, Switzerland, Croatia and Turkey (country: 'AT' | 'DE' | 'CH' | 'HR' | 'TR'). Plates have no checksum, so validate checks a per-country grammar plus a curated code → region table; parse's region is null when the code is structurally valid but not in the table (AT/DE only — CH/HR/TR require a known code). type classifies special-purpose plates (diplomatic, authority, military, historic, seasonal, electric, …) on a best-effort basis and defaults to 'standard' when a country's special forms aren't (yet) identifiable from the plate text alone.

IMEI

import { Imei } from '@kreiseck/validator/imei';

Imei.isValid('353880080078742');   // true (passes the Luhn checksum)

const info = Imei.parse('353880080078742')!;
info.tac;          // '35388008'
info.serialNumber; // '007874'
info.checkDigit;   // '2'

Passing { allowSv: true } additionally accepts a 16-digit IMEISV (the IMEI plus a 2-digit software version, no Luhn check) on every operation:

Imei.parse('3538800800787456', { allowSv: true })!.softwareVersion; // '56'

For a 16-digit IMEISV, checkDigit is null (IMEISV has no check digit); for a 15-digit IMEI, softwareVersion is null.

ICCID

import { Iccid } from '@kreiseck/validator/iccid';

Iccid.isValid('8949012345678901234'); // true

const info = Iccid.parse('8949012345678901234')!;
info.mii;            // '89'
info.country?.iso2;  // 'DE' (resolved from the embedded E.164 code)

MAC address

import { MacAddress } from '@kreiseck/validator/mac-address';

MacAddress.isValid('00:1A:2B:3C:4D:5E');                       // true
MacAddress.normalize('00-1A-2B-3C-4D-5E');                     // '00:1a:2b:3c:4d:5e'
MacAddress.format('00:1A:2B:3C:4D:5E', { notation: 'hyphen' }); // '00-1a-2b-3c-4d-5e'

const info = MacAddress.parse('00:1A:2B:3C:4D:5E')!;
info.oui;        // '00:1a:2b'
info.isUnicast;  // true

VIN

import { Vin } from '@kreiseck/validator/vin';

Vin.isValid('1HGCM82633A004352');          // true (structurally valid)
Vin.parse('1HGCM82633A004352')!.modelYear; // 2003

const info = Vin.parse('1HGCM82633A004352')!;
info.wmi;             // '1HG'
info.checkDigitValid; // true

Vin.validate checks structure only (17 chars from the ISO 3779 charset); the check digit is mandatory only for North American VINs, so its result is exposed via parse's checkDigitValid instead of blocking validation.

Postal code

import { PostalCode } from '@kreiseck/validator/postal-code';

PostalCode.isValid('1234 AB', { country: 'NL' }); // true
PostalCode.format('1234ab', { country: 'NL' });   // '1234 AB'
PostalCode.format('00950', { country: 'PL' });    // '00-950'
PostalCode.format('sw1a1aa', { country: 'GB' });  // 'SW1A 1AA'

country is required (ISO2) — a bare postal code is ambiguous across countries. Covers Europe + Turkey (51 countries) from a curated per-country pattern table; an unlisted country yields the postalUnknownCountry issue code.

All format/normalize calls throw FormatError on invalid input, with one exception: Email.normalize doesn't validate at all — it's a pure trim + lower-case transform, so it never throws. Use tryFormat for a null-returning variant instead of a try/catch on the types that do throw.

The result model

validate returns a discriminated union:

type ValidationResult =
  | { ok: true; normalized: string; suggestions: Suggestion[] }
  | { ok: false; issues: ValidationIssue[] };

isValid(x) is shorthand for validate(x).ok. Error codes (IssueCode) are a stable, translatable string union; the English message is only a default.

Tree-shaking: import from a subpath

The root import (from '@kreiseck/validator') pulls in every module. If you only need one, import its subpath instead so bundlers exclude the rest — e.g. an IBAN-only build won't inline phone metadata:

import { Iban } from '@kreiseck/validator/iban';
import { Phone } from '@kreiseck/validator/phone';
import { Email } from '@kreiseck/validator/email';
import { Url } from '@kreiseck/validator/url';
import { Host } from '@kreiseck/validator/host';
import { CreditCard } from '@kreiseck/validator/credit-card';
import { LicensePlate } from '@kreiseck/validator/license-plate';
import { Imei } from '@kreiseck/validator/imei';
import { Iccid } from '@kreiseck/validator/iccid';
import { MacAddress } from '@kreiseck/validator/mac-address';
import { Vin } from '@kreiseck/validator/vin';
import { PostalCode } from '@kreiseck/validator/postal-code';

Each subpath ships its own ESM, CommonJS and .d.ts build.

How behavior is pinned across languages

The exact expected result of every operation, for representative inputs, is captured once as data in the shared, language-independent JSON files under test/vectors/ at the repo root. This package's conformance tests run against the very same vectors as the Dart package, so the two implementations cannot quietly drift apart.

License

Apache-2.0 — see LICENSE.