斯威士兰网络无障碍指南的官方表格通常以多条准则为主体,每条准则包含编号、标题、等级、适用范围以及关联的检查点和豁免条件。这类表格如果直接以 any 或简单的 Record<string, string> 存储,调用方经常会在编译期拿到一个看似正常、运行时却出现 undefined 的数据结构。为IWAC(Inclusive Web Accessibility Compliance,团队内部的网页无障碍合规工具库)封装这部分数据时,需要把表格之间的引用关系显式地写入TypeScript类型。

一、识别指南表格中的关联维度
官方表格并不是孤立行。每一个准则编号(如3.1.1)对应一个标题,同时可能声明若干关联检查点,检查点又指向具体的技术角色。比如设计角色需要检查颜色对比度,前端角色需要检查焦点顺序,测试角色需要核对屏幕阅读器输出。除此之外,部分AAA等级条款会声明“不适用于关键业务表单”,这类豁免条件本身也是表格中的一列。将这些关系直接建模为字符串数组会丢失约束,所以类型封装的第一步是把关联维度拆开。
可以先定义行与检查点的基本结构,让编号格式、等级、适用范围都受到字面量类型的限制。以下代码展示了最基础的行模型:
type GuidelineCode = `3.${number}.${number}`;
interface GuidelineRow {
code: GuidelineCode;
title: string;
level: 'A' | 'AA' | 'AAA';
scope: 'page' | 'component' | 'document';
parentRef?: GuidelineCode;
dependsOn: GuidelineCode[];
checkpoints: CheckpointRef[];
}
interface CheckpointRef {
id: string;
role: 'frontend' | 'design' | 'qa';
description: string;
requiresReview: boolean;
}
这里用模板字面量类型 `3.${number}.${number}` 约束编号格式,但还不够。parentRef和dependsOn只是GuidelineCode数组,编译器无法知道引用的编号是否存在。真正有效的封装需要引入映射类型和已知键集合。
另外一个容易忽视的关联是表格列之间的条件关系。例如当scope为component时,checkpoints中必须至少包含一个role为frontend的检查点;当level为AAA时,parentRef必须存在。这类跨字段规则可以通过联合类型和泛型约束表达,后面会给出具体实现。
二、构建可推导的映射类型和联合键
为了约束引用完整性,可以先把指南编号的字面量集合提取出来。如果指南数据来自JSON或TypeScript常量,可以用 typeof 和 as const 生成联合类型。随后用映射类型生成一个以编号为键的只读结构,这样在查找时,键名就有了自动补全,错误键名会在编译期报错。
const guidelineCodes = ['3.1.1', '3.2.2', '4.1.1'] as const;
type GuidelineCode = typeof guidelineCodes[number];
type GuidelineMap = {
[K in GuidelineCode]: GuidelineRow & {
code: K;
};
};
联合键能够进一步表达角色和等级的关联。比如校验函数需要按“角色-等级”分组定位条款,直接拼接字符串容易写出无效组合。用模板字面量类型可以生成合法的键集合,再配合 Record 类型,就能让对象只接受规定组合。这样在传入数据时,缺少AA级别或误写角色都会失败。
type Role = 'frontend' | 'design' | 'qa';
type Level = 'A' | 'AA' | 'AAA';
type RoleLevelKey = `${Role}-${Level}`;
type RoleLevelMap = Record<RoleLevelKey, GuidelineCode[]>;
上面的 RoleLevelMap 要求每个组合都有对应数组,但实际指南表格里并不是所有角色和等级都有条款。可以改用 Partial 映射或可选键,避免填充无意义空数组。同时可以写一个辅助类型,根据给定角色返回相关键,这些类型工具都能让IWAC的表格加载器在编译阶段拒绝错位数据。
三、封装IWAC校验函数和类型守卫
有了基础类型和映射表,下一步是在IWAC内部提供入口函数。函数参数直接使用 GuidelineMap,返回值用一个描述问题的联合类型。不要返回布尔值,因为调用方需要知道具体哪些行、哪些字段不满足指南要求。这里给出一个校验示例:AAA条款不应声明硬性依赖,而且component范围内的记录必须包含frontend角色的检查点。
type ValidationIssue =
| { kind: 'missing-dependency'; code: GuidelineCode }
| { kind: 'invalid-exemption'; code: GuidelineCode; detail: string }
| { kind: 'missing-frontend-check'; code: GuidelineCode };
function validateGuidelineTable(
table: GuidelineMap
): ValidationIssue[] {
const issues: ValidationIssue[] = [];
for (const code of Object.keys(table) as GuidelineCode[]) {
const row = table[code];
if (row.level === 'AAA' && row.dependsOn.length > 0) {
issues.push({ kind: 'missing-dependency', code });
}
if (
row.scope === 'component' &&
!row.checkpoints.some((cp) => cp.role === 'frontend')
) {
issues.push({ kind: 'missing-frontend-check', code });
}
}
return issues;
}
仅靠类型推导还不够,因为数据可能来自后端接口或本地JSON文件。类型守卫可以缩小 unknown 值的范围。用 isGuidelineCode 函数配合类型谓词,就能让后续逻辑安全地使用 GuidelineCode。类型守卫需要维护一个运行时已知的编号集合,这可以与前面的 as const 常量共享。
const knownCodes = guidelineCodes as readonly string[];
function isGuidelineCode(value: unknown): value is GuidelineCode {
return typeof value === 'string' && knownCodes.includes(value);
}
function parseGuidelineTable(raw: unknown): GuidelineMap | null {
if (typeof raw !== 'object' || raw === null) return null;
const result: Partial<GuidelineMap> = {};
for (const [key, value] of Object.entries(raw)) {
if (!isGuidelineCode(key)) continue;
result[key] = value as GuidelineRow;
}
return Object.keys(result).length > 0 ? (result as GuidelineMap) : null;
}
实际项目中,建议把指南数据源放在一个独立的模块如 C:\projects\iwac\src\data\guidelines.ts,然后通过编译脚本生成类型。类型守卫和生成文件保持同一数据源,可以避免手写联合类型与实际数据不同步。注意Windows路径中的反斜杠必须保留,不能写成正斜杠。
四、处理豁免条件与类型扩展
斯威士兰指南的表格中还有一类“豁免条件”列,它决定某些条款在特定场景下可以跳过。例如对纯音频内容,颜色对比度条款可能不适用。这类豁免如果用布尔值表示,会丢失豁免原因以及是否已经经过审核。更合理的做法是使用判别联合类型,把豁免建模为“允许且附带原因”或“不允许”。
type Exemption<T extends string> =
| { allowed: true; reason: T }
| { allowed: false };
type GuidelineWithExemption<T extends string> = GuidelineRow & {
exemption: Exemption<T>;
};
type CommonExemptionReason =
| 'audio-only'
| 'legacy-document'
| 'external-content';
type FullGuidelineRow = GuidelineWithExemption<CommonExemptionReason>;
这种写法使豁免原因只能是预定义字符串,新增原因需要在联合类型中声明,否则编译失败。它比直接使用 string 更安全,也比单独维护枚举列表更直观。在IWAC的表格渲染组件中,可以通过 switch 或 if 来收窄 allowed 属性,从而自动获得 reason 类型,不用写额外的断言。
类型扩展的同时,要注意不要过度设计。并非指南表格中的每一列都需要立即映射到类型系统。可以先从核心引用关系和多发错误字段开始,逐步添加约束。如果表格更新频繁,可以写一个转换函数,把官方CSV或XLSX转为TypeScript源文件,并导出类型。这个文件可以放在 C:\projects\iwac\src\types\generated\,注意目录分隔符同样是反斜杠。
通过为斯威士兰网络无障碍指南的表格数据封装关联类型,IWAC在编译期获得了更强的数据一致性保障。关键点包括:提取关联维度、使用映射类型和模板字面量类型构建合法键、通过类型守卫缩小运行时数据、以及用判别联合处理豁免条件。这些类型不需要复杂运行时逻辑,却能显著减少调用方误用。

