Skip to content

Validate on submit

Composes: parsePhoneNumber + PhoneNumber + ValidationError

Accept or reject a phone field when a form is submitted, and store the accepted value. On submit, the field’s digits drive one deterministic automaton compiled from Google’s metadata; validity is an accepting state, and every ValidationError variant is a named way of not being in one (the model).

import { ensureEngineReady, parsePhoneNumber, type ValidationError } from '@telixon/core';
await ensureEngineReady();
type SubmitOutcome =
| { readonly accepted: true; readonly e164: string }
| { readonly accepted: false; readonly error: ValidationError | null };
function validatePhoneField(raw: string): SubmitOutcome {
const phoneNumber = parsePhoneNumber(raw, { defaultRegion: 'US' });
const e164 = phoneNumber.formatE164();
if (phoneNumber.isValid() && e164 !== null) {
return { accepted: true, e164 };
}
return { accepted: false, error: phoneNumber.getValidationError() };
}
validatePhoneField('+1 415 555 0132');
// -> { accepted: true, e164: '+14155550132' }
validatePhoneField('(415) 555-0132');
// -> { accepted: true, e164: '+14155550132' }
validatePhoneField('415 555');
// -> { accepted: false, error: { kind: 'TOO_SHORT', minLength: 10 } }

Submitting +1 415 555 0132 or (415) 555-0132 stores the same string, +14155550132. Submitting 415 555 is rejected with a TOO_SHORT error carrying the minimum national length, 10.

Await ensureEngineReady once before the first parse; parsePhoneNumber throws EngineNotReadyError if called earlier. Where to place this call in an application is covered in Engine loading.

Pass the value exactly as the user typed it; (415) 555-0132 resolves without stripping punctuation. Set defaultRegion to the region the form expects for national input; input with a leading + carries its own calling code and ignores defaultRegion.

Accept when isValid() returns true, reject otherwise; do not branch on getValidationError() === null (see step 5). By default isValid() accepts a number that is valid for any region the digits can resolve to. To accept only numbers of the defaultRegion, pass strict: true; see Strict mode.

E.164 is one region-independent string per number, so stored values compare by equality and reparse without a defaultRegion. formatE164() is non-null whenever isValid() returns true; the e164 !== null check in the handler narrows the type without an assertion. The other formatters on PhoneNumber are for display, not storage.

NATIONAL_PREFIX_MISSING is the one ValidationError variant that coexists with isValid() returning true: the digits reach an accepting state, and the variant records that the region’s national prefix was absent from the input.

const phoneNumber = parsePhoneNumber('7400 123456', { defaultRegion: 'GB' });
phoneNumber.isValid();
// -> true
phoneNumber.getValidationError();
// -> { kind: 'NATIONAL_PREFIX_MISSING', expectedPrefix: '0' }
phoneNumber.formatE164();
// -> '+447400123456'
phoneNumber.formatNational();
// -> '07400 123456'

Because the handler branches on isValid() first, this submission is accepted and +447400123456 is stored. Optionally echo formatNational() back into the field so the user sees the national form with the prefix restored: 07400 123456.

When isValid() returns false, getValidationError() names the reason as a discriminated union on kind, with variant-specific data such as minLength above. The full union is specified on the ValidationError page; mapping each variant to a field message is covered in Form errors.

  • Form errors: turn each ValidationError variant into a field message.
  • Drive an input: validate while the user types; parsePhoneNumber and the input controller are the same shared resolve layer, queried in batch versus per keystroke.
  • Strict mode: restrict validity to a single region.