在无障碍改造或者网站合规审查中,经常要和比利时网络无障碍指南打交道。这份指南所使用的缩写并不算少,比如BWAC本身代表Belgian Web Accessibility Guidelines,除此之外还有WCAG、ARIA、PDF/UA等一堆术语。如果只是做简单展示,用一个JavaScript对象存下缩写和全称的对应关系就足够了。但一旦这个映射被多个模块引用,或者需要根据缩写动态生成文档链接、标题、说明文字,类型安全就变得很重要。手写映射对象最大的问题是,TypeScript只能推断出键名是string,值也是string,编译器完全不知道某个键到底存不存在,更不会告诉你返回值是不是合理的全称。这导致开发时要么频繁查阅文档,要么只能靠运行时测试来兜底。

本篇文章将以BWAC缩写展开为具体场景,演示如何用TypeScript的类型系统来封装一个既灵活又严格的缩写展开工具。我们会从基础的类型定义入手,逐步加入模板字面量类型、条件类型和递归类型,最终得到一个可以在编译期拦截大量低级错误的方案。代码示例全部基于TypeScript 4.x及以上版本,不依赖任何第三方库。
传统映射对象的类型短板
假设一个最直观的实现,用const声明一个对象,把BWAC、WCAG、ARIA这些缩写作为键,对应的全称作为值。TypeScript会推断出一个类似 { BWAC: string; WCAG: string; ARIA: string; } 的类型,但键名被锁定为这三个字面量,值却宽泛地成了string。这意味着当你访问 mapping.BWAC 时,得到的类型是string,而不是精确到Belgian Web Accessibility Guidelines这个字面量。如果后续代码里需要根据返回值拼接固定格式的字符串,比如生成一个完整的标题,编译器无法确保拼接结果符合预期。
更大的问题出现在键名拼写上。比如误写成 mapping.BWCA,TypeScript会直接报错,因为对象类型里没有BWCA这个属性。这看起来不错,但如果是通过一个变量动态访问,比如 mapping[abbr],只要abbr的类型是string,编译器就完全放行。而实际业务中从URL参数、配置文件或者用户输入里拿到的缩写正是string类型,这种情况下类型系统起不到任何保护作用。
还有个容易被忽略的场景:当这个映射对象需要被多处导入使用时,可能有的模块会自行扩展新的缩写,有的模块则依赖原有的键集合。如果能用一个明确的类型接口来描述缩写与全称的映射关系,同时支持展开函数的类型推导,就可以避免不同模块之间的类型漂移。这正是我们需要封装类型定义的动机。
核心类型定义与展开函数签名
设计的第一步是定义一个联合类型,把所有合法的缩写字符串都列出来。对于BWAC场景,我们可以写作 type Abbreviation = 'BWAC' | 'WCAG' | 'ARIA' | 'PDF/UA'; 这里的字符串字面量联合类型会作为后续所有类型运算的基础。接下来定义一个映射类型,明确每个缩写对应的全称是什么。可以利用Record工具类型,但更建议使用显式的接口或者类型别名,因为这样能保留每个键对应的精确值类型。
type Abbreviation = 'BWAC' | 'WCAG' | 'ARIA' | 'PDF/UA';
interface FullNameMap {
BWAC: 'Belgian Web Accessibility Guidelines';
WCAG: 'Web Content Accessibility Guidelines';
ARIA: 'Accessible Rich Internet Applications';
'PDF/UA': 'Portable Document Format for Universal Accessibility';
}
有了完整的映射类型,展开函数就可以这样声明:接收一个Abbreviation类型的参数,返回FullNameMap中对应的值类型。这里的关键是使用泛型约束加索引访问,让TypeScript在调用时自动推断出精确的返回值。例如定义一个函数 function expandAbbr<K extends keyof FullNameMap>(abbr: K): FullNameMap[K],当传入BWAC时返回类型就是Belgian Web Accessibility Guidelines,传入WCAG时返回类型就是Web Content Accessibility Guidelines。这种模式让调用方无需手动断言,就能获得最准确的字面量类型。
function expandAbbr<K extends keyof FullNameMap>(abbr: K): FullNameMap[K] {
const map: FullNameMap = {
BWAC: 'Belgian Web Accessibility Guidelines',
WCAG: 'Web Content Accessibility Guidelines',
ARIA: 'Accessible Rich Internet Applications',
'PDF/UA': 'Portable Document Format for Universal Accessibility',
};
return map[abbr];
}
const bwacFull = expandAbbr('BWAC');
// 类型为 'Belgian Web Accessibility Guidelines'
const wcagFull = expandAbbr('WCAG');
// 类型为 'Web Content Accessibility Guidelines'
这里实现函数内部需要维护一个与类型一致的运行时映射对象,这是必需的,因为类型在编译后会被擦除,运行阶段仍然需要一个实际的数据结构来返回值。如果映射关系非常庞大,可以考虑使用as const断言让TypeScript推导出精确只读类型,但为了演示方便,显式声明FullNameMap接口更清晰。
上述做法已经能解决大部分拼写错误和类型精度问题,但如果缩写存在嵌套或者需要根据上下文返回不同形态(例如返回小写短横线形式),就需要更灵活的类型工具。接下来我们看看如何用条件类型和模板字面量类型扩展这个方案。
借助条件类型实现批量缩写展开
实际项目中,缩写的展开可能不只是返回全称,还可能需要生成对应的数据属性名、文档片段锚点或者CSS类名。例如把BWAC展开为bwac,或者把PDF/UA转换为pdf-ua。这类需求可以通过模板字面量类型加条件类型优雅地解决,而不需要为每一种转换手动编写类型。
假设我们要定义一个类型 KebabCaseAbbr,它能把联合类型中的每个字符串转换为短横线小写形式。由于TypeScript内置的 Lowercase 工具类型和模板字面量类型可以配合条件类型对联合类型进行逐项处理。我们可以这样写:type KebabCaseAbbr<T extends string> = T extends `${infer Head}${infer Tail}` ? ... 但处理特殊字符如斜杠比较复杂。对于BWAC这个具体案例,我们不妨直接维护一个额外的映射类型,把每个缩写转换为合适的短横线形式,然后用条件类型进行分发映射。
type AbbreviationToSlugMap = {
BWAC: 'bwac';
WCAG: 'wcag';
ARIA: 'aria';
'PDF/UA': 'pdf-ua';
};
type ToSlug<T extends keyof AbbreviationToSlugMap> = AbbreviationToSlugMap[T];
function getAnchor<K extends keyof AbbreviationToSlugMap>(abbr: K): `#${ToSlug<K>}` {
return `#${abbr.toLowerCase().replace('/', '-')}` as `#${ToSlug<K>}`;
}
const bwacAnchor = getAnchor('BWAC'); // 类型为 '#bwac'
const pdfAnchor = getAnchor('PDF/UA'); // 类型为 '#pdf-ua'
上面的例子中,ToSlug 是一个简单的条件类型别名,它根据传入的键从映射表中取出对应的slug字符串。返回值类型使用模板字面量类型拼接了#前缀。函数实现里通过一个as断言让TypeScript相信最终的字符串符合类型要求,在实际项目中你可以把转换逻辑写在独立的纯函数里,并搭配单元测试确保正确性。
更进一步,如果想让外部调用方能够传入任意字符串,然后由类型检查器判断该字符串是否属于已知缩写,可以使用条件类型加 never 进行过滤。例如定义 type ValidAbbr<T> = T extends keyof FullNameMap ? T : never; 然后在函数签名中配合重载,让非法输入直接触发类型错误。这种技巧在构建公开API时特别有用,能让使用者在编码阶段就发现无效的缩写值。
递归展开与嵌套对象映射
有些情况下缩写的映射并不是扁平的。比如比利时网络无障碍指南可能按照章节或主题划分,存在多级缩写,例如一级目录用BWAC,二级目录使用具体标准编号。这时就需要一个支持嵌套结构的类型展开。我们可以定义一个递归类型别名,接受一个嵌套的映射对象类型,然后生成对应的展开函数类型。
假设有一个结构如下:顶层键是主要标准缩写,值可以是字符串,也可以是一个子对象,子对象的键是子标准缩写,值同样可以是字符串或更深的子对象。为了处理这种递归结构,可以利用条件类型加上递归类型引用。在TypeScript中,递归类型别名是允许的,只要它最终能够被解析到确定类型。
type NestedAbbrMap = {
BWAC: {
WCAG: 'Web Content Accessibility Guidelines';
ARIA: 'Accessible Rich Internet Applications';
};
EN: {
'301 549': 'European standard for digital accessibility';
};
};
type ExpandNested<T> = {
[K in keyof T]: T[K] extends string ? T[K] : ExpandNested<T[K]>;
};
function expandNestedAbbr<T extends NestedAbbrMap>(map: T, path: string): string {
// 运行时实现省略具体遍历逻辑
return path;
}
type FullyExpanded = ExpandNested<NestedAbbrMap>;
// FullyExpanded 的类型为 { BWAC: { WCAG: 'Web Content Accessibility Guidelines'; ARIA: 'Accessible Rich Internet Applications'; }; EN: { '301 549': 'European standard for digital accessibility'; } }
上面 ExpandNested 的类型定义演示了对嵌套结构的递归处理。它遍历对象的每个键,如果值的类型可以直接赋值给string,就保留原值,否则递归调用自身展开更深一层。这个类型别名本身并不执行实际的展开操作,而是提供了一个类型层面描述展开结果的方式。真正的展开函数需要根据运行时传入的路径字符串解析嵌套对象,类型签名可以配合对应路径的字符串字面量类型来精确返回结果,但实现起来复杂度较高,这里不深入展开。
对于大多数实际项目,扁平的缩写映射已经足够。如果确实需要嵌套展开,建议优先考虑使用类和普通函数封装,类型定义只负责约束键和值的合法性,不必过度追求递归类型上的完美推断。过度复杂的类型系统会让代码变得难以维护,反而失去了类型安全带来的可读性收益。
类型测试与收窄验证
封装完类型定义后,需要有一套可靠的测试手段来确认类型行为符合预期。TypeScript的类型测试通常不依赖运行时,而是利用编译器对赋值语句的检查。可以创建一个专用的类型测试文件,在里面写一些有意为之的错误赋值,然后通过tsc命令观察是否报错。更规范的做法是使用第三方类型测试库,例如tsd或expect-type,但为了保持零依赖,我们也可以手动编写断言。
例如定义 type Assert<T extends true> = T;,然后通过条件类型比较两个类型是否相等。比如验证 expandAbbr('BWAC') 的返回类型是否等于 'Belgian Web Accessibility Guidelines'。可以写 type Test1 = Assert<Equal<ReturnType<typeof expandAbbr<'BWAC'>>, 'Belgian Web Accessibility Guidelines'>>; 但这里的泛型调用需要小心,因为 expandAbbr 本身是泛型函数,直接使用typeof无法捕获具体参数类型。更好的方式是用一个变量承接调用结果,然后对变量类型进行断言。
const bwacResult = expandAbbr('BWAC');
type BwacResultType = typeof bwacResult;
type ExpectedBwac = 'Belgian Web Accessibility Guidelines';
type AssertEqual<A, B> = A extends B ? (B extends A ? true : false) : false;
type BwacCheck = AssertEqual<BwacResultType, ExpectedBwac>; // 应为 true
如果类型不正确,BwacCheck 会被推断为false。在编译阶段并不能让这个false导致程序错误,但可以通过在代码中放置一个期望为true的赋值来触发错误:const check: true = null as unknown as BwacCheck; 当BwacCheck为false时,这条赋值语句会报错,提示类型不兼容。这种手写测试方式比较原始,但在简单场景下足够管用。
还需要测试非法输入是否被类型系统拒绝。可以写一个试图用错误键名调用展开函数的代码片段,预期TypeScript报告属性不存在的错误。这类测试可以放在一个单独的.ts文件里,并在构建脚本中临时启用tsc的noEmit检查,确保整个项目在任何改动后都能自动验证类型安全性。
总结来说,为BWAC封装缩写展开的类型定义并不是一次性的任务,而是一套可以复用的类型设计模式。先定义联合类型和映射接口,再利用泛型约束和索引访问实现精确返回,遇到批量转换时结合条件类型和模板字面量类型,最后通过类型测试锁定行为。这套方法同样适用于其他标准缩写或枚举映射,能显著降低维护成本,让开发者在无障碍合规这类细节繁琐的领域少踩一些类型层面的坑。
TypeScript类型定义缩写展开修改时间:2026-09-29 21:25:52