Validate phone numbers with Yup
Phone number validation in Yup that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets Yup's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the JavaScript guide; the rest is Yup's own validation hook, a few lines long.
1. The file
Take phone.mjs from the JavaScript guide, with the plans of the countries you serve (the link opens it for the United Kingdom, the United States and France, the plans the tests below use). It needs Node 20 or later, or any current browser; no dependency. Its parse reads a number as typed and returns the number with its E.164 form, region and line type, or null when no plan accepts it.
npm install yup2. The Yup side
phone-schema.mjs
The schema: a transform to E.164, then a test.
import { object, string } from 'yup'
import { parse } from './phone.mjs'
/**
* A phone number as people type it, read in the plans of phone.mjs and cast to its E.164 form.
* defaultRegion (an ISO code such as 'GB') reads numbers typed without their country code.
*/
export const phoneNumber = (defaultRegion) =>
string()
.transform((text) => parse(text, defaultRegion)?.e164 ?? text)
.test('phone-number', 'Not a valid phone number', (value) => value == null || parse(value) !== null)
export const Signup = object({
name: string().required(),
phone: phoneNumber('GB').required(),
})3. The tests
phone-schema.test.mjs: The tests, with node:test.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { Signup } from './phone-schema.mjs'
test('casts a national number in the default region to E.164', async () => {
assert.deepEqual(await Signup.validate({ name: 'Ada', phone: '07400 123456' }), { name: 'Ada', phone: '+447400123456' })
})
test('casts an international number whatever the default region', async () => {
assert.equal((await Signup.validate({ name: 'Ada', phone: '+1 201-555-0123' })).phone, '+12015550123')
})
test('refuses what no plan accepts, on the phone field', async () => {
await assert.rejects(Signup.validate({ name: 'Ada', phone: '+44 7400 12345' }), (error) => {
assert.equal(error.path, 'phone')
assert.deepEqual(error.errors, ['Not a valid phone number'])
return true
})
})node --testRun before publishing with yup 1.7.1 and the file generated for GB, US, FR, as 2026-10-25.
What is specific to Yup
- Yup runs transforms before tests: the number is cast to E.164 first, so the test only checks that the result is a number of some plan. A number no plan accepts keeps its text and fails the test.
validateresolves to the cast object, which holds what you store;isValidanswers yes or no without it.- Formik takes the schema as
validationSchema; React Hook Form takes it throughyupResolverfrom its resolvers package. Validate again on the server with the same schema: phone.mjs has no dependency.