Phone
Phone number validation, parsing, and formatting — the same helpers PhoneInput uses, without the input.
Quick Preview
- validatePhoneNumber
- true
- parsePhoneNumber
- +2348012345678
- formatPhoneNumber
- +234 801 234 5678
- formatPhoneNumber · "national"
- 0801 234 5678
- formatPhoneNumber · "rfc3966"
- tel:+2348012345678
- getPhoneNumberCountry
- NG
Usage
A phone number is validated in far more places than it is typed — a schema, a server action, a CSV import, a table cell — and every one of those was otherwise reaching for its own regex. These carry no React and no styling, so a server-only module can import them.
import {
formatPhoneNumber,
parsePhoneNumber,
validatePhoneNumber,
} from "@giddaa-housing/ui/utils/phone";
validatePhoneNumber("+2348012345678"); // true
parsePhoneNumber("0801 234 5678", { country: "NG" }); // "+2348012345678"
formatPhoneNumber("+2348012345678"); // "+234 801 234 5678"One rule runs through all of them: invalid in, empty out. Anything that is not a real, dialable number formats and parses to "" — never null, never undefined, never a half-cleaned version of what you passed. There is one thing to check instead of three.
With a schema
validatePhoneNumber is written to be passed by reference. Both libraries call a validator with a second argument of their own — a refinement context, a test context — and it is built to ignore anything that is not an options object.
// zod
z.object({
phone: z.string().refine(validatePhoneNumber, "Enter a valid phone number"),
});
// yup
yup.object({
phone: yup
.string()
.test("phone", "Enter a valid phone number", validatePhoneNumber),
});Empty and null are invalid. Mark the field optional in the schema rather than asking the validator to decide whether a missing number is a problem — that is the schema's business.
To validate against a country, wrap it:
z.string().refine((value) => validatePhoneNumber(value, { country: "NG" }));Countries
A number written nationally — 0801 234 5678 — is a valid Nigerian number and nothing at all without a country. One that carries its own calling code needs no help.
validatePhoneNumber("0801 234 5678"); // false
validatePhoneNumber("0801 234 5678", { country: "NG" }); // trueExamples
Validating, parsing, and formatting
Type a number and switch the country to watch each helper respond. Note that an invalid number blanks the parsed and formatted results rather than showing a partial one.
- validatePhoneNumber
- true
- parsePhoneNumber
- +2348012345678
- formatPhoneNumber
- +234 801 234 5678
- formatPhoneNumber · "national"
- 0801 234 5678
- formatPhoneNumber · "rfc3966"
- tel:+2348012345678
- getPhoneNumberCountry
- NG
Storing and displaying
Parse on the way in, format on the way out. Store E.164 and nothing else, so a number is comparable and dialable wherever it ends up.
const stored = parsePhoneNumber(input, { country }); // "+2348012345678"
<a href={formatPhoneNumber(stored, { format: "rfc3966" })}>
{formatPhoneNumber(stored)}
</a>;A country list
getPhoneCountries returns what PhoneInput builds its own picker from, so a country field elsewhere in the same form lists the same countries in the same order.
{getPhoneCountries().map(({ code, name, callingCode }) => (
<SelectItem key={code} value={code}>
{name} (+{callingCode})
</SelectItem>
))}Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
validatePhoneNumber | (value, options?) => boolean | - | Whether the value is a number that could be dialed. Checks the number against its country's assigned ranges, not just its digit count. |
parsePhoneNumber | (value, options?) => string | - | The E.164 form for storage, or "". |
formatPhoneNumber | (value, options?) => string | - | The number written for a reader, or "". |
getPhoneNumberCountry | (value, options?) => CountryCode | undefined | - | The country a valid number belongs to. |
getPhoneCountries | (locale?) => PhoneCountry[] | "en" | Every country, as { code, name, callingCode }, sorted by display name. |
options.country | CountryCode | - | The country to read a number against when it carries no calling code. |
options.format | "international" | "national" | "e164" | "rfc3966" | "international" | Only on formatPhoneNumber. |
Every helper takes unknown rather than string, so a null from a database or a context object from a validation library is a false or a "" rather than a crash.
Accessibility
- Display numbers with
formatPhoneNumberrather than raw E.164. The grouped form is read out in digestible chunks by a screen reader; an unbroken run of digits is read as one long number. - Use
format: "rfc3966"for atel:hrefso the link is dialable on a phone and announced as a phone number. - Do not rely on formatting alone to signal that a number is invalid — pair it with a message, since the empty string these helpers return has nothing to announce.