A comprehensive TypeScript/JavaScript library for validating and parsing national identification numbers from 85 countries across 6 continents.
API reference: every public function with a typed example, and a page for each of the 85 countries.
v2.3.0 is a minor release of Python-parity fixes with no API additions or removals. See the CHANGELOG for every change.
RG check
character X counts as 10, which changes which RG numbers validate; RG is a secondary ID type
exported from the BRA module, so validateNationalId('BRA', …) is unaffected
(#300).normalizeId() and formatId() also remove U+001C-U+001F and U+0085, and uppercase only ASCII
letters (#301).parse() no longer sets citizenship for old
NICs (#296), and South Africa (ZAF) and
Ecuador (ECU) checksum() return null for malformed input
(#299).main
(#302); see
docs/PARITY.md.v2.2.0 is a minor release with no API changes. See the CHANGELOG for every change.
parity/allowlist.json
(#133); see
docs/PARITY.md.npm run bench, plus enforced coverage thresholds with a coverage
badge (#137).METADATA.regexp of ESP, FIN, ITA, SGP and VEN now matches
lowercase input (#242).v2.1.1 is a patch release with Python-parity fixes and no API changes. See the CHANGELOG for every change.
invalid_birthdate (#214).v2.1.0 is a minor release with no breaking changes. See the CHANGELOG for every change.
formatId(countryCode, idNumber) / normalizeId(countryCode, idNumber): write an ID in
its country's display format, or return its compact form
(#128); see
formatId.getInputMask(countryCode) and IdMetadata.masks: input masks for form fields, with a
RegExp whose pattern.source also works as an HTML <input pattern> attribute
(#129,
#202); see
getInputMask.invalid_birthdate failure reason: validateNationalId() and parseIdInfo() report it when
a country's validator rejects an ID because the birth date it encodes isn't real
(#130); see
docs/FAILURE_REASONS.md.v2.0.0 is a major release with breaking changes (epic #127). See MIGRATION.md for how to upgrade from 1.x, and the CHANGELOG for every change.
package.json exports map
(#120); see Installation.idnumbers/core plus one idnumbers/countries/<iso3> subpath
per country, so a bundle carries only the countries you register (#122); see
Tree-shakeable imports. The root idnumbers import stays
batteries-included.parseIdInfo() returns { ok: true, info } or { ok: false, reason },
typed per country, instead of any | null (#123); see
parseIdInfo.IdMetadata fields
(#121).SUPPORTED_COUNTRIES and IMetadata, deprecated in v1.11.0 (#124).Which IDs validate does not change: v2.0.0 keeps parity with the Python idnumbers library.
Requirements: Node.js >= 22. The 1.x releases support Node.js >= 16.
npm install idnumbers
yarn add idnumbers
pnpm add idnumbers
From v2.0.0, the package ships both an ESM and a CommonJS build behind a package.json exports
map, so either form works without any extra configuration:
// ESM
import { validateNationalId } from 'idnumbers';
// CommonJS
const { validateNationalId } = require('idnumbers');
From v2.0.0, only the documented entry points are public — idnumbers, idnumbers/core,
idnumbers/countries/<iso3> (see Tree-shakeable imports), and
idnumbers/package.json. Deep imports such as idnumbers/dist/... are not part of the API and will
not resolve.
Avoid mixing require('idnumbers') and import 'idnumbers' for the same package within one process: Node
treats them as two separate module instances with two separate registries (the "dual-package
hazard"), so pick one style per process.
From v2.0.0, bundle only the countries you use: import the
validation API from idnumbers/core, which registers no countries, and register the ones
you need from idnumbers/countries/<iso3> (lowercase alpha-3 code):
import { register, validateNationalId } from 'idnumbers/core';
import { country as twn } from 'idnumbers/countries/twn';
import { country as usa } from 'idnumbers/countries/usa';
register(twn, usa);
validateNationalId('TWN', 'A123456789'); // registered: validated as usual
validateNationalId('JPN', '123456789012'); // not registered: reason 'unsupported_country'
Core plus one country is about 1.9–5.4 KB minified and gzipped, versus about 38 KB for
the batteries-included idnumbers root, which stays unchanged and registers all 85
countries. Importing the root anywhere in an app registers every country for the whole
app, because both entries share one registry.
Use the lowercase path exactly: on a case-insensitive file system (macOS, Windows),
idnumbers/countries/TWN loads a second copy of the module whose country definition
conflicts with the lowercase one, so registering both throws; on Linux it does not resolve.
import { validateNationalId, parseIdInfo } from 'idnumbers';
// Validate a US Social Security Number
const result = validateNationalId('USA', '123-45-6789');
console.log(result.isValid); // true or false
// Parse information from a South African ID
// (v2.0.0 shape, #123; 1.x returned the info itself, or null)
const parsed = parseIdInfo('ZAF', '8001015009087');
if (parsed.ok) {
console.log(parsed.info);
// {
// yyyymmdd: Date(1980-01-01),
// gender: 'male',
// citizenship: 'citizen',
// ...
// }
}
The API reference site has a typed example for every public function and a page per country. It is rebuilt and published with each release.
validateNationalId(countryCode, idNumber)Validates a national ID number for a specific country.
Parameters:
countryCode (string): ISO 3166-1 alpha-3 country code (e.g., 'USA', 'GBR', 'FRA'); the alpha-2 code (e.g. 'US', 'GB') is also accepted, case-insensitivelyidNumber (string): The ID number to validate. Which letter case, surrounding whitespace, and
separators are accepted varies by country: see docs/INPUT_FORMATS.md.Returns: ValidationResult
{
isValid: boolean;
countryCode: string;
idNumber: string;
extractedInfo?: ParsedInfo | null; // typed per country for a literal code (v2.0.0, #123)
errorMessage?: string;
reason?: ValidationFailureReason;
}
Example:
const result = validateNationalId('GBR', 'AB123456C');
if (result.isValid) {
console.log('Valid UK National Insurance Number');
} else {
console.log('Invalid:', result.reason);
}
When isValid is false, ValidationResult may carry a machine-readable reason
(a ValidationFailureReason enum member) describing why validation failed:
| Code | Meaning |
|---|---|
unsupported_country |
The country code doesn't resolve to a registered validator. |
invalid_length |
The ID's length doesn't fit the country's expected range. |
invalid_format |
The ID's length is plausible but it doesn't match the expected pattern. |
checksum_mismatch |
The ID matches the expected shape but fails a checksum digit. |
invalid_birthdate |
The ID matches the expected shape, but its encoded birth date isn't real (new in v2.1.0). |
validation_failed |
A generic fallback for any other failure (including thrown errors). |
not_parsable |
Only from parseIdInfo(): the ID is valid, but nothing can be parsed. |
reason is non-exhaustive: future minor releases may add new, more specific
codes, so always handle unknown values with a default branch:
switch (result.reason) {
case ValidationFailureReason.UNSUPPORTED_COUNTRY:
console.log('Unknown country code');
break;
case ValidationFailureReason.INVALID_LENGTH:
console.log('Wrong length');
break;
case ValidationFailureReason.INVALID_FORMAT:
console.log('Does not match the expected pattern');
break;
case ValidationFailureReason.CHECKSUM_MISMATCH:
console.log('Checksum digit is wrong');
break;
case ValidationFailureReason.INVALID_BIRTHDATE:
console.log('Encoded birth date is not a real date');
break;
case ValidationFailureReason.VALIDATION_FAILED:
default:
console.log('Validation failed for another reason');
break;
}
reason is also best-effort: checksum_mismatch is only reported for
validators whose checksum() reports a definite pass/fail (a boolean); validators
that expose a computed check digit instead, or none at all, fall back to
validation_failed. In 1.x, Bangladesh's registry metadata carried a match-anything
placeholder regexp (#160), so its
shape-related codes (invalid_length/invalid_format) never triggered there; v2.0.0 fixes the
metadata (#121).
invalid_birthdate (#130, new in
v2.1.0) is reported by each country whose validator checks the
birth date its IDs encode. See docs/FAILURE_REASONS.md for which
reasons each country can report.
parseIdInfo(countryCode, idNumber)Extracts information from a national ID number (if supported by the country).
Parameters:
countryCode (string): ISO 3166-1 alpha-3 country code; the alpha-2 code (e.g. 'US', 'GB') is also accepted, case-insensitivelyidNumber (string): The ID number to parseReturns: ParseIdInfoResult. Check ok, then read info or reason:
{ ok: true, countryCode, idNumber, info }: info holds the parsed fields, and
countryCode is the resolved alpha-3 code (e.g. 'SWE' for 'se').{ ok: false, countryCode, idNumber, reason, errorMessage? }: reason is a
ValidationFailureReason: unsupported_country, the reason
validateNationalId() reports for an invalid ID, or not_parsable for a valid ID the
country cannot parse.Changed in v2.0.0 (#123): 1.x returned the parsed info or
null, typedany. See MIGRATION.md.
info is typed per country. For a country code written as a literal ('SWE', 'se'),
TypeScript infers that country's parse result type from ParseResultMap; a country
without a parser is typed as always failing. For a string variable, info is a
ParsedInfo, whose fields read as unknown until narrowed.
The info varies by country but commonly includes:
yyyymmdd or birthDate: Date of birthgender: 'male' or 'female'citizenship: 'citizen' or 'resident'location or region: Geographic informationExample:
const result = parseIdInfo('SWE', '811218-9876');
if (result.ok) {
console.log(result.info.gender); // 'male', typed as Sweden's parse result
} else {
console.log(result.reason); // e.g. 'invalid_format'
}
validateMultipleIds(ids)Validates multiple ID numbers in batch.
Parameters:
ids (Array): Array of objects with countryCode and idNumberReturns: Array of ValidationResult
Example:
const results = validateMultipleIds([
{ countryCode: 'USA', idNumber: '123-45-6789' },
{ countryCode: 'GBR', idNumber: 'AB123456C' },
{ countryCode: 'FRA', idNumber: '255081416802538' },
]);
results.forEach(result => {
console.log(`${result.countryCode}: ${result.isValid}`);
});
listSupportedCountries()Returns a list of all supported countries, derived from the registry at call time and sorted by ISO 3166-1 alpha-3 code. Each call returns a fresh array.
Removed in v2.0.0 (#124): the
SUPPORTED_COUNTRIESconstant, a snapshot of this list that 1.x deprecated. UselistSupportedCountries()instead.
Returns: Array of country information
[
{
code: 'USA',
name: 'United States',
idType: 'Social Security Number'
},
...
]
Example:
const countries = listSupportedCountries();
console.log(`Supports ${countries.length} countries`);
getCountryIdFormat(countryCode)Gets format information for a country's ID number.
Parameters:
countryCode (string): ISO 3166-1 alpha-3 country code; the alpha-2 code (e.g. 'US', 'GB') is also accepted, case-insensitivelyReturns: Format information, or null for unsupported codes. Always includes countryCode, countryName, idType, length, hasChecksum, isParsable, and metadata (a copy of the registered METADATA, so changing it doesn't affect validation). Documented countries also include the optional fields format (a human-readable display mask), example (a valid sample ID), checksumAlgorithm (a description of the check-digit algorithm), and officialName (the local/official name of the ID).
Example:
const format = getCountryIdFormat('SWE');
console.log(format);
// {
// countryCode: 'SWE',
// countryName: 'Sweden',
// idType: 'Personal Identity Number',
// format: 'YYMMDD[+-]XXXC',
// example: '811218-9876',
// checksumAlgorithm: 'Luhn (mod 10)',
// officialName: 'Personnummer',
// length: { min: 10, max: 13 },
// hasChecksum: true,
// isParsable: true,
// ...
// }
formatId(countryCode, idNumber)New in v2.1.0 (#128).
Writes an ID in its country's display format. The input is normalized first (see
normalizeId), so compact, formatted, partly formatted, and
lowercase input all work.
Parameters:
countryCode (string): ISO 3166-1 alpha-3 or alpha-2 code, case-insensitive; the built-in codes
autocomplete in TypeScriptidNumber (string): The ID, in any of those formsReturns: The formatted ID, or null when it can't be laid out: an unsupported country, or a
compact length that none of the country's masks has. An ID written
without separators, like France's, comes back in its compact form. Formatting checks only the
length and doesn't validate, but when validateNationalId() accepts an ID in any form, it also
accepts formatId()'s output for it. The exception is Sweden's + form (people aged 100 or
over), for which formatId() returns null.
formatId('BRA', '39053344705'); // '390.533.447-05'
formatId('usa', ' 123 45 6789 '); // '123-45-6789'
formatId('HKG', 'a1234563'); // 'A123456(3)'
formatId('BRA', '123'); // null: no mask has 3 characters
formatId() follows each country's METADATA.masks, so it also works with idnumbers/core for
the countries you register.
normalizeId(countryCode, idNumber)New in v2.1.0 (#128).
Returns the compact form of an ID: ASCII letters uppercased (other letters, such as ß or a Greek
letter, are kept as written), with whitespace and the separators . - / ( ) removed. Characters
that belong to the ID stay, such as Finland's century sign in 131052-308T or Sweden's + for
people aged 100 or over. Returns null for an unsupported
country.
normalizeId('BRA', '111.444.777-35'); // '11144477735'
normalizeId('FIN', '131052-308t'); // '131052-308T'
validateNationalId() accepts the compact form for every country except USA and KOR, whose
validators require the separators: validate formatId()'s output there. See
docs/INPUT_FORMATS.md for what each validator accepts.
getInputMask(countryCode)New in v2.1.0 (#129).
Returns a country's input masks, one per length its ID comes in, ready for a form field. Returns
null for an unsupported country, or for a country registered through idnumbers/core without
masks; every built-in country has them.
Returns:
countryCode: the alpha-3 code the country code resolved tomasks: the masks in this library's vocabulary (below)imask: the same masks in imask's pattern syntax, as its
dynamic-mask listpattern: a RegExp matching an ID written in any of the masks, in uppercase (the form
formatId() returns), e.g. for react-hook-form's pattern rule| Mask character | Meaning |
|---|---|
# |
a digit |
L |
a letter |
X |
a letter or a digit |
* |
any character except whitespace (the zero-width space and joiners U+200B–U+200D and U+2060 aren't whitespace here; a zero-width no-break space, U+FEFF, is), e.g. Finland's century sign (-, +, a letter) |
space, . - / ( ) |
a separator: formatId() inserts it, normalizeId() removes it |
getInputMask('BRA');
// {
// countryCode: 'BRA',
// masks: ['###.###.###-##'],
// imask: [{ mask: '000.000.000-00' }],
// pattern: /^(?:\d\d\d\.\d\d\d\.\d\d\d-\d\d)$/,
// }
getInputMask('HKG')!.masks; // ['L######(X)', 'LL######(X)']
// imask: the dynamic list picks the mask that fits what has been typed
IMask(input, { mask: getInputMask('BRA')!.imask, prepareChar: c => c.toUpperCase() });
// react-hook-form: check the formatted value
register('cpf', { pattern: getInputMask('BRA')!.pattern });
// pattern.source is also valid as a native HTML pattern attribute: browsers anchor it
// (`^(?:<value>)$`) and compile it under the Unicode-set (`v`) regex flag, both of
// which pattern.source is built to satisfy
input.pattern = getInputMask('BRA')!.pattern.source;
Each mask allows every character the country's validation pattern accepts in that position, so it
doesn't block a valid ID. A test checks this against every country's example and its separator
variants. A mask can allow more, for example any letter where only one letter is valid: validate
the result with validateNationalId().
pattern.source is a shape check only, same as the RegExp itself: use it for a browser's native
<input pattern> hint, and validate the real ID with validateNationalId().
See docs/FORMS.md for a complete forms integration guide using this function.
import { validateNationalId } from 'idnumbers';
// US Social Security Number
const usa = validateNationalId('USA', '123-45-6789');
console.log(usa.isValid); // true
// UK National Insurance Number
const uk = validateNationalId('GBR', 'AB123456C');
console.log(uk.isValid); // true
// French Social Security Number
const france = validateNationalId('FRA', '255081416802538');
console.log(france.isValid); // true
import { parseIdInfo } from 'idnumbers';
// South Africa - Extract birth date, gender, citizenship
const zaf = parseIdInfo('ZAF', '8001015009087');
if (zaf.ok) console.log(zaf.info);
// {
// yyyymmdd: Date(1980-01-01),
// gender: 'male',
// citizenship: 'citizen',
// ...
// }
// Sweden - Extract birth date and gender
const swe = parseIdInfo('SWE', '811218-9876');
if (swe.ok) console.log(swe.info);
// {
// yyyymmdd: Date(1981-12-18),
// gender: 'male',
// ...
// }
// China - Extract address code, birth date, and gender
const chn = parseIdInfo('CHN', '11010219840406970X');
if (chn.ok) console.log(chn.info);
// {
// addressCode: '110102',
// birthDate: Date(1984-04-06),
// gender: 'female',
// ...
// }
// A country without a parser fails with a reason instead of returning null
const usa = parseIdInfo('USA', '123-45-6789');
console.log(usa.ok, usa.ok ? undefined : usa.reason); // false 'not_parsable'
import { validateMultipleIds } from 'idnumbers';
const ids = [
{ countryCode: 'USA', idNumber: '123-45-6789' },
{ countryCode: 'GBR', idNumber: 'AB123456C' },
{ countryCode: 'JPN', idNumber: '123456789012' },
{ countryCode: 'XXX', idNumber: '123' }, // Invalid country
];
const results = validateMultipleIds(ids);
results.forEach((result, index) => {
console.log(`ID ${index + 1}: ${result.isValid ? 'Valid' : 'Invalid'}`);
if (!result.isValid) {
console.log(` Reason: ${result.reason}`);
}
});
import { validateNationalId } from 'idnumbers';
const result = validateNationalId('USA', '000-45-6789');
if (!result.isValid) {
console.log('Validation failed:', result.reason);
// "Validation failed: invalid_format" (the 000 area number is not allowed)
}
// Unsupported country: also sets a human-readable errorMessage
const invalid = validateNationalId('XXX', '123456789');
console.log(invalid.reason, invalid.errorMessage);
// "unsupported_country Unsupported country code: XXX"
import { validateNationalId, parseIdInfo, ValidationResult } from 'idnumbers';
// Type-safe validation
const result: ValidationResult = validateNationalId('USA', '123-45-6789');
if (result.isValid && result.extractedInfo) {
// Annotated as ValidationResult, extractedInfo is a ParsedInfo; without the
// annotation, it has the country's own parse result type
console.log('Valid ID with extracted info:', result.extractedInfo);
}
// Type-safe parsing
const parsed = parseIdInfo('ZAF', '8001015009087');
if (parsed.ok) {
// `parsed.info` is typed as South Africa's parse result
console.log('Birth date:', parsed.info.yyyymmdd);
console.log('Gender:', parsed.info.gender);
}
import { validateNationalId, ValidationFailureReason } from 'idnumbers';
function validateUserID(
country: string,
idNumber: string
): {
valid: boolean;
message: string;
} {
const result = validateNationalId(country, idNumber);
if (result.isValid) {
return { valid: true, message: 'Valid ID number' };
}
switch (result.reason) {
case ValidationFailureReason.UNSUPPORTED_COUNTRY:
return { valid: false, message: 'Unknown country code' };
case ValidationFailureReason.INVALID_LENGTH:
return { valid: false, message: 'Wrong length' };
case ValidationFailureReason.INVALID_FORMAT:
return { valid: false, message: 'Does not match the expected pattern' };
case ValidationFailureReason.CHECKSUM_MISMATCH:
return { valid: false, message: 'Checksum digit is wrong' };
case ValidationFailureReason.INVALID_BIRTHDATE:
return { valid: false, message: 'Encoded birth date is not a real date' };
default:
return { valid: false, message: 'Validation failed for another reason' };
}
}
// In your form handler
const validation = validateUserID('USA', userInput);
if (!validation.valid) {
showError(validation.message);
}
See docs/FORMS.md for a complete guide: a React + react-hook-form worked
example, a framework-free example, input masks with getInputMask(), and tree-shakeable
single-country subpath imports.
Accepted letter case, surrounding whitespace, and separators for every country are listed in docs/INPUT_FORMATS.md.
XXX-XX-XXXX, with the dashes (123456789 without them is rejected)000, 666, 900-999123-45-6789BG, GB, NK, KN, TN, NT, ZZAB123456CX (representing 10), uppercase only11010219840406970X800101500908725508141680253865929970489The test suite covers:
METADATA.regexp (fast-check)Run tests:
npm test
Run tests with coverage:
npm run test:coverage
Contributions are welcome! Please feel free to submit a Pull Request.
See CONTRIBUTING.md for local setup, the development workflow, and the pull-request process.
MIT License - see LICENSE file for details
See CHANGELOG.md for release history and version details.
This library is inspired by and maintains compatibility with validation logic from various national ID systems worldwide.
For issues, questions, or contributions, please visit: