表单错误提示看似只是几行文案,但在无障碍场景下,它牵涉到语义关联、焦点管理、屏幕阅读器播报等一系列细节。澳大利亚的网络无障碍指南以WCAG为核心标准,其中对错误提示有明确要求:错误必须以文字描述、必须能被程序化确定、必须与对应的输入元素建立关联。如果在IWAC这类组件化项目中直接用字符串散落各处传递错误,很容易出现关联缺失、文案不规范的问题。用TypeScript把错误封装成结构化类型,是让这些问题在编译期就暴露出来的有效手段。

一、先明确指南对表单错误的核心要求
WCAG的相关条款可以归纳为三个层面。第一,错误识别:错误必须以文本形式呈现,不能只靠红色边框或图标颜色来传达,否则色觉障碍用户无法感知。第二,错误关联:错误提示需要通过aria-describedby或<label>等机制与输入控件关联,屏幕阅读器聚焦到输入框时应能读出错误内容。第三,错误修正指引:提示要说明问题所在以及如何修正,例如"请输入有效的手机号,格式为04XX XXX XXX"。
对照这些要求,可以提炼出错误类型需要承载的信息:错误码、所属字段、面向用户的修正建议、机器可读的元数据。值得注意的是,错误码与文案必须分离存储,文案渲染交给展示层,这样才能支持多语言并保证文案统一维护。
二、设计类型化的错误模型
核心思路是用可辨识联合来定义错误类型,每个错误变体携带自己特有的元数据,避免用一个松散的大对象装下所有可能的字段。先定义错误码枚举和基础结构:
// 错误码集中定义,与后端或校验层保持一致
export const FormErrorCode = {
Required: 'required',
InvalidFormat: 'invalid_format',
TooShort: 'too_short',
Custom: 'custom',
} as const;
export type FormErrorCode = typeof FormErrorCode[keyof typeof FormErrorCode];
// 每种错误携带各自不同的元数据
export interface RequiredError {
code: typeof FormErrorCode.Required;
field: string;
}
export interface InvalidFormatError {
code: typeof FormErrorCode.InvalidFormat;
field: string;
/** 给出格式示例,帮助用户修正 */
expectedFormat: string;
}
export interface TooShortError {
code: typeof FormErrorCode.TooShort;
field: string;
minLength: number;
actualLength: number;
}
export type FieldError = RequiredError | InvalidFormatError | TooShortError;
// 一个表单的错误集合:字段名到错误的映射
export type FormErrors<TFields extends string = string> = Partial<Record<TFields, FieldError>>;这样设计的好处是显而易见的。当拿到一个FieldError时,TypeScript会根据code字段自动收窄类型,比如收窄到TooShortError后就能安全访问minLength,访问不存在的属性会直接报编译错误。泛型TFields让FormErrors只能以合法的字段名作为键,拼写错误立刻被发现。
三、用类型守卫与文案映射实现无障碍渲染
错误模型确定后,下一步是把结构化错误转换为符合指南的用户文案,并自动生成无障碍属性。这里的关键是文案生成函数必须覆盖所有错误变体,利用switch对联合类型的穷尽性检查,新增错误码时漏写文案会直接编译失败:
export function describeError(error: FieldError): string {
switch (error.code) {
case 'required':
return '此项为必填项,请填写后再提交。';
case 'invalid_format':
return `格式不正确,正确示例:${error.expectedFormat}`;
case 'too_short':
return `内容过短,至少需要${error.minLength}个字符,当前${error.actualLength}个。`;
default: {
// 穷尽性检查:漏写分支时这里会编译报错
const _exhaustive: never = error;
return '输入有误,请检查后重试。';
}
}
}
/** 生成需要挂到输入控件上的无障碍属性 */
export function errorAriaAttrs(
fieldId: string,
error: FieldError | undefined
): Record<string, string> {
if (!error) return {};
const errorId = `${fieldId}-error`;
return {
'aria-invalid': 'true',
'aria-describedby': errorId,
};
}渲染层拿到这些属性后,将错误文案输出到一个带role="alert"的元素中并赋予对应的id,屏幕阅读器在错误出现时就会主动播报,聚焦输入框时也会朗读关联的提示。这种把无障碍属性的计算收敛到类型安全函数中的做法,避免了在各个组件里手写字符串拼接导致的遗漏。
四、在IWAC组件中落地与测试
落地时建议把上述类型与工具函数抽成独立模块,再提供一个泛型的高阶工具把字段定义、校验规则和错误集合并起来:
export interface FormConfig<TFields extends string> {
fields: Record<TFields, { id: string; label: string }>;
validate: (values: Record<TFields, string>) => FormErrors<TFields>;
}
export function createAccessibleForm<TFields extends string>(config: FormConfig<TFields>) {
let errors: FormErrors<TFields> = {};
return {
submit(values: Record<TFields, string>): FormErrors<TFields> {
errors = config.validate(values);
return errors;
},
getErrors(): FormErrors<TFields> {
return errors;
},
};
}在测试环节,除了常规的单元测试,还应补充两类针对无障碍的断言:一是校验函数返回的错误对象一定包含可生成完整文案的元数据,可以用类型层面的断言配合运行时抽样验证;二是对渲染结果做DOM断言,确认出现错误时输入控件上确实带有aria-invalid和aria-describedby,且对应id的元素真实存在。有条件的团队可以再接入axe这类自动化扫描工具,把WCAG合规检查纳入持续集成流程。
整体来看,用TypeScript封装表单错误类型的价值在于把无透明度的规范要求转化为类型约束:错误码穷尽性保证了文案完整,泛型字段映射杜绝了拼写错误,集中的无障碍属性生成避免了关联遗漏。规范要求不再依赖开发者的记性,而是由编译器替你把关。
TypeScriptIWAC网络无障碍修改时间:2026-09-06 20:26:34