Skip to content

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 yup

2. 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 --test

Run 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.
  • validate resolves to the cast object, which holds what you store; isValid answers yes or no without it.
  • Formik takes the schema as validationSchema; React Hook Form takes it through yupResolver from its resolvers package. Validate again on the server with the same schema: phone.mjs has no dependency.