在国际化项目中处理无障碍需求时,很多团队只关注WCAG,却忽略了目标市场自己的标准。俄罗斯的国家标准GOST R 52872-2019对网页内容可访问性有独立规定,其中关于颜色的部分尤其严格:文本前景色与背景色之间的亮度对比必须达到指定阈值,并且不允许仅用颜色传达信息。如果项目基于IWAC这类无障碍组件库构建设计系统,就可以把这套规则直接编码进TypeScript的类型层,让编译器替我们把关。

一、理解GOST R 52872中的颜色规则
在动手写类型之前,必须先弄清楚标准到底规定了什么。GOST R 52872-2019借鉴了WCAG 2.0的框架,但在具体条目上有本地化调整。与颜色直接相关的核心要求可以归纳为三条:第一,普通文本的前景与背景亮度对比度不低于4.5:1,大号文本不低于3:1,这一点与WCAG AA级别一致;第二,不允许单独依赖颜色来区分状态或传达警告,必须辅以文字、图标或形状线索;第三,交互组件的视觉焦点边界与相邻区域的对比度也要满足最低要求。
这三条规则翻译到类型系统里,对应的是三个不同的建模方向:对比度是数值计算问题,需要用字面量联合类型或品牌类型表达等级;禁止纯颜色状态是组合约束问题,需要在类型层面强制状态组件必须携带非颜色线索;焦点边界则是组件属性之间的依赖关系。理解了这一层映射,后面的类型设计就有了清晰的骨架。
需要提醒的是,GOST是面向俄语环境的规范,而色觉障碍人群对红色和绿色的区分困难是全球性的。因此在定义色相类型时,不要只考虑标准条文,还要把红绿色盲友好的约束一并纳入设计,这也是IWAC这类库一贯的做法。
二、定义基础色相类型与对比度等级
第一步是把颜色空间本身类型化。不要直接用string表示颜色,那样等于放弃约束。用模板字面量类型可以把合法的十六进制颜色收敛成可校验的结构:
type HexDigit = '[0-9a-fA-F]';
type HexColor = `#${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}`;
// 对比度等级:对应GOST R 52872的文本分级
type ContrastLevel =
| 'AAA' // 7:1 以上,增强级
| 'AA' // 4.5:1 以上,普通文本
| 'AA-Large' // 3:1 以上,大号文本
| 'Fail'; // 不达标接下来是对比度的计算与判定。TypeScript类型系统本身做不了浮点运算,但可以借助一个运行时工具函数配合类型谓词,实现类型收窄:
function relativeLuminance(hex: HexColor): number {
const n = hex.slice(1).match(/.{2}/g)!
.map(h => parseInt(h, 16) / 255)
.map(c => (c <= 0.03928) ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
return 0.2126 * n[0] + 0.7152 * n[1] + 0.0722 * n[2];
}
export function contrastRatio(fg: HexColor, bg: HexColor): number {
const [l1, l2] = [relativeLuminance(fg), relativeLuminance(bg)].sort((a, b) => b - a);
return (l1 + 0.05) / (l2 + 0.05);
}这个函数在运行时给出精确数值,类型层面则由ContrastLevel联合类型描述结果。两者的配合方式是:工具函数返回带判别字段的判定对象,业务代码通过switch或类型守卫处理,编译器会强制每个分支都被覆盖,漏掉Fail分支时会产生编译错误,这正是我们想要的效果。
三、用品牌类型封装合规颜色组合
光有数值计算还不够,真正有价值的是让不合规的配色根本无法被构造出来。品牌类型是经典手段。核心思路是:普通HexColor不能直接传给组件的fg/bg属性,必须先通过一个验证函数,拿到带品牌标记的类型。
declare const brand: unique symbol;
type GostCompliantColor<L extends ContrastLevel> =
HexColor & { readonly [brand]: L };
type ColorPair = {
fg: GostCompliantColor<'AA' | 'AAA'>;
bg: GostCompliantColor<'AA' | 'AAA'>;
level: ContrastLevel;
};
// 唯一的合法入口:运行时校验 + 类型提升
export function verifyPair(
fg: HexColor, bg: HexColor, isLargeText = false
): ColorPair | never {
const ratio = contrastRatio(fg, bg);
const threshold = isLargeText ? 3 : 4.5;
if (ratio < threshold) {
throw new Error(
`对比度 ${ratio.toFixed(2)}:1 低于GOST R 52872要求的 ${threshold}:1`
);
}
const level = ratio >= 7 ? 'AAA' : isLargeText ? 'AA-Large' : 'AA';
return { fg: fg as ColorPair['fg'], bg: bg as ColorPair['bg'], level };
}IWAC组件的颜色属性就可以直接声明为ColorPair类型。这样任何绕过verifyPair、直接拼字符串的做法都会在编译期报错。团队里新成员即使没读过GOST文档,也不可能在代码里悄悄引入一个对比度不达标的组合。
四、禁止纯颜色状态:组合约束的类型表达
标准明确要求不能仅凭颜色区分状态,这意味着一个红色边框的错误提示必须同时携带图标或文字。这个规则很适合用联合类型中的互斥分支表达:
type NonColorCue =
| { icon: string; label?: string }
| { label: string };
type StatusIndicator = {
state: 'error' | 'warning' | 'success';
color: GostCompliantColor<'AA-Large'> | GostCompliantColor<'AA'>;
} & NonColorCue; // 必须提供非颜色线索才能通过类型检查
// 使用示例:缺少icon或label会直接编译失败
const bad: StatusIndicator = {
state: 'error',
color: verifyPair('#d32f2f', '#ffffff').fg
// Error: 缺少 NonColorCue 中的必需属性
};这里的技巧在于用交叉类型把颜色属性和线索属性绑定在一起,TS的过量属性检查会确保对象字面量必须同时提供icon或label。如果组件库还支持主题切换,可以进一步用泛型把主题令牌映射到已验证的ColorPair,让所有主题在发布前都跑一遍对比度校验。
五、落地建议与边界情况
把规范编码进类型系统并非没有代价。首先,品牌类型会让代码签名变长,建议配合类型别名和良好的命名空间收敛复杂度;其次,运行时校验函数应在构建阶段或CI中调用,而不是每次渲染都执行,可以用代码生成思路预先产出主题令牌的验证结果;最后,GOST R 52872与WCAG 2.0大体对齐但细节有差异,如果产品同时面向欧盟和俄语市场,建议把ContrastLevel做成可扩展的枚举,按地区加载不同阈值配置。
还有一类边界情况值得注意:透明度和渐变背景。当前的字面量类型只覆盖纯色,遇到rgba或渐变时需要先在工具层合成出等效纯色再校验,否则类型约束会形同虚设。可以考虑为IWAC扩展一个resolvedColor中间层,专门处理叠加计算。
总体来看,用TypeScript封装颜色依赖类型的收益在于把一份静态PDF规范变成编译期约束,规范变更时只需修改类型定义和验证函数,所有使用方立刻获得编译反馈。这种规范即代码的思路,同样适用于其他可量化的无障碍要求,值得在设计系统建设初期就纳入规划。
TypeScriptIWAC无障碍颜色修改时间:2026-09-08 13:35:13