阿联酋网络无障碍指南对颜色依赖关系的要求非常明确:前景色和背景色不是两个独立变量,而是一组必须共同满足对比度阈值的约束。在IWAC组件体系中,颜色通常以token形式存在,如果只用string类型描述文本色和背景色,开发阶段很难阻止表面合理、实际对比度不足的组合。借助TypeScript的模板字面量类型和条件类型,可以将这些颜色依赖前置到编译期,让不合规配色在IDE里就直接报错。

具体的做法是把颜色对比度从文档约束转成类型映射,再通过IWAC主题接口向外暴露。接下来从对比度计算、条件类型建模、主题集成以及维护策略四个方面展开。
颜色依赖为什么必须从值约束提升为类型约束
普通的颜色属性通常写成color?: string,这种写法只能保证传入的是字符串,无法检查前景色在背景色上是否清晰。阿联酋网络无障碍指南沿用了国际通行的对比度要求:普通文本至少需要4.5:1,大号文本和界面组件至少需要3:1。对于政务、医疗和金融服务类界面,这个要求更严格,因为用户群体中可能存在低视力或色觉障碍人群。
颜色依赖的核心是相对亮度。前景色和背景色各自的亮度决定了对比度,而不是简单的色相差异。比如把浅蓝色文字放在白色背景上,色相不同但亮度接近,阅读体验仍然很差。IWAC封装颜色时如果只保留token名称,不保留token之间的关系,设计系统换一套调色板后,原有类型声明就可能失真。因此先要建立统一的调色板,并在值空间完成对比度计算。
下面这段代码定义了最小化的调色板,并实现了对比度计算函数。后续的类型约束都建立在同一个调色板数据之上,避免类型映射和运行时计算各说各话。
type ColorToken = 'brand' | 'accent' | 'surface' | 'text';
type HexColor = `#${string}`;
const palette: Record<ColorToken, HexColor> = {
brand: '#0B5FFF',
accent: '#FFB800',
surface: '#FFFFFF',
text: '#101820'
};
function relativeLuminance(hex: HexColor): number {
const value = hex.replace('#', '');
const r = parseInt(value.slice(0, 2), 16) / 255;
const g = parseInt(value.slice(2, 4), 16) / 255;
const b = parseInt(value.slice(4, 6), 16) / 255;
const channel = (c: number) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
}
function contrastRatio(fg: HexColor, bg: HexColor): 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);
}
用条件类型表达前景与背景的合规关系
TypeScript的类型系统不适合做浮点亮度计算,也不应该把复杂的数学过程搬进类型层。更稳妥的做法是先在运行时计算好每个token组合的对比度,再把结论固化成布尔映射表。类型层只需要读取这个映射,就能判断某种前景色是否允许出现在指定背景上。
下面的ContrastMap是一个嵌套映射,键是前景色,值是背景色的允许状态。比如ContrastMap.brand.surface为true,表示品牌蓝可以作为白色背景上的前景使用。映射表必须与实际对比度计算结果保持一致,可以由脚本自动生成。
接着用条件类型从映射中推导可选前景。ForegroundOn<B>会遍历所有ColorToken,只保留映射值为true的项。CompliantPair再把这些选项展开成联合类型,最终形成可供外部使用的合规颜色对。
type ContrastMap = {
brand: { surface: true; accent: false; text: false };
surface: { brand: true; accent: false; text: true };
accent: { brand: false; surface: false; text: true };
text: { surface: true; accent: true; brand: false };
};
type ForegroundOn<B extends ColorToken> = {
[F in ColorToken]: ContrastMap[F][B] extends true ? F : never
}[ColorToken];
type CompliantPair = {
[B in ColorToken]: { bg: B; fg: ForegroundOn<B> }
}[ColorToken];
declare function applyTextColor<B extends ColorToken>(
bg: B,
fg: ForegroundOn<B>
): void;
applyTextColor('surface', 'text');
applyTextColor('surface', 'brand');
applyTextColor('surface', 'accent'); // 编译错误:对比度不足
接入IWAC主题配置与运行时兜底
IWAC通常会把主题配置作为独立接口暴露出来,颜色token和颜色对可以集中在同一个对象里管理。我们可以定义IWACColorTheme,要求其中的pairs数组只能包含CompliantPair类型的项。这样任何试图加入不合规组合的提交都会在编译阶段失败。
例如在主题文件中写入{ bg: 'surface', fg: 'accent' }会立刻被类型系统拦截,因为ForegroundOn<'surface'>只包含text和brand。如果设计系统更新了色值,对比度改变,只需要重新生成ContrastMap和pairs,不需要修改组件内部的颜色逻辑。
类型检查虽然覆盖了token场景,但运行时仍需兜底。当颜色来自用户输入、远程配置或动态渐变时,类型无法提前判断。因此保留一个assertContrast函数,在开发环境或CI中做最终校验仍然必要。
interface IWACColorTheme {
palette: Record<ColorToken, HexColor>;
pairs: ReadonlyArray<CompliantPair>;
}
const iwacTheme: IWACColorTheme = {
palette,
pairs: [
{ bg: 'surface', fg: 'text' },
{ bg: 'surface', fg: 'brand' },
{ bg: 'text', fg: 'surface' },
{ bg: 'text', fg: 'accent' }
]
};
function assertContrast(fg: ColorToken, bg: ColorToken): boolean {
return contrastRatio(palette[fg], palette[bg]) >= 4.5;
}
维护建议与常见误区
一个常见误区是尝试在TypeScript类型层直接实现对比度公式。类型系统虽然强大,但处理浮点乘方、条件分支和迭代会迅速让编译变慢,可读性也差。正确做法是用脚本读取设计系统导出的JSON,计算所有token两两之间的对比度,再生成ContrastMap类型声明。这样既保持类型安全,又不会让类型定义变成难以维护的数学工具。
另一个容易忽略的问题是token名称和实际色值不同步。例如品牌色由蓝色调成橙色,但ContrastMap仍然沿用旧的允许关系,类型检查就会产生虚假的安全感。因此在CI中加一步重新生成并比对映射表,或者用测试断言每个CompliantPair的实际对比度,可以有效防止漂移。
最后,颜色依赖只是阿联酋网络无障碍指南的一部分。IWAC组件还需要配合语义标签、焦点可见性、屏幕阅读器文本等检查。类型约束解决的是编译期可见的颜色组合问题,不能替代完整的无障碍审计,但它能把最容易忽视的对比度问题挡在最早期。
TypeScriptIWAC颜色依赖类型修改时间:2026-09-23 07:07:02