在无障碍合规实践中,链接目的(link purpose)是判断可访问名称是否有效的关键依据。希腊网络无障碍指南沿用了WCAG 2.x中关于链接目的在上下文中可确定的成功标准,要求链接文本、图像替代文本或二者组合能够明确表达交互目标。用TypeScript封装这些类型时,不能只定义一个字符串联合类型;还需要把链接目的与链接文本、上下文信息关联起来,形成可推导、可校验的模型。本文以GWAC(Greek Web Accessibility Compliance)项目为背景,给出可落地的类型定义方案。

链接目的在希腊指南中的约束来源
希腊网络无障碍指南并未脱离WCAG的框架另起炉灶,而是在成功标准2.4.4 Link Purpose (In Context) 的基础上,增加了对希腊语语义和本地化场景的补充说明。该标准要求链接目的能够从链接文本本身、链接所在句子、列表项、表头或可编程确定的关联信息中推导出来。如果链接文本模糊,比如只写了“点击这里”,就必须依赖上下文或额外的程序化标记来弥补。这意味着类型定义不能只停留在“文本”层面,必须显式区分目的类别和上下文信息。
常见的链接目的可以归纳为导航、操作、内容展示和下载四类。导航类链接用于在站内或页面锚点间移动,操作类链接触发提交、取消、删除等动作,内容类链接指向文章、附件或外部资源,下载类链接则额外需要媒体类型信息。这些类别在希腊语环境下可能对应不同的动词和名词,例如希腊语的“περισσότερα”(更多)在无障碍上下文中往往需要配合隐藏的上下文说明才能明确目的。因此,类型建模的第一步是建立基础的分类联合类型。
type LinkPurposeCategory = 'navigation' | 'action' | 'content' | 'download';
这个基础联合类型只是起点。实际封装时,不同类别的链接目的携带的元数据差异很大,如果只用一个字符串字段表示目的,就无法约束各字段之间的关系。比如下载链接需要MIME类型,而导航链接需要目标锚点或路径。这就要求类型系统能够根据类别收窄字段,并让非法组合在编译期直接报错。
用TypeScript建模链接目的类型
单纯的字符串联合类型无法表达“导航类链接必须有target字段、下载类链接必须有mimeType字段”这类约束。更合理的做法是使用判别式联合(discriminated union),让每个链接目的对象包含一个category字段作为判别依据,然后为每个类别定义独立的接口。这样,TypeScript不仅能够检查category的取值,还能在switch或if分支中自动收窄到具体类型。
interface LinkPurposeBase {
label: string;
context?: string;
}
interface NavigationLinkPurpose extends LinkPurposeBase {
category: 'navigation';
target: 'home' | 'section' | 'breadcrumb';
}
interface ActionLinkPurpose extends LinkPurposeBase {
category: 'action';
action: 'submit' | 'cancel' | 'delete';
}
interface ContentLinkPurpose extends LinkPurposeBase {
category: 'content';
contentType: 'article' | 'attachment' | 'external';
}
interface DownloadLinkPurpose extends LinkPurposeBase {
category: 'download';
mimeType: string;
}
type LinkPurpose = NavigationLinkPurpose | ActionLinkPurpose | ContentLinkPurpose | DownloadLinkPurpose;
在这个模型中,label字段保存链接的可访问名称,context字段保存从上下文中提取的辅助信息,category字段决定对象属于哪个具体类型。base接口中的label是所有链接目的都必需的,因为即便上下文再清晰,也需要一个可编程确定的名称用于屏幕阅读器读取。context字段可选,因为某些链接自身文本已经足够明确,不需要额外补充。
为了让使用方能够方便地区分类型,还需要提供类型守卫函数。这些函数在运行时判断category值,帮助TypeScript在条件分支中收窄类型。如果直接访问target或mimeType字段而不先收窄类型,编译器会报错,从而避免运行时出现undefined相关的问题。
function isNavigationLinkPurpose(purpose: LinkPurpose): purpose is NavigationLinkPurpose {
return purpose.category === 'navigation';
}
function isActionLinkPurpose(purpose: LinkPurpose): purpose is ActionLinkPurpose {
return purpose.category === 'action';
}
function isDownloadLinkPurpose(purpose: LinkPurpose): purpose is DownloadLinkPurpose {
return purpose.category === 'download';
}
类型守卫虽然简单,但在组件内部很有用。例如一个链接组件需要根据目的类别决定是否渲染下载图标或添加download属性,就可以先调用isDownloadLinkPurpose判断,再安全地读取mimeType字段。这种做法比使用可选字段加非空断言更稳健,也让代码意图更加清晰。
运行时校验与错误提示
TypeScript的类型检查只在编译期生效,而从API响应、配置文件或用户输入中获取的数据在运行时并不受类型系统保护。如果数据来自外部系统,就可能出现category为navigation但缺少target字段的对象。为了让无障碍组件在运行时也能保持数据质量,需要补上运行时校验逻辑。校验函数接收unknown类型的值,先做基础对象判断,再根据category逐个验证必填字段。
function assertLinkPurpose(value: unknown): asserts value is LinkPurpose {
if (typeof value !== 'object' || value === null) {
throw new Error('Link purpose must be an object');
}
const candidate = value as Record<string, unknown>;
switch (candidate.category) {
case 'navigation':
if (typeof candidate.target !== 'string') {
throw new Error('Navigation link purpose requires a target');
}
break;
case 'action':
if (typeof candidate.action !== 'string') {
throw new Error('Action link purpose requires an action');
}
break;
case 'content':
if (typeof candidate.contentType !== 'string') {
throw new Error('Content link purpose requires a contentType');
}
break;
case 'download':
if (typeof candidate.mimeType !== 'string') {
throw new Error('Download link purpose requires a mimeType');
}
break;
default:
const exhaustive: never = candidate.category;
throw new Error(`Unknown link purpose category: ${String(exhaustive)}`);
}
}
这里使用了asserts value is LinkPurpose的断言签名,函数调用后TypeScript会认为value已经是LinkPurpose类型,后续可以直接读取字段。switch中的default分支利用了never类型做穷尽检查:如果未来有人给LinkPurpose联合类型新增一个类别,但忘记在switch中处理,编译器会在default分支报错,因为candidate.category无法被赋给never。这种设计让类型定义和运行时校验同步演进。
对于更复杂的校验需求,也可以引入zod或io-ts这类运行时类型库来替代手写守卫。不过手写版本的好处是零依赖、可读性强,在小中型组件库中更易维护。实际选型可以根据GWAC项目的依赖策略决定。
在GWAC组件中集成
类型定义最终要落到组件接口上,才能真正影响开发者的使用方式。以React组件为例,链接组件的props可以约束为LinkPurpose类型,这样传入任意字符串或结构不完整的对象都会在编译期被拦截。组件内部调用assertLinkPurpose做双重保险,再根据不同的category渲染对应的无障碍属性。
import { LinkPurpose, assertLinkPurpose } from './linkPurpose';
interface AccessibleLinkProps {
href: string;
purpose: LinkPurpose;
children: React.ReactNode;
}
function AccessibleLink({ href, purpose, children }: AccessibleLinkProps) {
assertLinkPurpose(purpose);
const accessibleName = purpose.label;
return (
<a href={href} aria-label={accessibleName}>{children}</a>
);
}
这个示例只展示了基础用法。实际组件中还可以根据purpose.category决定是否添加aria-describedby指向上下文区域,或者在下载类链接中设置download属性和MIME类型提示。由于purpose已经是判别式联合,在使用isDownloadLinkPurpose收窄后,TypeScript会允许访问mimeType字段,这让条件渲染变得安全。
更进一步,可以把希腊语标签的生成逻辑也纳入类型模块。例如根据category和target自动生成默认的希腊语可访问名称,同时允许传入自定义label覆盖默认值。这种集中管理避免了在多个组件中重复编写字符串拼接逻辑,也让无障碍审计更容易追踪链接目的的来源。
与WCAG成功标准2.4.4的关系及测试
链接目的类型的核心价值在于帮助满足WCAG成功标准2.4.4。标准要求链接目的能够被编程确定,而不是仅靠视觉上下文推断。通过TypeScript类型定义,我们把链接目的从自由文本变为结构化数据,这为可编程确定提供了基础。测试策略可以围绕两个维度展开:一是所有LinkPurpose对象都能生成非空可访问名称;二是每个类别在缺失关键字段时会被运行时校验拦截。
下面是一个基于vitest的简单测试示例,验证导航类链接目的能够正确提取希腊语标签。
import { describe, it, expect } from 'vitest';
import { LinkPurpose, getAccessibleName } from './linkPurpose';
describe('LinkPurpose accessible name', () => {
it('should produce a name for navigation purpose', () => {
const purpose: LinkPurpose = { category: 'navigation', label: 'Αρχική', target: 'home' };
expect(getAccessibleName(purpose)).toBe('Αρχική');
});
});
getAccessibleName函数可以简单返回label字段,也可以根据category做回退处理,例如当label为空时,导航类使用target、操作类使用action、内容类使用contentType、下载类使用mimeType。这样既保证了可访问名称的可用性,又不会强制调用方手动拼装字符串。测试覆盖这些回退分支后,可以显著降低因遗漏label而导致的合规风险。
将这些类型定义和校验逻辑打包为独立模块后,GWAC项目中的其他无障碍组件都可以复用。链接目的不再是散落在各处的字符串,而是有明确约束的结构化数据。对于希腊语环境下的本地化需求,只需在标签生成层做翻译映射,不会影响底层类型安全,也不会破坏已有的编译期检查。
TypeScriptGWAC链接目的类型修改时间:2026-09-17 01:17:11