链接目的在无障碍指南中并不是一个模糊的概念,它直接关系到用户能否在进入链接前判断这个链接会把自己带到哪里。屏幕阅读器用户在浏览页面时,往往会把链接列表单独提取出来,如果链接文本只是单纯的“点击这里”或“查看更多”,脱离上下文后就没有任何信息量。IWAC组件库如果要把这条规则真正落到代码里,就不能只靠文档约定或代码审查,而需要在TypeScript类型层面对链接目的做出约束。

为什么链接目的需要专门的类型体系
链接目的可以从两个维度理解。第一个维度是链接本身要表达的动作,比如打开文档、跳转外部站点、触发弹窗、下载文件或展开内容。第二个维度是链接所处的上下文,比如面包屑、导航区、正文内容区或页脚。两者共同决定了一个链接是否满足可访问性要求。如果只是在组件里写一个label: string,开发者可以传入空字符串,也可以传入“详情”这种脱离上下文后无法判断目的的文本,类型系统无法提前拦截这些问题。
通过定义一组字符串字面量联合类型,可以强制调用方在传入链接信息时选择已经归纳好的目的类别。比如链接目的是下载文件还是打开外部页面,从类型上就能区分开。这样不仅能在开发阶段减少歧义,也能让无障碍审计更容易追溯。更重要的是,当后续指南要求补充新的链接目的类型时,只需要扩展联合类型,所有使用该类型的调用点都会自动获得编译期提示,不会出现散落在各处的魔法字符串。
另一个容易忽略的点是,链接目的的类型定义不能只停留在string层面。如果只用purpose: string,那么任何文本都能通过编译,类型约束就形同虚设。使用字面量联合类型以后,purpose字段的可选值被限制在一个明确的集合内。这种设计还能带来更准确的自动补全,开发者不需要记忆某个含义晦涩的枚举值,IDE会直接把可选项展示出来。
设计可扩展的TypeScript类型定义
先定义链接所处的上下文类型。上下文可以分成导航、正文、页脚、面包屑等常见区域。对于面包屑链接,目的通常是返回上一级,对于页脚链接,可能是打开法律声明或联系方式。将上下文固定为字面量联合类型,可以避免调用方传入“top”、“main”这类含义不够稳定的值。
type LinkContext = | 'navigation' | 'content' | 'footer' | 'breadcrumb' | 'aside'; type LinkPurposeType = | 'opens-document' | 'opens-external' | 'opens-mail' | 'downloads-file' | 'navigates-to-anchor' | 'expands-content' | 'opens-dialog' | 'same-page-action';
上面这组类型只是基础层,它回答了“链接在什么区域做什么事”。但仅有这些字段还不够,因为一个“打开外部页面”的链接还需要明确目标地址和可访问名称。可访问名称通常来自链接内部文本,但当文本不足时,需要通过aria-label或title补充。把这类信息放到同一个接口里,可以保证调用方一次性提供完整的语义。
进一步可以用判别联合来区分不同目的下的附加字段。比如下载文件需要文件类型,外部链接需要域名,弹窗或展开动作需要说明触发后的行为。这样每个链接目的都会关联到自己的数据形状,而不是所有字段都可有可无。
interface LinkBaseSpec {
context: LinkContext;
accessibleName: string;
target: string;
}
interface DocumentLinkSpec extends LinkBaseSpec {
purpose: 'opens-document' | 'downloads-file';
fileType: 'pdf' | 'doc' | 'xls' | 'zip';
}
interface ExternalLinkSpec extends LinkBaseSpec {
purpose: 'opens-external' | 'opens-mail';
domain?: string;
sameWindow?: boolean;
}
interface ActionLinkSpec extends LinkBaseSpec {
purpose: 'navigates-to-anchor' | 'expands-content' | 'opens-dialog' | 'same-page-action';
actionHint: string;
}
type LinkPurposeSpec = DocumentLinkSpec | ExternalLinkSpec | ActionLinkSpec;
这种判别联合的好处在于,当开发者声明一个purpose: 'downloads-file'的链接时,TypeScript会要求同时提供fileType,否则无法通过编译。反过来,当purpose是opens-external时,fileType就不应该出现。这样链接目的的类型定义就从简单枚举升级成了携带结构信息的模型,减少了很多运行时才发现的数据缺失问题。
在IWAC内部,这些类型还需要和渲染层解耦。渲染函数只关心最终解析好的结构,而类型定义负责保证输入结构正确。可以用一个ResolvedLinkPurpose表示已经通过校验、可以直接交给模板使用的链接数据,这样业务层不需要再重复判断字段是否存在。
在IWAC中封装和校验链接目的
类型定义本身不会在运行时产生任何检查,因此还需要一组工具函数来做实际校验。类型守卫可以把未知来源的数据收窄为LinkPurposeSpec,这样从后端返回的对象、用户配置或测试数据在进入组件之前都能得到验证。校验逻辑应当足够明确,不能只判断字段是否存在,还要检查值是否落在允许的字面量集合内。
const contextSet = new Set<LinkContext>([
'navigation',
'content',
'footer',
'breadcrumb',
'aside'
]);
function isLinkContext(value: unknown): value is LinkContext {
return typeof value === 'string' && contextSet.has(value as LinkContext);
}
function isLinkPurposeSpec(value: unknown): value is LinkPurposeSpec {
if (typeof value !== 'object' || value === null) return false;
const spec = value as Partial<LinkPurposeSpec>;
if (!isLinkContext(spec.context)) return false;
if (typeof spec.accessibleName !== 'string' || spec.accessibleName.trim() === '') return false;
if (typeof spec.target !== 'string') return false;
if (spec.purpose === 'downloads-file' || spec.purpose === 'opens-document') {
return spec.fileType === 'pdf' || spec.fileType === 'doc' || spec.fileType === 'xls' || spec.fileType === 'zip';
}
if (spec.purpose === 'opens-external' || spec.purpose === 'opens-mail') {
return typeof spec.sameWindow === 'undefined' || typeof spec.sameWindow === 'boolean';
}
if (spec.purpose === 'expands-content' || spec.purpose === 'opens-dialog' || spec.purpose === 'same-page-action') {
return typeof spec.actionHint === 'string' && spec.actionHint.trim() !== '';
}
return false;
}
这段代码中的<LinkContext>和<LinkPurposeSpec>是泛型参数,在TypeScript源码里必须使用尖括号。上例已经将尖括号转义,浏览器能够正确显示,复制到.ts文件时也需要恢复为普通尖括号。这类细节在编写技术文档时很容易出错,但如果不加处理,页面渲染时会把泛型当作HTML标签解释。
除了类型守卫,IWAC还可以导出一个工厂函数,用来创建符合无障碍要求的链接配置。这个函数只接受LinkPurposeSpec,内部补齐默认行为,并返回一个冻结后的对象,防止运行时被意外修改。这种封装方式把规则集中在一个入口,其他模块无需感知具体的链接目的细节。
function createIwaCLink(spec: LinkPurposeSpec): Readonly<LinkPurposeSpec> {
if (!isLinkPurposeSpec(spec)) {
throw new Error('Invalid link purpose spec: ' + JSON.stringify(spec));
}
return Object.freeze({ ...spec });
}
const downloadSpec = createIwaCLink({
context: 'content',
purpose: 'downloads-file',
accessibleName: '下载2024年度报告',
target: '/files/annual-report.pdf',
fileType: 'pdf'
});
示例中的链接文本写明了文件内容,而且fileType明确为pdf,这样屏幕阅读器用户可以预知点击后会下载PDF文件。即使链接文本本身包含了“下载”和“年度报告”,仍然建议在accessibleName中提供完整描述,因为有些浏览器会截断较长的链接文本。
当IWAC的链接组件接收到LinkPurposeSpec之后,渲染模板可以依据purpose字段选择不同的标签属性和提示图标。例如opens-external可以在链接后追加“新窗口打开”的视觉提示,downloads-file可以显示文件类型标签。这些视觉提示不能替代文本描述,但可以作为辅助信息增强可感知性。渲染层只需要一次switch,无需在多个组件中重复判断字符串。
最终,这套类型定义和封装带来的收益是双重的。一方面,开发者面对编辑器提示时,能更快判断一个链接需要补充哪些无障碍信息;另一方面,审计工具或单元测试可以利用类型守卫批量检查已有配置,发现不符合指南的链接声明。相比在页面完成后逐个检查HTML中的<a>标签,把校验前置到数据层和类型层,成本更低,反馈也更快。
TypeScriptIWAC链接目的类型定义修改时间:2026-10-06 13:41:28