libphonenumber-js
libphonenumber-js parses, validates, and formats personal phone numbers using Google's own numbering-plan data, rewritten from scratch in JavaScript instead of compiled from Google's Closure-based port. You give it a string like ' 8 (800) 555-35-35 ' and a default country, and you get back a PhoneNumber object that knows the E.164 form (+78005553535), the country, the national number, and how to print it internationally or nationally. It also ships an AsYouType class for formatting a number while the user is still typing, and findPhoneNumbersInText for pulling numbers out of prose. The headline difference from Google's port is size: Google's compiled bundle is around 550 kB, and this one is roughly 145 kB with full metadata and about 80 kB with the default reduced set, because you pick which metadata bundle to import. It has zero runtime dependencies and ships TypeScript definitions.
The default choice for phone input on the web: correct enough because it uses Google's metadata, small enough because you choose how much of it to ship. Decide between the min and max entry points on day one, and prefer isPossible() over isValid() unless you will keep the dependency updated.
Use it if
- You need real E.164 normalization before storing a phone number, so that +1 213 373 4253, (213) 373-4253, and 213.373.4253 all land in the database as the same string
- You are validating international phone input and a regex is not going to cut it: numbering plans differ per country and change several times a year
- You want an as-you-type formatter for a phone input, with parentheses and spacing appearing correctly for whichever country the user picked
- You want Google's data without Google's bundle: importing from libphonenumber-js/min gives you length validation and formatting for every country in about 80 kB of metadata
- You need to detect number type (mobile, fixed line, toll free) to decide whether to offer SMS, which the /max metadata bundle supports through getType()
- You only take numbers from one country with a fixed format. A length check plus a digit strip is smaller and easier to reason about than 45 kB gzipped of parser and metadata
- Bundle size is the constraint on a mobile landing page. Even the default min build is one of the largest single dependencies most forms will ever pull in, and the max build roughly doubles the metadata
- You want geolocation, carrier lookup, or short codes. This library deliberately skips emergency numbers, SMS short codes, numbers beginning with *, alphabetic vanity numbers, and city-level geocoding, all of which Google's library does handle
- You plan to deploy once and never update. isValid() checks digits against numbering plans that change, so an unmaintained site will eventually start rejecting legitimate newly allocated numbers; the author recommends isPossible() for exactly this reason
- You want to file issues where you file everything else. Primary development moved to GitLab after the author's GitHub account was banned in 2020, and the GitHub repo is described in the README as a backup
- You expect a bug report to be accepted on its merits. The stated policy is that behavior matching Google's demo page is not a bug, so numbering-plan disputes get redirected to Google
Setup reality
npm install libphonenumber-js and there are no peer dependencies and no build step, but the first real decision is which entry point to import from, and getting it wrong is the most common complaint. Plain 'libphonenumber-js' is an alias for the min metadata: isPossible() works, but isValid() is loose and getType() returns undefined for most countries. If you need strict digit validation or number type, import from 'libphonenumber-js/max' and accept roughly 145 kB of metadata; 'libphonenumber-js/mobile' is the middle option; 'libphonenumber-js/core' ships no metadata at all and expects you to pass your own as the last argument to every function. Second gotcha, and the one that generates the most confused bug reports: the default export returns undefined on unparseable input, while the named export literally called parsePhoneNumber is an alias for parsePhoneNumberWithError and throws a ParseError. Autocomplete offers the named one first, so people end up with unhandled exceptions in a form handler. Whichever you choose, every call needs a guard before you touch .country. Third: without a defaultCountry, a national-format string with no + is unparseable, so a country selector has to exist in your UI or come from somewhere. Fourth: the metadata JSON files are large enough that some bundlers with aggressive JSON inlining produce noticeably slower builds.
Patterns
Parse user input into a phone number objectparse-number
// the DEFAULT export returns undefined on bad input
import parsePhoneNumber from 'libphonenumber-js';
const phone = parsePhoneNumber(' 8 (800) 555-35-35 ', 'RU');
if (phone) {
phone.country; //=> 'RU'
phone.number; //=> '+78005553535'
phone.nationalNumber; //=> '8005553535'
phone.countryCallingCode; //=> '7'
phone.isPossible(); //=> true
}Import this as the default export. The named export called parsePhoneNumber is an alias for parsePhoneNumberWithError and throws a ParseError instead of returning undefined, which is the single easiest way to get this library wrong.
Normalize to E.164 before storingnormalize-e164
import parsePhoneNumber from 'libphonenumber-js';
function toE164(input, country) {
const phone = parsePhoneNumber(input, country);
if (!phone || !phone.isPossible()) return null;
return phone.number; // always '+<digits>'
}
toE164('(213) 373-4253', 'US'); //=> '+12133734253'Store phone.number and nothing else: it is the one representation that stays stable and comparable. Formatting for display is cheap to redo on read, and keeping the user's punctuation in the database guarantees duplicate rows.
Validate a number without parsing it firstvalidate-input
import {
isPossiblePhoneNumber,
isValidPhoneNumber,
} from 'libphonenumber-js';
isPossiblePhoneNumber('8 (800) 555-35-35', 'RU'); //=> true (length only)
isValidPhoneNumber('8 (800) 555-35-35', 'RU'); //=> true (length + digits)isPossible() checks length; isValid() also checks the digit ranges against the numbering plan. The author recommends isPossible() for forms, because isValid() goes stale on a site that stops updating dependencies and starts rejecting real numbers.
Format for display and for tel: linksformat-display
import parsePhoneNumber from 'libphonenumber-js';
const phone = parsePhoneNumber('+12133734253');
phone.formatInternational(); //=> '+1 213 373 4253'
phone.formatNational(); //=> '(213) 373-4253'
phone.getURI(); //=> 'tel:+12133734253'
phone.format('E.164'); //=> '+12133734253'
phone.format('IDD', { fromCountry: 'US' });International format uses spaces rather than hyphens or brackets by design, so output will not match Google's library character for character. Use getURI() for a tel: href rather than concatenating digits, and note that format('IDD') returns undefined when options.fromCountry is missing or has no IDD prefix.
Format a phone field while the user typesas-you-type
import { AsYouType } from 'libphonenumber-js';
const formatter = new AsYouType('US');
formatter.input('213'); //=> '213'
formatter.input('3734'); //=> '(213) 373-4'
// in a controlled input, build a fresh formatter each keystroke
function onChange(value) {
setValue(new AsYouType('US').input(value));
}An AsYouType instance is stateful and appends: calling .input() twice on the same instance concatenates the two strings. Either call .reset() or construct a new instance per render, which is why the controlled-input version above looks wasteful but is correct.
Tell a mobile number from a landlinenumber-type
// requires the full metadata bundle
import parsePhoneNumber from 'libphonenumber-js/max';
const phone = parsePhoneNumber('+12133734253');
phone.getType(); //=> 'FIXED_LINE_OR_MOBILE'
if (phone.getType() === 'MOBILE') {
offerSmsVerification();
}getType() returns undefined on the default min metadata, which is the single most reported non-bug. Also expect FIXED_LINE_OR_MOBILE for North America, where the plan does not separate the two, so never treat undefined or that value as 'not a mobile'.
Pick the metadata bundle you actually pay forchoose-metadata
// ~80 kB metadata: length checks and formatting only
import parsePhoneNumber from 'libphonenumber-js/min';
// ~145 kB metadata: strict isValid() and getType()
import parsePhoneNumber from 'libphonenumber-js/max';
// ~95 kB metadata: max capabilities, mobile numbers only
import parsePhoneNumber from 'libphonenumber-js/mobile';Bare 'libphonenumber-js' is an alias for /min. Mixing entry points in one app ships both metadata files, so pick one and enforce it with an import lint rule.
Ship metadata for only the countries you supportcustom-metadata
// generate a trimmed metadata file, then:
import parsePhoneNumber, { isValidPhoneNumber } from 'libphonenumber-js/core';
import metadata from './metadata.custom.json';
parsePhoneNumber('(213) 373-4253', 'US', metadata);
isValidPhoneNumber('(213) 373-4253', 'US', metadata);Every function in the core entry point takes metadata as its last argument, and forgetting it throws rather than falling back. Worth it only when you support a handful of countries and the bundle budget is genuinely tight.
Extract phone numbers from free textfind-in-text
import { findPhoneNumbersInText } from 'libphonenumber-js';
const found = findPhoneNumbersInText(
'Call +7 (800) 555-35-35 or the US branch at (213) 373-4253 ext. 1234.',
'US',
);
for (const { number, startsAt, endsAt } of found) {
console.log(number.number, number.ext, startsAt, endsAt);
}Each result carries the PhoneNumber plus character offsets, which is what you need for highlighting or link substitution. This is ported from Google's Java matcher, so it is deliberately conservative and will miss loosely written numbers.
Build a country selector from the library's own datacountry-helpers
import {
getCountries,
getCountryCallingCode,
isSupportedCountry,
} from 'libphonenumber-js';
const options = getCountries().map(code => ({
code,
dial: '+' + getCountryCallingCode(code),
}));
isSupportedCountry('ZZ'); //=> falsegetCountries() reflects whichever metadata bundle you imported, so a custom build returns a shorter list. Guard user-supplied country codes with isSupportedCountry() before passing them in, because an unknown code throws.
Tell the user why the number is rejectedlength-feedback
import { validatePhoneNumberLength } from 'libphonenumber-js';
validatePhoneNumberLength('+1213373'); //=> 'TOO_SHORT'
validatePhoneNumberLength('+121337342531234'); //=> 'TOO_LONG'
validatePhoneNumberLength('+12133734253'); //=> undefined (ok)Returns undefined when the length is acceptable, which reads backwards but lets you write `const error = validatePhoneNumberLength(v)`. Other possible values include INVALID_COUNTRY and NOT_A_NUMBER, so map them to messages rather than showing the raw string.
Accept numbers from one country onlycountry-restricted
import parsePhoneNumber from 'libphonenumber-js/max';
function isValidForCountry(input, country) {
const phone = parsePhoneNumber(input, {
defaultCountry: country,
extract: false,
});
if (!phone) return 'NOT_A_NUMBER';
if (phone.country !== country) return 'WRONG_COUNTRY';
return phone.isValid() ? 'OK' : 'INVALID';
}The library deliberately does not export a helper for this; the README shows this exact implementation and explains why. isValidPhoneNumber('+44...', 'US') returns true, because a leading + makes the default country irrelevant, so a naive check accepts the whole world.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| google-libphonenumber | npm | You need feature parity with Google's Java library, including geocoding, carrier lookup, and short codes, and can afford the much larger bundle |
| awesome-phonenumber | npm | You want Google's compiled logic behind a modern ESM and TypeScript API, with the metadata still bundled in |
| react-phone-number-input | npm | You want a ready-made React input with country selector and flags rather than wiring AsYouType up yourself |