victorfernandesraton/zod-phonenumber(opens in a new tab)
Language: TypeScript
Zod phoennumber solution for phone validation with libphonumber support
- stars
- 4
- watchers
- 0
- forks
- 0
- open issues
- 0
- Last updated
- 08/22/2026
Language: TypeScript
Zod phoennumber solution for phone validation with libphonumber support
Zod extension for phone number validation using libphonenumber-js.
Most phone validation libraries treat phone numbers as plain strings — you validate them, maybe format them, and move on. They don't leverage zod's composable, chainable API.
z.phone() is native to zod's ecosystem. You chain constraints directly on the schema — .country(), .ddi(), .ddd() — the same way you chain .min(), .max(), .email() on z.string(). And because it uses zod's refine + transform, the output is a fully typed PhoneNumber object from libphonenumber-js, ready to use.
.country('BR').ddd('11') feels like zod, not a wrapperPhoneNumber — not a boolean, not a reformatted string. Full object with .country, .nationalNumber, .formatInternational(), etc.BR.timezone('America/Bahia') infers country/DDI and parses national numbersany, zero @ts-ignore, fully type-safenpm install @victorfernandesraton/zod-phonenumber installs everythingimport { z } from '@victorfernandesraton/zod-phonenumber' const schema = z.object({ name: z.string(), phone: z.phone().country('BR').ddd('11'), }) const result = schema.parse({ name: 'Joao', phone: '+5511999988888' }) result.phone.country // 'BR' result.phone.number // '+5511999988888'
npm install @victorfernandesraton/zod-phonenumber
Pattern A — Re-export (recommended):
import { z } from '@victorfernandesraton/zod-phonenumber' const phone = z.phone().country('BR').parse('+5511999988888')
Pattern B — Named import:
import { z } from 'zod' import { phone } from '@victorfernandesraton/zod-phonenumber' const schema = z.object({ phone: phone().country('BR').ddd('11'), })
z.phone() validates any international phone number and returns a PhoneNumber object:
const phone = z.phone().parse('+12133734253') phone.country // 'US' phone.number // '+12133734253' phone.nationalNumber // '2133734253' phone.countryCallingCode // '1' phone.isPossible() // true phone.formatInternational() // '+1 213 373 4253' phone.formatNational() // '(213) 373-4253'
Invalid numbers throw ZodError:
z.phone().parse('not-a-phone') // throws ZodError
Restrict to a specific country:
z.phone().country('BR').parse('+5511999988888') // ok z.phone().country('BR').parse('+12133734253') // throws
Or pass as constructor argument:
z.phone('BR').parse('+5511999988888') // ok z.phone('BR').parse('+12133734253') // throws
Filter by country calling code:
z.phone().ddi('+55').parse('+5511999988888') // ok z.phone().ddi('55').parse('+5511999988888') // ok (without +) z.phone().ddi('+55').parse('+12133734253') // throws
Filter by Brazilian DDD (2-digit area code). Automatically sets country to 'BR':
z.phone().ddd('11') // Sao Paulo z.phone().ddd('21') // Rio de Janeiro z.phone().ddd('31') // Belo Horizonte z.phone().ddd('11').parse('+5511999988888') // ok z.phone().ddd('11').parse('+5521999988888') // throws (wrong area code)
Methods are chainable:
z.phone().country('BR').ddd('11') z.phone().ddi('+55').ddd('11') z.phone('BR').ddd('11')
Use an IANA timezone to infer the country and parse national numbers. The timezone map is generated during development from @vvo/tzdb and bundled with the package.
const value = z.phone('America/Bahia').parse('71999927837') value.countryCallingCode // '55' value.number // '+5571999927837'
The chainable form is also supported with .timezone('America/Bahia'):
const value = z.phone().timezone('America/Bahia').parse('71999927837') value.number // '+5571999927837'
The timezone can be combined with .ddd():
z.phone().timezone('America/Bahia').ddd('71').parse('71999927837') // ok z.phone().timezone('America/Bahia').ddd('11').parse('71999927837') // throws
Technical zones such as UTC and Etc/*, invalid zones, and zones without a unique country are rejected. The timezone infers country/DDI only; it does not validate DDD on its own.
const userSchema = z.object({ name: z.string(), phone: z.phone().country('BR').ddd('11'), }) const user = userSchema.parse({ name: 'Maria', phone: '+5511999988888', }) // user.phone is a PhoneNumber object
Creates a phone number schema. Returns a schema with methods:
| Method | Description |
|---|---|
.parse(data) |
Validates and returns PhoneNumber. Throws on invalid |
.safeParse(data) |
Returns { success, data } or { success, error } |
.country(code) |
Filters by country ISO code |
.ddi(code) |
Filters by international calling code |
.ddd(code) |
Filters by Brazilian area code (auto-sets BR) |
.timezone(zone) |
Infers country from an IANA timezone |
The constructor z.phone() accepts either a country code ('BR') or an IANA timezone ('America/Bahia'). Values containing a / are treated as timezones.
schema.parse() returns a PhoneNumber instance from libphonenumber-js:
| Property/Method | Description |
|---|---|
.country |
ISO country code |
.number |
E.164 format |
.nationalNumber |
National significant number |
.countryCallingCode |
Country calling code |
.isPossible() |
Validates length |
.isValid() |
Full validation (needs max metadata) |
.getType() |
Phone type (needs max metadata) |
.formatInternational() |
e.g. '+55 11 99999 8888' |
.formatNational() |
e.g. '(11) 99999-8888' |
.getURI() |
e.g. 'tel:+5511999988888' |
# Install dependencies npm install # Regenerate the timezone map from @vvo/tzdb (dev dependency) npm run generate:timezones # Run tests with coverage report (65 tests) npm test # Lint the project npm run lint # Build for distribution npm run build
The test and test:all scripts run the native Node.js test runner through tsx with coverage enabled. Tests exercise phone validation, import patterns, timezone resolution, and the timezone generator.
.number is the complete E.164 value. Timezone mappings are generated by scripts/generate-timezone-regions.ts using the development-only @vvo/tzdb dependency. The generated timezone-regions.generated.ts file is bundled; @vvo/tzdb is not required at runtime. Runtime identifiers are checked with Intl.DateTimeFormat.
BSD-3-Clause