链接上下文(Link Context)是指帮助用户理解链接目的的周边信息,例如链接所在段落的文字、aria-label属性值或者链接自身的可读文本。IWAC缅甸网络无障碍指南针对本地化场景,对缅文链接文本提出了多项具体要求,比如链接文本必须独立可理解、不能使用“点击这里”这类模糊表述、屏幕阅读器朗读时需要保留缅文字符的正确发音信息等。如果只用普通的string类型来承载这些数据,编译器无法帮我们做任何约束,不合规的内容只能靠人工review发现。用TypeScript为这些规则建立类型层,可以让规范检查前移到编译阶段。

一、先梳理IWAC指南对链接上下文的数据要求
动手写类型之前,需要先把指南中与链接上下文相关的条款翻译成数据结构层面的描述。IWAC指南对链接的约束大致可以归纳为四类:一是链接自身必须有可读文本,文本来源可以是链接内部内容、aria-label或者aria-labelledby;二是链接文本的用途必须明确,禁止出现孤立的无意义短语;三是当链接指向非HTML资源(如PDF、图片)时,需要在文本中标识文件类型;四是链接文本如果是缅文,需要正确处理缅甸文字的组合字符,避免朗读引擎拆散发音单元。
把这些要求整理成类型设计输入,可以得到一个清晰的责任划分:哪些字段是必填、哪些字段互斥、哪些字段有格式要求。例如aria-label和aria-labelledby在HTML规范中就是互斥使用的,类型上应该用联合类型表达,而不是两个可选字段随意组合。再比如文件类型标识,可以用一组固定的字符串字面量来约束,超出范围的值直接报编译错误。这种从规范条款到类型规则的映射过程,是整个封装工作的核心,代码反而是后面水到渠成的部分。
建议在团队内先形成一份字段对照表,明确每个类型字段对应指南的哪一条款。这样做的好处是后期审查时可以直接追溯,指南更新时也能快速定位需要调整的类型定义。
二、定义基础类型与互斥字段的联合类型
先定义链接文本来源的基础类型。IWAC要求链接的可访问名称至少来自一个渠道,我们可以用联合类型配合类型守卫来强制这个约束。下面是基础类型定义的代码:
// 链接可访问名称的来源类型,三选一
export type LinkNameSource =
| { kind: 'inline-text'; text: string }
| { kind: 'aria-label'; label: string }
| { kind: 'aria-labelledby'; ids: string[] };
// 非HTML资源的类型标识,对应IWAC的资源标识条款
export type LinkedResourceType =
| 'pdf'
| 'doc'
| 'image'
| 'audio'
| 'video'
| 'external-page';
// 链接上下文的完整类型
export interface IwacLinkContext {
href: string;
name: LinkNameSource;
resourceType?: LinkedResourceType;
opensInNewTab?: boolean;
/** 缅文文本是否已通过发音单元校验 */
burmeseTextValidated?: boolean;
}这个设计有几个值得注意的点。LinkNameSource用带kind标签的可辨识联合(Discriminated Union)来表示三种来源,调用方在使用时必须通过类型守卫收窄类型,编译器会强制处理所有分支。resourceType用字面量联合而不是string,任何拼写错误或超出范围的值都会被拦截。burmeseTextValidated是一个运行时校验结果的标记,因为缅甸文字的组合字符校验无法在类型层完成,只能标记校验状态,让类型系统至少记录这个信息。
接着写一个类型守卫函数,用于判断链接名称是否有效:
export function isInlineText(
source: LinkNameSource
): source is Extract<LinkNameSource, { kind: 'inline-text' > {
return source.kind === 'inline-text';
}
// 构建函数,强制要求至少提供一个名称来源
export function createLinkContext(input: {
href: string;
} & LinkNameSource): IwacLinkContext {
const { href, ...name } = input;
return { href, name };
}createLinkContext的参数用了交叉类型,把href和LinkNameSource交叉在一起,这样调用方在构造对象时就必须内联提供名称来源,漏掉任何字段都会编译失败。这种“构造函数收口”的模式比直接暴露接口让调用方自行拼装对象更安全,因为接口一旦公开,调用方就可能绕过约束构造出不完整的对象。
三、用模板字面量类型约束文本格式与禁止用语
IWAC禁止链接文本使用模糊表述,比如“点击这里”“阅读更多”这类没有信息量的短语。虽然无法用类型系统穷举所有违规文本,但可以反向操作:定义一个类型,要求链接文本必须包含足够的具体信息。一种实用做法是结合模板字面量类型,对包含文件类型的链接文本做结构约束:
// 要求链接文本以资源标识结尾,例如 “2024年度报告 (PDF)”
type WithResourceHint<T extends string> =
`\${T} (\${LinkedResourceType})`;
// 示例:函数签名约束
function createResourceLink<T extends string>(
text: T,
resource: LinkedResourceType,
href: string
): IwacLinkContext {
const expected = `\${text} (\${resource})` as WithResourceHint<T>;
return {
href,
name: { kind: 'inline-text', text: expected },
resourceType: resource,
};
}模板字面量类型在这里的作用是把“链接文本需要包含资源类型提示”这条指南要求编码进类型签名。虽然运行时仍靠模板字符串拼接保证格式,但类型层面已经把约束显式表达出来,后续维护者看到类型定义就能明白规则。
对于禁止用语,更务实的方案是维护一个禁止短语列表,配合编译期的Exclude做部分拦截,其余交给单元测试。完全依赖类型系统做自然语言校验是不现实的,类型层做能做的部分,剩下的边界用测试兜底,这是工程上更合理的分工。
四、封装泛型组件并接入React或前端框架
类型定义完成后,下一步是把类型约束传递到组件层。以React为例,封装一个链接组件,把IwacLinkContext作为props的强约束:
import React from 'react';
interface AccessibleLinkProps {
context: IwacLinkContext;
className?: string;
children?: React.ReactNode;
}
export function AccessibleLink({
context,
className,
children,
}: AccessibleLinkProps) {
const { href, name, resourceType, opensInNewTab } = context;
const ariaAttributes: Record<string, string> = {};
if (name.kind === 'aria-label') {
ariaAttributes['aria-label'] = name.label;
} else if (name.kind === 'aria-labelledby') {
ariaAttributes['aria-labelledby'] = name.ids.join(' ');
}
return (
<a
href={href}
className={className}
target={opensInNewTab ? '_blank' : undefined}
rel={opensInNewTab ? 'noopener noreferrer' : undefined}
{...ariaAttributes}
>
{name.kind === 'inline-text' ? name.text : children}
</a>
);
}组件内部根据name的kind分支处理三种来源,这正是可辨识联合的价值:switch分支遗漏时编译器会报错。opensInNewTab为true时自动补上rel属性,这也对应了无Accessibility指南中关于新窗口打开需要告知用户的条款,虽然完整的告知文案还需要额外字段,可以在类型中继续扩展。
更进一步,可以用映射类型为IwacLinkContext生成一个“深度只读”的版本,防止组件内部意外修改上下文数据:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
export type ReadonlyLinkContext = DeepReadonly<IwacLinkContext>;这样组件props直接使用ReadonlyLinkContext,任何试图修改context.name.text的代码都会在编译期被拒绝。对于跨团队共享的无障碍基础组件来说,只读约束可以有效避免各业务方对上下文数据的隐式篡改。
五、方案对比与落地建议
这类封装常见的有三种做法:第一种是全部用string加运行时校验,实现简单但约束力弱,问题只能在运行时暴露;第二种是本文推荐的联合类型加构造函数收口,编译期能拦截大部分结构性错误,运行时只保留缅文字符校验等无法静态化的部分;第三种是借助JSON Schema加代码生成,规范程度最高,但引入了额外的构建链路,对中小团队来说成本偏高。从投入产出比看,第二种方案在绝大多数场景下是最平衡的选择。
落地时有几点经验值得参考。首先是类型定义要与指南条款编号建立注释关联,方便规范更新时同步维护。其次是构造函数只暴露createLinkContext这一个入口,避免调用方绕过。最后是配合ESLint的jsx-a11y插件做运行前检查,类型层管结构,lint规则管使用方式,两层配合才能覆盖完整的无障碍要求。类型封装不是终点,而是把指南从文档变成工具链的一部分,让每个开发者在写代码时就自然遵守规范。
TypeScriptIWAC链接上下文类型修改时间:2026-09-08 20:45:21