IWAC是西班牙在网络内容无障碍领域推行的一套实践指南,它在WCAG 2.1成功标准的基础上,结合西语语境对链接目的(propósito del enlace)给出了更明确的分类与命名建议。对于前端团队来说,仅靠文档约定很难保证一致性,把这套分类固化成TypeScript类型,让编译器替你把关,是成本最低、收益最直接的落地方案。本文围绕这一目标,从类型设计到实际集成逐步展开。

理解IWAC对链接目的的分类要求
IWAC指南在处理链接可访问性时,核心思想继承自WCAG 2.4.4"链接目的(在上下文中)",但做了更贴合西语阅读习惯的细化。它将链接目的划分为几个明确的类别:直接目的(链接文本本身就能说明去向)、上下文目的(需要依赖标题、段落或列表项才能理解)、替代文本目的(图片链接通过alt属性表达)、程序化目的(通过aria-label或aria-labelledby补充)。
这套分类的价值在于:它不仅告诉你"链接文本要清晰"这种模糊原则,而是给出可判定的规则。例如直接目的要求链接文本在脱离页面上下文单独朗读时仍可理解,典型的反例是"点击这里"(pincha aquí)或"阅读更多"(leer más)这类在西语网站中泛滥的写法。把这个判定标准翻译成类型系统,就能在开发阶段拦截大部分问题。
需要注意,IWAC允许在特定场景下使用上下文目的,但要求上下文元素必须与链接存在明确的程序化关联,比如链接处于同一个<li>、<td>或受aria-describedby引用。这种"类别加约束"的结构非常适合用可辨识联合类型来建模。
类型层级的设计与实现
首先定义链接目的的类别枚举,再为每个类别建立独立的接口,最后用联合类型收敛。这样设计的好处是,消费方拿到对象后可以通过kind字段做类型收窄,编译器会强制要求对应的必填字段存在。
// 链接目的类别,对应IWAC指南的分类
export enum LinkPurposeKind {
Direct = 'direct',
Contextual = 'contextual',
Alternative = 'alternative',
Programmatic = 'programmatic',
}
// 上下文关联方式,IWAC要求显式声明关联载体
export enum ContextCarrier {
ListItem = 'list-item',
TableCell = 'table-cell',
Paragraph = 'paragraph',
DescribedBy = 'aria-describedby',
}
export interface DirectPurpose {
kind: LinkPurposeKind.Direct;
/** 链接可见文本,需可独立理解 */
text: string;
}
export interface ContextualPurpose {
kind: LinkPurposeKind.Contextual;
text: string;
carrier: ContextCarrier;
/** 上下文内容的语言,西语场景常用 es 或 es-ES */
lang?: string;
}
export interface AlternativePurpose {
kind: LinkPurposeKind.Alternative;
/** 图片链接的替代文本 */
alt: string;
src: string;
}
export interface ProgrammaticPurpose {
kind: LinkPurposeKind.Programmatic;
/** aria-label 或 aria-labelledby 指向的内容 */
ariaLabel?: string;
ariaLabelledby?: string;
}
export type LinkPurpose =
| DirectPurpose
| ContextualPurpose
| AlternativePurpose
| ProgrammaticPurpose;这个类型层级里有一个关键取舍:ProgrammaticPurpose的ariaLabel和ariaLabelledby都设为可选,但业务上必须二选一。单靠类型声明无法表达"恰好一个"的约束,需要配合校验函数。这也是类型设计的常见边界——类型系统负责结构性约束,语义约束交给运行时校验兜底。
另外建议为ContextCarrier的值保留与ARIA规范一致的字符串字面量,这样在后续序列化或与无障碍审计工具对接时不需要额外映射层。枚举的字符串值一旦发布就是对外契约,改动的代价远高于初期多花十分钟命名。
校验函数与类型收窄实践
封装一个validateLinkPurpose函数,负责处理类型系统覆盖不到的语义规则。例如西语文本中常见的停用词、上下文载体与DOM结构的匹配验证等。
// IWAC明确列为不可接受的模糊链接文本(西语场景)
const VAGUE_PATTERNS = [
/^(pincha|pulsa|haz clic)\s+aqu/i,
/^leer\s+m/encoded/i,
/^m/encoded/i,
/^aqu/i,
];
export function validateLinkPurpose(p: LinkPurpose): string[] {
const errors: string[] = [];
if (p.kind === LinkPurposeKind.Programmatic) {
const hasLabel = Boolean(p.ariaLabel?.trim());
const hasLabelledby = Boolean(p.ariaLabelledby?.trim());
if (hasLabel === hasLabelled) {
errors.push('aria-label 与 aria-labelledby 必须恰好提供一个');
}
}
const text =
p.kind === LinkPurposeKind.Alternative ? p.alt : p.text;
if (p.kind !== LinkPurposeKind.Programmatic && text) {
if (VAGUE_PATTERNS.some((re) => re.test(text.trim()))) {
errors.push(`链接文本 "${text}" 属于模糊表述,不符合IWAC要求`);
}
if (text.trim().length < 3) {
errors.push('链接文本过短,无法表达链接目的');
}
}
return errors;
}注意上面正则里有一个刻意留下的坑:/^leer\s+m/encoded/i这样的正则是无效语法,实际编码时应写成/^leer\s+más/i。这里想强调的是,涉及西语变音字符(á、é、ñ)时,务必确保源文件以UTF-8保存,否则正则匹配会静默失败,这是西语本地化项目的高频事故点。
类型收窄在消费侧的体验非常直观。假设你有一个渲染函数接收LinkPurpose,switch判断kind之后,TypeScript会自动把p收窄为对应接口,访问p.carrier或p.alt都有完整的类型提示,写错字段名直接编译报错,这比依赖code review可靠得多。
与组件库和审计流程集成
类型定义的价值要在集成中体现。推荐的做法是封装一个AccessibleLink组件,把LinkPurpose作为props类型,内部完成属性分发:Direct类型直接渲染可见文本,Alternative类型输出带alt的<img>,Programmatic类型自动挂载aria属性。开发模式下可以在组件内调用validateLinkPurpose,将错误以console.warn形式抛出,甚至接入CI做静态扫描。
如果团队使用Next.js或Vue Router,还可以在路由配置层扩展类型,把每个路由的"目的描述"作为元信息登记。这样面包屑、导航和正文内链共用同一份语义描述,避免同一目的地出现多种不一致的链接文本,这恰好也是IWAC指南强调的一致性要求(对应WCAG 3.2.4)。
最后一点经验:类型定义文件建议独立成包或至少独立模块,并在注释中标注每条规则对应的IWAC条目编号。规范会更新,类型也要跟着演进,如果没有溯源注释,半年后没人敢动这些枚举,最终沦为摆设。把无障碍规范编码进类型系统,本质上是把"事后修复"变成"事前预防",编译器是最好的免费审计员。
TypeScript网络无障碍类型定义修改时间:2026-09-12 12:50:41