英国网络无障碍指南对链接文本有一条明确要求:链接的目的应当能够从链接文本本身或者其直接上下文中判断出来。屏幕阅读器用户经常通过列表形式浏览页面上的全部链接,此时连续的“了解更多”“查看详情”“点击这里”会完全失去导航意义。移动端和键盘导航用户同样依赖清晰的链接文本决定是否跳转。在TypeScript项目中,这种原本依赖人工审查的软约束,可以通过类型系统变成编译期的硬约束。IWAC(Inclusive Web Accessibility Components)是一套面向无障碍场景的组件库,本文围绕如何为它封装英国指南中的链接目的类型展开。

为什么需要把链接目的放入类型系统
链接目的规则通常写在设计文档或检查清单里,例如“链接文本必须能独立表达目的”。但清单只能事后检查,无法阻止开发者写出模糊文本。类型系统的价值在于提前限制输入空间。TypeScript的字符串字面量、判别联合和条件类型可以描述“哪些文本是允许的,哪些是禁止的”,并让IDE在编码时给出提示。这样做不是替代语义理解,而是缩小出错范围。比如当链接目的是下载文档时,文本应包含动作“下载”和文档标题,而不是只有“下载”两个字;当链接目的是跳转到其他页面时,文本应包含目标页面名称。通过结构化建模,可以让这些规则成为组件API的一部分。
英国指南的具体要求不仅适用于静态页面,也适用于单页应用、表格操作、文件下载等场景。对于IWAC来说,链接组件被大量复用,一旦类型封装不合理,违规文本就会在系统各处传播。因此,把链接目的建模为可判别的数据结构,而不是普通字符串,是设计上的关键一步。这样所有使用IWAC链接的开发者都被迫显式表达链接目的,减少默认值或自由文本带来的风险。
用判别联合与模板字面量类型建模链接目的
链接目的可以从多个维度描述:下载文档、触发操作、页面导航、打开外部站点。每种目的具有不同字段。判别联合适合表达这种互斥结构。核心类型可以这样定义:
export type LinkPurpose =
| { kind: 'document'; title: string; format?: 'PDF' | 'DOCX' | 'XLSX' }
| { kind: 'action'; verb: '申请' | '查看' | '编辑' | '提交'; object: string }
| { kind: 'navigation'; destination: string }
| { kind: 'external'; label: string; domain: string };
随后可以根据判别联合生成符合指南的链接文本,例如:
function getPurposeText(purpose: LinkPurpose): string {
switch (purpose.kind) {
case 'document':
return `下载${purpose.title}(${purpose.format ?? 'PDF'})`;
case 'action':
return `${purpose.verb}${purpose.object}`;
case 'navigation':
return `前往${purpose.destination}`;
case 'external':
return `${purpose.label}(${purpose.domain})`;
}
}
不过,如果object允许任意字符串,开发者仍然可能写入“详情”“这里”等模糊文本。此时可以借助条件类型进一步约束。虽然TypeScript不能完全理解中文语义,但可以禁止一组明确的禁用词:
type Forbidden = '更多' | '这里' | '详情' | '点击';
type SafeText<T extends string> = T extends Forbidden ? never : T;
function defineActionPurpose<T extends string>(verb: '申请' | '查看' | '编辑', object: SafeText<T>) {
return { kind: 'action' as const, verb, object };
}
defineActionPurpose('查看', '详情'); // 类型错误
defineActionPurpose('查看', '申请记录'); // 通过
这种做法的好处是错误会直接出现在编辑器中,而不是等到无障碍测试阶段。需要注意的是,开放文本仍然无法被完全验证,但禁用词列表可以覆盖大部分常见违规写法,并且在IWAC内部统一维护,后续还能扩充。
在IWAC组件中落地类型封装
如果只提供类型定义,开发者仍然可以在业务代码里绕过约束。更好的做法是让IWAC链接组件本身只接收purpose对象,不提供自由label字段。这样组件内部负责生成可访问文本,外部使用时无法传入模糊文案:
interface IWACLinkProps {
href: string;
purpose: LinkPurpose;
target?: '_self' | '_blank';
}
export function IWACLink({ href, purpose, target = '_self' }: IWACLinkProps) {
const text = getPurposeText(purpose);
const externalProps = purpose.kind === 'external'
? { rel: 'noopener noreferrer' }
: {};
return (
<a href={href} target={target} {...externalProps}>{text}</a>
);
}
通过这个API,开发者不能写<IWACLink href="/doc" label="点击这里" />,因为IWACLinkProps上根本没有label字段。只能传入结构化的purpose对象。团队可以根据英国指南扩充LinkPurpose的判别分支,例如增加“电话拨打”“邮件发送”等目的,而无需修改组件渲染逻辑。
如果链接文本需要与上下文关联,比如表格中每一行都有“编辑”,单独看“编辑”不够清晰,但整行信息提供了上下文。英国指南允许这种情况,但需要确保程序化可判定性。类型可以扩展为上下文相关分支,要求提供contextLabel:
type LinkPurposeWithContext =
| { kind: 'self-contained'; text: string }
| { kind: 'context-dependent'; text: string; contextLabel: string };
function renderAccessibleText(purpose: LinkPurposeWithContext) {
if (purpose.kind === 'context-dependent') {
return (
<>
{purpose.text}
<span className="visually-hidden">{purpose.contextLabel}</span>
</>
);
}
return purpose.text;
}
这样既保留了视觉上的简洁文本,又为屏幕阅读器补充了必要的上下文信息。类型要求开发者在选择上下文相关目的时必须提供contextLabel,避免遗漏。
实践中需要注意的边界
类型系统不是语义理解,无法判断object字符串是否真正描述目标。比如object: '这个页面'可能通过类型检查,但语义仍然模糊。因此类型封装是基础,不是万能。还需要配合代码审查、自动化测试和真实用户测试。可以进一步结合ESLint插件扫描代码中的<a>标签文本,但IWAC类型封装已经让大部分简单错误在编译期被拦截。团队可以在CI中增加tsc检查,确保类型错误阻断合并。
英国网络无障碍指南可能随标准更新而调整,类型定义要集中维护,避免散落在各业务代码里。IWAC建议把LinkPurpose和getPurposeText放在单一模块中,业务只依赖导出的类型和组件。当指南新增要求时,只需修改该模块,例如为external链接增加“是否打开新窗口”的提示,所有使用方自动获得新的文本规则。
如果项目已有大量自由文本链接,迁移可以渐进进行。先为新建的链接组件启用purpose类型,旧链接保留原样或通过ESLint警告逐步替换。一次性强制所有链接符合类型约束可能造成较大改动,但长期看,编译期约束比人工检查更节约成本,也能让无障碍要求成为开发流程的默认部分。

