表单是网页中最容易忽视无障碍细节的区域,而阿尔及利亚的IWAC网络无障碍指南(Instruction Générale sur l'Accessibilité du Web pour les Communautés)对表单控件提出了相当具体的规范:每个输入控件必须有可编程确定的无障碍名称,错误状态必须以文本形式关联到控件,分组控件必须使用语义化容器等。如果团队使用TypeScript开发组件库,完全可以把这些规范要求转化为类型约束,让不符合规范的代码在编译阶段就报错。本文将从规范解读入手,逐步完成一套面向IWAC的表单控件类型封装。

一、梳理IWAC对表单控件的核心要求
IWAC指南在很大程度上参考了WCAG 2.1的AA级标准,落到表单控件上主要有四类要求。第一是可辨识性:每个<input>、<select>、<textarea>都必须有无障碍名称,来源可以是<label>关联、aria-label或aria-labelledby。第二是错误提示的可编程关联,控件出错时必须通过aria-describedby把错误信息ID挂到控件上,而不能只靠红色边框表达。第三是分组语义,例如单选组必须包裹在fieldset与legend结构中。第四是必填与状态声明,aria-required、aria-invalid等属性需要与实际状态保持一致。
这些要求看似琐碎,但正是类型系统擅长表达的领域。TypeScript的联合类型、字面量类型、条件类型可以把“属性必须成对出现”、“某属性取值受限于枚举”这类规则直接编码进类型定义。例如aria-invalid的合法值只有true、false、grammar、spelling这几个,用字面量联合类型即可约束,写错任意一个值编译器都会立刻提示。相比依靠ESLint插件或人工Code Review,类型约束的覆盖面更稳定,也不会随着代码重构而失效。
二、封装基础类型:约束aria属性与无障碍名称
第一步是建立公共的无障碍属性类型。React对aria属性已有内置类型支持,但默认它们全部可选,我们需要做的是“收紧”其中与IWAC相关的部分。下面这段代码定义了一个AccessibleName类型,要求无障碍名称三选一强制提供:
// 无障碍名称来源:label关联、aria-label 或 aria-labelledby,三选一强制
export type AccessibleName =
| { 'aria-label': string; 'aria-labelledby'?: never; id?: never; label?: never }
| { 'aria-label'?: never; 'aria-labelledby': string; id?: never; label?: never }
| { 'aria-label'?: never; 'aria-labelledby'?: never; id: string; label: string };这段代码的关键技巧是利用never类型的互斥性。当开发者传入aria-label时,aria-labelledby和label就必须省略,从类型层面避免了多种命名方式混用导致的优先级混乱。 IWAC审查工具对重复命名方式很敏感,混用label与aria-label时辅助技术读出的名称可能不符合预期,这种问题在类型阶段拦截成本最低。
接下来处理aria-invalid的取值约束和错误关联。IWAC要求控件一旦标记为invalid,就必须能通过aria-describedby找到错误文本,这可以用条件类型表达:
// aria-invalid 取值收窄为合法字面量
export type AriaInvalid = boolean | 'grammar' | 'spelling';
// 无效状态时必须提供 aria-describedby 指向错误提示节点
export type ErrorDescribed<T> = T extends { 'aria-invalid': true | 'grammar' | 'spelling' }
? { 'aria-describedby': string }
: { 'aria-describedby'?: string };把这两个工具类型组合起来,就得到了所有IWAC表单控件Props的公共底座。任何控件只要继承这个底座,就天然带上了“有名称、错误有关联”的保证。团队中不熟悉无障碍规范的成员即使想偷懒省掉label,编辑器也会直接标红,并且提示信息会准确指出缺少哪个属性。
三、构建泛型表单控件组件类型
底座类型就绪后,可以开始封装具体控件。以文本输入控件为例,除了基础属性外,还需要支持受控与非受控两种模式,同时保持无障碍约束不被稀释。一个实用的做法是用泛型参数分离值类型,用交叉类型叠加无障碍底座:
export interface TextFieldProps<T extends string = string>
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'aria-label' | 'aria-labelledby'> {
// 受控模式:value 与 onChange 成对出现
value?: T;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
// 必填状态:IWAC 要求 aria-required 与视觉星号并存
required?: boolean;
// 提示文本,自动渲染并挂到 aria-describedby
helperText?: string;
// 错误文本,存在时自动设置 aria-invalid 并合并描述ID
errorText?: string;
// 无障碍命名(三选一强制)
name?: AccessibleName;
}这里有一个容易踩的坑:直接extends原生InputHTMLAttributes会把aria-label的类型放宽回可选any式宽松状态,因此必须先用Omit剔除原生属性,再叠加自己的AccessibeName约束。同理,aria-describedby也应该剔除,因为errorText存在时组件内部会自动生成描述节点与ID,外部传入反而会破坏关联关系。这种“内部管理、外部禁止”的设计能让组件的无障碍行为保持一致。
对于单选组这类必须使用fieldset与legend结构的控件,类型层面同样可以强制。定义RadioGroupProps时把legend设为必填属性,组件内部固定渲染fieldset包裹结构,开发者无法绕过语义化容器:
export interface RadioGroupProps {
// IWAC 要求分组控件必须有 legend 描述组用途
legend: string;
name: string;
options: ReadonlyArray<{ value: string; label: string; disabled?: boolean }>;
value?: string;
onChange?: (value: string) => void;
required?: boolean;
errorText?: string;
}这种封装的好处是规范知识被固化在组件内部。后续即使团队人员流动,新成员使用这些组件时也不需要通读IWAC文档,只要按类型提示填齐属性,产出的表单就自动符合阿尔及利亚无障碍法规的要求。
四、用类型守卫与工具函数辅助运行时校验
类型约束只在编译期生效,如果要对接CI中的自动审查,还需要运行时的类型守卫。可以把IWAC规则写成可复用的断言函数,与类型定义保持同一套常量来源:
const VALID_ARIA_INVALID = [true, false, 'grammar', 'spelling'] as const;
export function isValidAriaInvalid(v: unknown): v is AriaInvalid {
return VALID_ARIA_INVALID.includes(v as any);
}
// 检查控件是否具备可编程确定的无障碍名称
export function hasAccessibleName(el: HTMLElement): boolean {
const labelId = el.getAttribute('aria-labelledby');
if (labelId) return labelId.split(/\s+/).every(id => document.getElementById(id) !== null);
if (el.getAttribute('aria-label')) return true;
if (el.id) {
return !!document.querySelector(`label[for="${el.id}"]`);
}
return false;
}这类守卫函数可以挂到测试用例中,对渲染出的DOM做批量断言,形成“类型层拦截加测试层兜底”的双保险。特别是当表单由低代码平台或JSON Schema动态生成时,静态类型无法覆盖所有分支,运行时校验就成了最后一道防线。将VALID_ARIA_INVALID声明为as const的元组并让类型与值共用同一个来源,还能避免规范更新时类型定义与校验逻辑不同步的问题。
五、实践建议与注意事项
落地这套方案时有几点经验值得注意。首先,类型约束要循序渐进,可以先在公共底座上启用最关键的名称与错误关联约束,等团队适应后再收紧其他属性,一次性收紧所有规则容易引发大面积编译报错,打击 adoption积极性。其次,Omit原生属性时务必检查TypeScript版本对属性剔除的行为差异,某些版本下Omit对索引签名属性的处理会导致意外的属性丢失。最后,建议把这套类型封装发布为独立的类型包,与组件实现解耦,这样即使某些页面使用Vue或原生JS开发,也能复用同一份类型定义作为参考规范。
总体来看,用TypeScript封装IWAC表单控件类型的核心思路是“把法规条文翻译成类型规则”。字面量联合约束枚举取值,条件类型表达属性依赖,never互斥防止命名冲突,泛型保证值类型安全。这些手段组合起来,能让无障碍合规从事后补救变成开发阶段的自然产物,也为后续对接阿尔及利亚官方的无障碍审计打下了扎实基础。
TypeScriptIWAC表单控件类型修改时间:2026-09-07 04:20:40