在IWAC项目里,哥伦比亚网络无障碍指南对表单的限制并不仅是字段不能为空。指南明确要求每个输入都必须有可访问名称,错误提示必须通过 aria-describedby 与字段关联,提交失败后焦点要移动到首个错误元素,并且错误摘要需要以 role=alert 或礼貌区域宣布。把这些规则写成零散的 if 判断,很容易出现标签遗漏、焦点丢失和错误消息顺序混乱。本文使用TypeScript把指南中的验证要求抽象成类型,让表单校验在编译期就能暴露大部分可访问性问题。

一、先把指南条文转成字段级类型
哥伦比亚网络无障碍指南在 WCAG 的基础上增加了对西语内容长度、错误消息朗读顺序和焦点恢复的本地化要求。例如输入框的 label 不能只靠 placeholder 代替,必填字段需要同时提供 required 和 aria-required,错误提示必须具有稳定可引用的 id。这些约束如果只存在于组件内部,类型系统无法知道某个字段是否满足指南。
第一步是定义字段的元数据类型。下面的代码把指南中对文本字段和选择字段的要求拆成 FieldA11yMetadata,并利用条件类型让 validation 的内容随字段种类变化。
type FieldKind = 'text' | 'select' | 'radio' | 'checkbox';
interface A11yTextRule {
language: 'es-CO';
minLength: number;
maxLength: number;
}
interface FieldA11yMetadata {
labelId: string;
descriptionId?: string;
errorId: string;
required: boolean;
ariaRequired: boolean;
liveRegion: 'polite' | 'assertive';
}
interface AccessibleField<T extends FieldKind> {
kind: T;
name: string;
value: unknown;
metadata: FieldA11yMetadata;
validation: T extends 'text' ? A11yTextRule : Record<string, never>;
}
这样做的好处是,当开发者给 SelectField 写入 minLength 时,TypeScript会立即报错,因为条件类型已经排除了这个属性。无障碍验证不再依赖代码评审时的人工检查,而是成为编辑器的实时提示。
type TextField = AccessibleField<'text'>;
type SelectField = AccessibleField<'select'>;
const nameField: TextField = {
kind: 'text',
name: 'fullName',
value: '',
metadata: {
labelId: 'fullName-label',
errorId: 'fullName-error',
required: true,
ariaRequired: true,
liveRegion: 'assertive',
},
validation: { language: 'es-CO', minLength: 3, maxLength: 120 },
};
二、封装可组合的验证类型与类型守卫
仅靠数据模型还不够,验证函数本身的返回值也需要结构化。布尔值 true 或 false 无法告诉界面该把焦点移到哪里,也无法生成错误摘要。因此可以定义 A11yValidationIssue 类型,把所有与无障碍相关的问题统一描述。
type ValidationSeverity = 'error' | 'warning';
interface A11yValidationIssue {
fieldName: string;
ruleId: string;
message: string;
severity: ValidationSeverity;
elementId: string;
}
type FieldValidator<TField> = (field: TField) => A11yValidationIssue[];
type ValidatorMap<TFields> = {
[K in keyof TFields]: FieldValidator<TFields[K]>;
};
ValidatorMap 通过映射类型把每个字段类型和对应的验证器绑定起来,防止把文本字段的验证器错误地用在选择框上。验证器返回数组而不是抛异常,这样多个错误可以一次性展示给屏幕阅读器用户,符合指南中对错误摘要的要求。
下面的 requiredValidator 是必填校验的简单实现。它检查必填元数据和值的实际内容,并返回带规则编号的错误对象。
const requiredValidator: FieldValidator<TextField> = (field) => {
if (field.metadata.required && String(field.value).trim() === '') {
return [{
fieldName: field.name,
ruleId: 'CO-A11Y-REQ-001',
message: `${field.metadata.labelId} 是必填字段`,
severity: 'error',
elementId: field.metadata.errorId,
}];
}
return [];
};
实际项目中不会只用一个验证器。通过 composeValidators 可以把必填、长度、焦点顺序等规则组合起来,每个验证器独立维护,最后按顺序汇总。这样既保留了单一职责,也方便为不同页面搭配不同的规则集。
function composeValidators<TField>(
...validators: FieldValidator<TField>[]
): FieldValidator<TField> {
return (field) => validators.flatMap((validate) => validate(field));
}
const textFieldValidator = composeValidators<TextField>(requiredValidator);
如果团队已经在使用 Zod 等模式校验库,也可以把 Zod schema 作为底层值校验,上面再覆盖无障碍元数据。类型系统可以保证 z.ZodType<T> 的参数与字段类型一致,避免校验器与数据模型脱节。
三、接入IWAC表单状态与错误恢复流程
验证器只有在正确的流程中被调用才有价值。IWAC表单提交后,先收集所有字段的问题,再根据问题列表决定是否阻断提交。如果存在错误,状态机进入 error 状态,并记录第一个错误元素的 id,供焦点恢复使用。
interface FormState {
status: 'idle' | 'submitting' | 'error';
fields: TextField[];
activeFieldIndex: number;
lastAnnouncementId?: string;
}
function collectIssues(fields: TextField[]): A11yValidationIssue[] {
return fields.flatMap((field) => textFieldValidator(field));
}
function focusFirstInvalidField(issues: A11yValidationIssue[], document: Document): void {
if (issues.length === 0) return;
const first = issues[0];
const target = document.getElementById(first.elementId);
target?.focus();
}
collectIssues 使用 flatMap 把所有字段的问题平铺成数组,focusFirstInvalidField 则根据第一个问题元素进行焦点移动。这里没有直接操作 DOM 的魔法字符串,因为字段元数据中的 errorId 已经和页面上的 id 形成类型级契约。
接下来是提交入口。它调用收集函数,将问题摘要通过状态返回。对于错误消息,界面可以按 issues 的顺序渲染,确保焦点顺序和朗读顺序一致。下面的 submitForm 示例省略了网络请求,只展示类型如何约束返回状态。
async function submitForm(fields: TextField[]): Promise<FormState> {
const issues = collectIssues(fields);
if (issues.length > 0) {
return {
status: 'error',
fields,
activeFieldIndex: 0,
lastAnnouncementId: issues[0].elementId,
};
}
// 模拟提交流程
await Promise.resolve();
return { status: 'idle', fields, activeFieldIndex: -1 };
}
常见的类型漏洞是把 metadata 设为可选,或者用 any 绕过条件类型约束。前者让 errorId 可能为 undefined,后者则让所有无障碍检查失去意义。建议用判别联合覆盖所有字段类型,并在 validateField 的 switch 中使用 never 检查保证穷尽。类型系统不能替代屏幕阅读器测试,但它可以减少大量低级的属性遗漏和字段规则错配问题,让开发者把精力集中在更复杂的交互场景上。
TypeScript表单验证网络无障碍指南修改时间:2026-09-26 20:21:58