给IWAC组件体系补上无障碍能力时,最先遇到的往往不是交互逻辑,而是ARIA属性定义过于松散。项目里可能已经有人写了 interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> 或者更省事的 Record<string, string>,但这些类型对巴巴多斯网络无障碍指南中规定的取值完全没有约束力。例如 aria-current 在指南里只允许 page、step、location、date、time、true 和 false,但 Record<string, string> 会放过 aria-current="pag" 这样的拼写错误。TypeScript 的类型系统正好可以把这些取值规则从文档搬到编译器,让错误在开发阶段暴露。

一、宽松类型为什么会让ARIA属性形同虚设
很多组件库为了快速支持所有HTML属性,会直接把原生属性类型或索引签名透传。例如 type ButtonProps = React.HTMLAttributes<HTMLButtonElement> & { variant?: 'primary' | 'secondary' }。这样虽然能获得自动补全,但补全列表包含所有原生HTML属性,ARIA属性只是其中一部分,且取值都收窄成string,无法区分合法与非法。更糟的是,某些自定义封装会使用 [key: string]: any,这时连属性名是否存在都检查不了。开发者写着 aria-hidden="yes" 也能通过编译,但屏幕阅读器根本不会按预期处理,因为布尔型ARIA属性的合法值是 true 和 false,而非 yes。
巴巴多斯网络无障碍指南对常见ARIA角色和属性做了进一步整理,特别强调政务、金融类系统的键盘可达性和状态提示。它把 aria-current 的取值限定为明确的导航节点类型,把 aria-expanded 限制为可折叠区域,把 aria-haspopup 的取值分成 menu、listbox、tree、grid、dialog 等不同弹层语义。如果类型定义中只是把这些写成 string,相当于放弃了指南中最有价值的部分——属性值语义。所以需要为每个高频ARIA属性建立精确的字面量联合类型。
// 宽松写法:任何字符串都能通过
type LooseProps = {
'aria-current'?: string;
'aria-expanded'?: string;
'aria-haspopup'?: string;
};
const bad: LooseProps = {
'aria-current': 'pag', // 本应是 'page',拼写错误无法被捕获
'aria-expanded': 'maybe', // 本应只接受 true/false
};
二、用字面量联合和映射类型构建AriaProps
先根据指南把ARIA属性分成几类:布尔型、枚举型、ID引用型、整型。布尔型如 aria-hidden、aria-expanded 可以定义为 boolean | 'true' | 'false',因为原生JSX允许直接传布尔值,也有代码会传字符串字面量;枚举型如 aria-current、aria-haspopup 用联合类型;ID引用型如 aria-controls 至少需要 string,但如果希望更强约束可以配合模板字面量类型限制为 `#${string}` 或更细的ID格式,不过一般保持 string 可读性更好。这类属性的核心是名称要精确,不让不存在的属性混入。
可以定义一个 AriaMap 接口,把属性名作为键,取值类型作为值,然后通过 keyof 和映射类型生成最终 AriaProps。这样做的好处是:以后新增ARIA属性只需修改 AriaMap,所有继承的类型自动同步。示例如下:
type AriaCurrent = 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false';
type AriaHasPopup = 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false';
type AriaBoolean = boolean | 'true' | 'false';
interface AriaMap {
'aria-current': AriaCurrent;
'aria-expanded': AriaBoolean;
'aria-haspopup': AriaHasPopup;
'aria-hidden': AriaBoolean;
'aria-label': string;
'aria-labelledby': string;
'aria-describedby': string;
'aria-controls': string;
}
type AriaProps = {
[K in keyof AriaMap]?: AriaMap[K];
};
这个 AriaProps 类型只接受 AriaMap 中列出的属性名,属性值的类型也被收窄。比如写 aria-current="pag" 会直接提示 Type 'pag' is not assignable to type AriaCurrent。相比 React.AllHTMLAttributes 那种把所有ARIA都揉进大接口的做法,这种映射类型更利于按项目指南裁剪。巴巴多斯指南可能只要求一部分ARIA属性,没必要把WAI-ARIA全套都放进来,避免补全时出现大量用不上的选项。
三、在IWAC组件中接入AriaProps并验证编译
IWAC如果是内部组件库,通常每个基础组件都会定义自己的props。以按钮为例,可以这样继承: interface IWACButtonProps extends AriaProps { variant?: 'primary' | 'secondary'; disabled?: boolean; }。组件内部把ARIA属性从props中解构出来,再传给原生 <button> 元素。为了避免 aria-hidden 与 disabled 等产生冲突,可以在组件层增加运行时警告,但类型层已经能防止拼错属性名。示例代码:
interface IWACButtonProps extends AriaProps {
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
function IWACButton({ variant = 'primary', disabled, ...ariaProps }: IWACButtonProps) {
return (
<button
className={`iwac-button iwac-button--${variant}`}
disabled={disabled}
{...ariaProps}
/>
);
}
使用组件时,TypeScript 会检查传入的ARIA属性。假设有人写 <IWACButton aria-current="pge" />,编辑器会在编译前标红,提示 pge 不在 page、step 等合法值内。这种反馈比代码评审时口头提醒更可靠,也让无障碍规范从口头约定变成代码的一部分。
另一个好处是自动补全。当开发者输入 aria- 时,只会出现 AriaMap 中定义过的属性,不会出现 aria-required 这种指南未纳入或已废弃的属性。对于大型团队,这能显著减少审阅时对无障碍属性的争论。可以在CI中增加 tsc --noEmit 检查,任何非法ARIA赋值都会使构建失败,而不是等到QA用读屏软件测试才发现问题。
四、处理复杂约束与自定义扩展
ARIA属性之间并非完全独立。例如 aria-haspopup="dialog" 通常需要配合 aria-expanded 和 aria-controls 使用,菜单组件可能还要限制 role 为 menu 或 menuitem。对于这种组合约束,可以用判别联合来建模。比如定义 type MenuAriaState = { role: 'menu'; 'aria-haspopup'?: 'menu' | 'true'; 'aria-expanded'?: AriaBoolean },但要注意与通用 AriaProps 的兼容。更实际的做法是保持 AriaProps 作为基础层,在具体组件使用交叉类型补充角色约束。代码示例:
type MenuRole = 'menu' | 'menuitem' | 'menuitemcheckbox' | 'menuitemradio';
interface IWACMenuProps extends AriaProps {
role?: MenuRole;
'aria-haspopup'?: 'menu' | 'true';
'aria-expanded'?: AriaBoolean;
}
function IWACMenu({ role = 'menu', ...ariaProps }: IWACMenuProps) {
return (
<div role={role} {...ariaProps} />
);
}
当本地指南需要扩展一些特殊属性,比如政务系统要求的 aria-process-state,可以声明一个额外的映射类型并交叉。示例如下:
interface LocalAriaMap extends AriaMap {
'aria-process-state': 'idle' | 'loading' | 'success' | 'error';
}
type LocalAriaProps = {
[K in keyof LocalAriaMap]?: LocalAriaMap[K];
};
这种扩展不会破坏原有组件的类型,新旧组件可以逐步迁移。最后要强调,类型定义只是无障碍工程的一部分。它能减少拼写和非法取值,但不能保证屏幕阅读器实际体验,也无法替代键盘导航测试。要把巴巴多斯网络无障碍指南中的检查项做成类型、lint规则和测试用例三层防护:类型管属性值与属性名,lint规则管必填属性组合,e2e测试管真实辅助技术行为。这样IWAC组件库才能在无障碍支持上站得住脚。
TypeScriptARIA属性IWAC修改时间:2026-09-22 05:12:34