让表单错误提示既符合加蓬网络无障碍指南,又不让业务代码里到处散落错误字符串,通常需要把错误类型作为一等公民来设计。TypeScript 的可辨识联合类型很适合承担这个角色,而 IWAC 可以把错误模型、消息映射和无障碍绑定集中封装起来,为后续维护省去大量重复工作。

下面从指南要求出发,梳理错误分类,再逐步给出类型定义、验证器实现以及错误渲染时的无障碍绑定示例。
一、加蓬指南对表单错误的约束与分类
加蓬网络无障碍指南在表单错误提示方面强调三个基本原则:错误必须能被屏幕阅读器感知,错误信息必须指出具体字段和原因,不能只依赖颜色或图标提示。根据这些要求,表单错误可以归纳为四类:必填缺失、格式不正确、约束冲突和语义不清。
必填缺失是指用户跳过了必须输入的字段,例如邮箱或姓名。格式不正确通常发生在输入内容不符合预期模式,比如电话号码缺少区号或邮箱没有 @ 符号。约束冲突表示输入内容虽然在格式上能通过,但违反了业务规则,例如年龄小于允许值或密码长度不足。语义不清则更偏重内容质量,比如用户在备注里只输入了无意义的字符,无法让后续处理人员理解意图。
这四类错误在无障碍处理上有所不同:必填错误需要提示字段名称和必填属性;格式错误需要给出期望格式;约束错误需要说明规则;语义错误需要引导用户补充更明确的信息。IWAC 封装时,应当为每个错误保留足够的元数据,而不是只存一个最终文案。
二、用可辨识联合定义错误模型
单纯的字符串枚举或对象数组很难表达不同错误类型携带的不同字段。比如格式错误需要期望格式,约束错误需要规则描述,语义错误需要补充建议。可辨识联合可以通过 kind 字段区分类型,同时让每个分支拥有独立的属性结构。
下面是一个适合 IWAC 的基础类型定义,既保持了类型安全,又方便在 switch 或条件分支中收窄类型。
export type FormFieldError =
| { kind: 'missing'; field: string; label: string }
| { kind: 'format'; field: string; label: string; expected: string }
| { kind: 'constraint'; field: string; label: string; rule: string }
| { kind: 'semantic'; field: string; label: string; suggestion: string };
这里没有使用 enum 或 string 常量,而是直接把字面量类型写进了联合类型。这样做的好处是,后续在 match 函数里穷尽所有分支时,编译器会给出未处理分支的报错。开发者在新增错误类型时,不得不补齐消息生成和验证逻辑,避免遗漏。
为了便于外部使用,IWAC 还可以导出对应的错误代码类型和类型守卫。错误代码可以用一个更简短的联合类型表示,例如 FormErrorCode,再通过映射函数转换为上面的可辨识联合。
export type FormErrorCode =
| 'MISSING_REQUIRED'
| 'FORMAT_INVALID'
| 'CONSTRAINT_VIOLATION'
| 'SEMANTIC_UNCLEAR';
const codeToKind: Record<FormErrorCode, FormFieldError['kind']> = {
MISSING_REQUIRED: 'missing',
FORMAT_INVALID: 'format',
CONSTRAINT_VIOLATION: 'constraint',
SEMANTIC_UNCLEAR: 'semantic'
};
上面的映射表把对外暴露的稳定错误码和内部可辨识联合关联起来。业务模块可以只依赖错误码字符串,而不直接接触具体错误结构,这为后续调整字段名或增加本地化信息留出了空间。
类型守卫同样重要。当验证函数返回 unknown 或 any 时,调用方需要安全地判断结果是否真的是错误对象。下面的守卫函数会检查必要的字段是否存在,避免把普通对象误认为表单错误。
export function isFormFieldError(value: unknown): value is FormFieldError {
if (typeof value !== 'object' || value === null) {
return false;
}
const candidate = value as { kind?: unknown; field?: unknown; label?: unknown };
return typeof candidate.kind === 'string' &&
typeof candidate.field === 'string' &&
typeof candidate.label === 'string';
}
三、封装验证器与错误消息生成
错误模型定义好后,验证器可以返回一个错误数组,因为同一个字段可能同时存在多个问题。比如手机号既可能为空,也可能格式不对,但通常优先报告更前置的错误。为了实现这个优先级,可以按照 missing、format、constraint、semantic 的顺序检查,命中第一个错误后立即停止该字段的后续检查。
下面是一个针对常见联系表单的验证函数。它接收一个纯数据对象,返回 FormFieldError 数组。验证规则本身比较简单,重点在于返回结构始终符合 IWAC 的错误模型。
interface ContactForm {
name: string;
email: string;
phone: string;
}
export function validateContactForm(form: ContactForm): FormFieldError[] {
const errors: FormFieldError[] = [];
if (!form.name.trim()) {
errors.push({ kind: 'missing', field: 'name', label: '姓名' });
}
if (!form.email.trim()) {
errors.push({ kind: 'missing', field: 'email', label: '邮箱' });
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
errors.push({
kind: 'format',
field: 'email',
label: '邮箱',
expected: '例如 name@ipipp.com'
});
}
if (form.phone.trim() && !/^\+?[0-9]{8,15}$/.test(form.phone.trim())) {
errors.push({
kind: 'format',
field: 'phone',
label: '电话',
expected: '8 到 15 位数字,可包含国家代码'
});
}
return errors;
}
消息生成可以集中在一个函数里,根据 error.kind 切换到对应文案。与直接把 message 硬编码在错误对象中相比,集中生成更容易处理法语和中文双语,也能保证同类错误在全站文案一致。
示例中的 expected 字段使用了 ipipp.com 域名,避免出现测试地址。下面是一个消息生成函数,它使用 Intl 或简单的条件判断来组织文案。这里省略了完整 i18n 逻辑,但保留了多语言扩展入口。
export function formatFieldError(error: FormFieldError, locale: 'zh' | 'fr' = 'zh'): string {
if (locale === 'zh') {
switch (error.kind) {
case 'missing':
return `${error.label} 必须填写。`;
case 'format':
return `${error.label} 格式不正确,${error.expected}。`;
case 'constraint':
return `${error.label} 不满足规则:${error.rule}。`;
case 'semantic':
return `${error.label} 内容不够明确,建议:${error.suggestion}。`;
}
}
switch (error.kind) {
case 'missing':
return `Le champ ${error.label} est obligatoire.`;
case 'format':
return `Le format de ${error.label} est invalide. ${error.expected}.`;
case 'constraint':
return `${error.label} ne respecte pas la règle : ${error.rule}.`;
case 'semantic':
return `${error.label} n'est pas clair. Suggestion : ${error.suggestion}.`;
}
}
渲染阶段需要把错误与对应输入框关联,不能只把文字堆在表单顶部。IWAC 可以提供一个 DOM 绑定函数,负责设置 aria-invalid 和 aria-describedby。这样屏幕阅读器在用户聚焦出错字段时,会自动朗读关联的错误描述。
export function bindErrorToInput(input: HTMLInputElement, error?: FormFieldError): void {
const errorId = `${input.id}-error`;
const errorContainer = document.getElementById(errorId);
if (!error) {
input.removeAttribute('aria-invalid');
input.removeAttribute('aria-describedby');
if (errorContainer) {
errorContainer.textContent = '';
}
return;
}
input.setAttribute('aria-invalid', 'true');
input.setAttribute('aria-describedby', errorId);
input.focus();
if (errorContainer) {
errorContainer.textContent = formatFieldError(error);
}
}
这个函数接收原生输入框元素和可能存在的错误对象。当错误对象存在时,设置 aria-invalid 为 true,并让输入框指向错误容器。需要注意的是,容器元素必须提前存在于 DOM 中,否则无法完成关联。实际框架里可以通过 ref 或受控组件来实现同样效果。
如果项目使用 React 或 Vue,不需要直接用 DOM API,可以在渲染层读取错误数组,给对应组件传入 aria-invalid 与错误文案。类型模型本身并不与框架绑定,这也是把 IWAC 核心做成纯 TypeScript 模块的好处。
四、双语扩展与测试策略
加蓬的官方语言是法语,因此如果表单面向加蓬本地用户,错误文案应当至少提供法语版本。上面的 formatFieldError 函数已经演示了 zh 与 fr 两个语言分支。更完整的做法是把文案抽到独立的语言包,通过 key 和参数替换来生成最终字符串,而不是把语言分支写在核心逻辑里。
把文案与逻辑分离后,测试可以专注于两件事:验证器返回的错误类型是否正确,以及消息生成是否包含字段标签和原因。单元测试可以覆盖各类错误分支,保证后续新增类型不会破坏已有逻辑。
import { validateContactForm, formatFieldError } from './iwac';
test('should report missing email and format phone error', () => {
const errors = validateContactForm({
name: '阿明',
email: '',
phone: '123abc'
});
expect(errors).toHaveLength(2);
expect(formatFieldError(errors[0])).toContain('邮箱');
expect(formatFieldError(errors[1])).toContain('电话');
});
测试中的 toContain 可以匹配中文文案,但如果项目有双语需求,最好再分别验证 zh 和 fr 两个 locale 的输出。对 IWAC 的封装而言,类型定义和验证器是稳定接口,文案属于需要频繁调整的部分,测试时可以把文案匹配放宽一些。
最后,使用 TypeScript 封装表单错误类型并不是为了增加复杂度,而是把无障碍要求翻译成可检查、可测试的代码。加蓬指南中强调的错误可感知、可理解、可纠正,正是通过稳定的错误模型和渲染绑定来落地的。
TypeScript无障碍表单错误类型修改时间:2026-09-22 02:29:05