莱索托网络无障碍指南在颜色使用上提出了两条硬性要求:第一,颜色不能作为传递信息的唯一视觉手段;第二,常规文本与背景的对比度至少达到4.5比1,大号文本至少3比1。对于IWAC这样的无障碍审计前端,如果颜色规则只存在于设计文档或测试用例中,重构主题、替换色板或新增组件时很容易破坏合规性。把颜色依赖关系提升到类型层面,可以让TypeScript在开发阶段就发现部分违规。下面结合类型定义给出一种封装方案。

从指南约束到可类型化规则
IWAC的代码库通常包含主题色板、组件状态和审计规则三部分。莱索托指南的本地化版本特别强调,在错误提示、成功提示、焦点边框和禁用状态中,颜色不能单独承担信息传递任务。例如错误文本如果只是从黑色变成红色,色觉障碍用户可能无法分辨;必须同时给出文字标签、图标或下划线等非颜色信号。这个要求看似简单,但如果每个组件都靠自己实现,就会出现规则不一致、回归测试覆盖不全的问题。
类型化的第一步是把指南中的颜色令牌固定下来。与其允许任意十六进制字符串,不如在类型层面声明一个受限的调色板。这样后续的依赖规则和组件属性都能引用同一个令牌集合。调色板可以定义为字面量联合类型,每个键对应一个具体的颜色值。虽然TypeScript的模板字面量类型不能直接证明两个颜色值的对比度比率,但它可以约束开发者在合规的候选颜色中选择,并禁止未定义的色值进入主题配置。
接下来还需要把状态与所需的非颜色信号绑定。可以定义一个规则对象,明确某个状态使用哪个颜色令牌,以及该状态必须同时出现哪些辅助标识。这样在IWAC的配置文件中,每条规则都能被集中管理,而不是散落在按钮、表单和提示组件里。
核心类型设计与代码示例
先定义基础颜色令牌。HexColor 使用模板字面量类型保留十六进制格式,LesothoPalette 则收窄到指南认可的色板。ColorToken 把角色、值和令牌名组合起来,便于后续按角色区分前景与背景。代码如下:
type HexColor = `#${string}`;
type LesothoPalette = {
textPrimary: '#1A1A1A';
textSecondary: '#3E3E3E';
background: '#FFFFFF';
errorText: '#B00020';
successText: '#0B7A3B';
borderStrong: '#6B6B6B';
};
type ColorRole = 'text' | 'background' | 'border';
type ColorToken = {
role: ColorRole;
value: HexColor;
tokenName: keyof LesothoPalette;
};
这里没有直接使用 string 作为颜色值类型,而是借助字面量联合让非法色值在赋值时就被拦截。对于需要扩展色板的情况,可以新增键名和色值,再同步调整规则即可。接下来定义非颜色信号与状态依赖。NonColorSignal 表示非颜色视觉线索,InteractionState 覆盖典型交互状态,ColorDependencyRule 把状态、颜色令牌、辅助信号和最低对比度组合在一起。
type NonColorSignal = 'icon' | 'underline' | 'pattern' | 'textLabel';
type InteractionState = 'error' | 'success' | 'disabled' | 'focus';
type ColorDependencyRule = {
state: InteractionState;
colorToken: keyof LesothoPalette;
requiredSignals: NonColorSignal[];
minContrast: number;
};
const lesothoColorRules: ColorDependencyRule[] = [
{
state: 'error',
colorToken: 'errorText',
requiredSignals: ['textLabel', 'icon'],
minContrast: 4.5,
},
{
state: 'focus',
colorToken: 'borderStrong',
requiredSignals: ['underline'],
minContrast: 3,
},
];
这种设计的优势在于,规则本身成为可导入的数据,IWAC的审计面板可以直接读取这些规则生成检查项。同时TypeScript会校验规则对象是否缺少字段、状态是否写错、颜色令牌是否存在。比单纯维护JSON配置更安全,也更容易重构。
要进一步让组件属性也接受约束,可以定义对比度守卫类型。虽然它不能在编译期精确计算任意两个颜色的对比度,但可以通过映射关系限制已知合规组合。对于调色板较小且规则固定的项目,这样做的收益非常明显。
type AssertContrast<Bg extends HexColor, Fg extends HexColor> =
Bg extends '#FFFFFF'
? Fg extends '#1A1A1A' ? true
: Fg extends '#3E3E3E' ? true
: Fg extends '#B00020' ? true
: false
: false;
type ContrastGuard<Bg extends HexColor, Fg extends HexColor> = {
background: Bg;
foreground: Fg;
__valid: AssertContrast<Bg, Fg> extends true ? true : never;
};
function defineTheme<Bg extends HexColor, Fg extends HexColor>(
config: ContrastGuard<Bg, Fg>,
) {
return config;
}
defineTheme({
background: '#FFFFFF',
foreground: '#1A1A1A',
__valid: true,
});
// 下面的调用会编译报错,因为当前规则未声明该前景色通过对比度检查
defineTheme({
background: '#FFFFFF',
foreground: '#DDDDDD',
__valid: true,
});
示例中的 AssertContrast 类型把背景色限定为白色,并用条件类型逐一判断前景色是否位于已批准列表。ContrastGuard 通过 __valid 字段在类型层面表达校验结果,一旦传入未声明的前景色,__valid 会解析为 never,传入 true 就无法通过类型检查。这种做法的限制也很清楚:它只适用于颜色集合小且预先穷举的场景,不适合完全动态的取色器。
运行时校验与类型系统的边界
类型定义能拦截手误和明显的规则破坏,但无法替代真正的对比度计算。例如当调色板从设计工具导出,或用户通过配置覆盖默认颜色时,编译期约束就不再可靠。IWAC需要在运行时计算相对亮度与对比度比率,并将结果与规则中的 minContrast 进行比较。下面给出标准的亮度计算函数。
function relativeLuminance(hex: string): number {
const clean = hex.replace('#', '');
const r = parseInt(clean.substring(0, 2), 16) / 255;
const g = parseInt(clean.substring(2, 4), 16) / 255;
const b = parseInt(clean.substring(4, 6), 16) / 255;
const toLinear = (c: number) =>
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
const rLin = toLinear(r);
const gLin = toLinear(g);
const bLin = toLinear(b);
return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
}
function contrastRatio(fg: string, bg: string): number {
const l1 = relativeLuminance(fg);
const l2 = relativeLuminance(bg);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
在IWAC的运行时校验阶段,可以遍历 lesothoColorRules,根据颜色令牌取出实际色值,调用 contrastRatio 验证是否达到 minContrast。同时还要检查每个状态的 requiredSignals 是否真的出现在DOM中。例如错误提示必须包含文字标签和图标,则审计逻辑可以查询对应元素是否同时具备这两个信号。如果缺失,就报告违规。
这种运行时与编译期结合的方式,才是实际项目中更稳妥的做法。类型定义负责固定结构和早发现低级错误,运行时计算负责应对动态颜色和真实渲染结果。两者共享同一套规则数据,避免维护两份不同的无障碍配置。
扩展方向与落地建议
当IWAC需要支持多主题或用户自定义配色时,单一调色板会变得不够用。可以把 LesothoPalette 抽象为泛型,让每个主题提供自己的色板,同时保持规则结构不变。此时类型约束依然可以限制主题必须覆盖指南要求的令牌,但具体的对比度判断通常需要交给运行时处理。设计时可以将编译期守卫用于默认主题,将运行时计算用于扩展主题,兼顾开发体验与灵活性。
另一个容易忽视的问题是,颜色依赖规则不能只覆盖文本与背景。焦点边框、图表图例、表格高亮和气泡提示都可能依赖颜色传达信息。IWAC在扩展组件时,应优先复用 ColorDependencyRule 中的状态定义,而不是为每个组件单独发明一套状态字符串。统一的 InteractionState 和 NonColorSignal 联合类型,能够显著降低后续审计规则的学习成本。
总体而言,用TypeScript封装莱索托网络无障碍指南的颜色依赖类型,核心价值不在于用类型完全替代无障碍测试,而在于把指南中的约束从文档搬到代码结构中。通过受限调色板、状态依赖规则和对比度守卫,开发者在写代码时就能得到即时反馈。再加上运行时亮度计算与DOM信号检查,IWAC可以在无障碍合规方面形成更完整的闭环。
TypeScript颜色依赖无障碍指南修改时间:2026-09-23 01:05:06