ValidationError
ValidationError is the discriminated union returned by PhoneNumber.getValidationError(): when a number is not valid, it names the class of failure and carries the values needed to describe it. 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 union is produced by the shared resolve layer, so parsePhoneNumber and the input controller report identical errors for identical digits, queried in batch versus per keystroke.
Import
Section titled “Import”import type { ValidationError } from '@telixon/core';ValidationError is a type-only export and needs no engine. Producing a value does: it is read from a PhoneNumber, which requires the engine to be ready.
Signature
Section titled “Signature”Verbatim from the source:
/** Structured validation outcome beyond `valid: boolean`. Kind values mirror libphonenumber where applicable. */export type ValidationError = | { readonly kind: 'EMPTY' } | { readonly kind: 'INVALID_CALLING_CODE' } | { readonly kind: 'TOO_SHORT'; readonly minLength: number } | { readonly kind: 'TOO_LONG'; readonly maxLength: number } | { readonly kind: 'INVALID_LENGTH'; readonly possibleLengths: readonly number[] } | { readonly kind: 'POSSIBLE_LOCAL_ONLY' } | { readonly kind: 'PATTERN_MISMATCH' } | { readonly kind: 'NATIONAL_PREFIX_MISSING'; readonly expectedPrefix: string };The accessor on PhoneNumber:
getValidationError(): ValidationError | null;Every field is readonly; treat payloads as immutable. As the source comment states, kind values mirror Google libphonenumber where applicable, so TOO_SHORT, TOO_LONG, and INVALID_LENGTH mean exactly what they mean there.
Variants
Section titled “Variants”Each variant names a distinct class of non-accepting state. Every payload below is the executed output for its repro input; (US) and (GB) mean the input was parsed with { defaultRegion: 'US' } or { defaultRegion: 'GB' }.
| Variant | Payload | Triggering condition | Repro input (region) | isValid() |
|---|---|---|---|---|
EMPTY |
none | No digits and no calling code in the input | '' |
false |
INVALID_CALLING_CODE |
none | The digits after + match no calling code |
'+999 123 4567' |
false |
TOO_SHORT |
minLength: 10 |
Fewer national significant digits than any possible number | '415 555' (US) |
false |
TOO_LONG |
maxLength: 10 |
More national significant digits than any possible number | '(415) 555-01320' (US) |
false |
INVALID_LENGTH |
possibleLengths: [9, 12] |
The digit count matches none of the allowed lengths | '+41 44 668 18 000' |
false |
POSSIBLE_LOCAL_ONLY |
none | Possible only for local dialing, such as a subscriber number without its area code | '555 0132' (US) |
false |
PATTERN_MISMATCH |
none | The length is possible, but the digits end in a non-accepting state | '+1 123 456 7890' |
false |
NATIONAL_PREFIX_MISSING |
expectedPrefix: '0' |
Valid digits, but national-mode input omitted a required trunk prefix | '7400 123456' (GB) |
true |
NATIONAL_PREFIX_MISSING is the one advisory variant: isValid() stays true. See the advisory variant.
Returns
Section titled “Returns”getValidationError() returns ValidationError | null. null means nothing is wrong: the digits are in an accepting state and no advisory applies.
import { ensureEngineReady, parsePhoneNumber } from '@telixon/core';
await ensureEngineReady();
parsePhoneNumber('415 555', { defaultRegion: 'US' }).getValidationError();// { kind: 'TOO_SHORT', minLength: 10 }
parsePhoneNumber('+1 415 555 0132').getValidationError();// nullErrors
Section titled “Errors”getValidationError() itself never throws. The error surface is upstream: calling parsePhoneNumber before the engine is initialized throws EngineNotReadyError, so no PhoneNumber exists to query. See ensureEngineReady.
Behavior
Section titled “Behavior”Map variants to messages; never re-derive the conditions. The payload already carries every value a message needs, computed by the same automaton that decided validity.
Precedence
Section titled “Precedence”Checks run in a fixed order and the first match wins, so at most one variant is ever reported:
EMPTYINVALID_CALLING_CODETOO_SHORTTOO_LONGINVALID_LENGTHPOSSIBLE_LOCAL_ONLYPATTERN_MISMATCHNATIONAL_PREFIX_MISSING
Length variants report before PATTERN_MISMATCH deliberately. A wrong-length number usually fails the pattern too, but the length variant carries the expected digit count, so the copy built from it stays concrete: “at least 10 digits” instead of a generic rejection.
// 9 national digits: both too short and pattern-invalid. Length wins.parsePhoneNumber('+1 123 456 789').getValidationError();// { kind: 'TOO_SHORT', minLength: 10 }Length payloads
Section titled “Length payloads”minLength, maxLength, and possibleLengths count national significant digits. The calling code and the national prefix are excluded.
// 12 digits typed, but only the 10 after the calling code count ('4466818000').// Swiss numbers allow 9 or 12 national significant digits; 10 matches neither.parsePhoneNumber('+41 44 668 18 000').getValidationError();// { kind: 'INVALID_LENGTH', possibleLengths: [ 9, 12 ] }
// 11 digits typed, but the leading '1' is the US national prefix:// 10 national significant digits remain, and the number is valid.parsePhoneNumber('1 415 555 0132', { defaultRegion: 'US' }).getValidationError();// nullRelationship to isPossibleWithReason
Section titled “Relationship to isPossibleWithReason”isPossibleWithReason() returns the coarse possibility verdict (PossibilityResult); getValidationError() returns the classified failure. The two share vocabulary: INVALID_CALLING_CODE, TOO_SHORT, TOO_LONG, and INVALID_LENGTH name the same states in both unions.
isPossibleWithReason() |
getValidationError() |
|---|---|
'IS_POSSIBLE' |
null, PATTERN_MISMATCH, or NATIONAL_PREFIX_MISSING |
'IS_POSSIBLE_LOCAL_ONLY' |
POSSIBLE_LOCAL_ONLY |
'INVALID_CALLING_CODE' |
INVALID_CALLING_CODE, or EMPTY for empty input |
'TOO_SHORT' |
TOO_SHORT |
'TOO_LONG' |
TOO_LONG |
'INVALID_LENGTH' |
INVALID_LENGTH |
Possibility is a length-level verdict, so it cannot separate a valid number from one that fails the pattern; getValidationError() resolves that remainder into null, PATTERN_MISMATCH, or NATIONAL_PREFIX_MISSING. The EMPTY row shows precedence at work: empty input has no calling code, so its coarse verdict is 'INVALID_CALLING_CODE', but the classification checks EMPTY first.
const empty = parsePhoneNumber('');empty.isPossibleWithReason(); // 'INVALID_CALLING_CODE'empty.getValidationError(); // { kind: 'EMPTY' }The advisory variant
Section titled “The advisory variant”NATIONAL_PREFIX_MISSING is the only variant that coexists with isValid() === true. The digits are in an accepting state; what is missing is the trunk prefix that national dialing conventions require in front of them. The payload names it, and every format method emits it.
const phoneNumber = parsePhoneNumber('7400 123456', { defaultRegion: 'GB' });
phoneNumber.getValidationError(); // { kind: 'NATIONAL_PREFIX_MISSING', expectedPrefix: '0' }phoneNumber.isValid(); // truephoneNumber.formatNational(); // '07400 123456'phoneNumber.formatE164(); // '+447400123456'Only national-mode input can trigger it. International input legitimately omits the trunk prefix, so a number entered with + never reports this variant:
parsePhoneNumber('+44 7400 123456').getValidationError();// nullThe variant is also absent when the matching format marks the prefix optional, as US formats do.
Examples
Section titled “Examples”Map every variant to a message
Section titled “Map every variant to a message”The canonical consumption pattern is one exhaustive switch over kind. All eight cases return, so under noImplicitReturns TypeScript rejects the function if any case is missing.
import type { ValidationError } from '@telixon/core';
export function toMessage(error: ValidationError): string { switch (error.kind) { case 'EMPTY': return 'Enter a phone number.'; case 'INVALID_CALLING_CODE': return 'No country matches this calling code.'; case 'TOO_SHORT': return `At least ${error.minLength} digits are required.`; case 'TOO_LONG': return `At most ${error.maxLength} digits are allowed.`; case 'INVALID_LENGTH': return `Expected ${error.possibleLengths.join(' or ')} digits.`; case 'POSSIBLE_LOCAL_ONLY': return 'Add the area code.'; case 'PATTERN_MISMATCH': return 'This number does not exist in its numbering plan.'; case 'NATIONAL_PREFIX_MISSING': return `Start the number with ${error.expectedPrefix}.`; }}Do not gate on the error alone
Section titled “Do not gate on the error alone”error === null implies isValid(), but the reverse fails for the advisory variant. Treating any non-null error as invalid misclassifies valid input:
const phoneNumber = parsePhoneNumber('7400 123456', { defaultRegion: 'GB' });
// Wrong: rejects a valid number.const rejected = phoneNumber.getValidationError() !== null; // true
// Right: gate on validity, use the error for the message or the fix-up.phoneNumber.isValid(); // truephoneNumber.formatNational(); // '07400 123456'Used in guides
Section titled “Used in guides”- Form errors: map each variant to field-level copy.
- Validate on submit: gate submission on
isValid()and classify the failure. - Drive an input: the same classification per keystroke, from the input controller.
See also
Section titled “See also”- PhoneNumber: the handle whose
getValidationError()produces this union. - parsePhoneNumber: the batch entry into the shared resolve layer.
- The model: why validity is an accepting state.