科特迪瓦在推进电子政务和公共信息服务数字化的过程中,逐步建立了本国的网络无障碍指南体系,业内常简称为IWAC。这套指南大量参考了WCAG国际标准,其中非文本内容(Non-text Content)是最基础也最容易出错的一类要求。本文将介绍如何使用TypeScript的类型系统,把IWAC中关于非文本内容的规定封装成可复用、可校验的类型模型,让无障碍要求从文档约定变成编译期的硬约束。

为什么非文本内容需要类型化封装
IWAC对非文本内容的定义与WCAG 1.1.1类似:任何以图片、图标、音频、视频、图表形式呈现的信息,都必须提供等价的文本替代。问题在于,传统开发流程中这些要求只存在于设计文档里,开发者忘记写alt属性、把装饰图和内容图混为一谈、给验证码配上毫无意义的替代文本,这些都只能在评审或测试时被发现。
如果用TypeScript把这些规范抽象成类型,情况就完全不同了。装饰性图像必须显式声明为装饰用途,内容图像必须携带替代文本,复杂图表必须提供长描述,验证码必须给出替代验证方式。任何不符合规范的写法都会在编译阶段直接报错,无障碍缺陷的发现成本从测试阶段提前到了编码阶段。
更重要的是,类型模型一旦建立,就可以在整个团队的组件库中复用。设计者按类型字段填写信息,开发者按类型约束编写代码,评审者按类型检查结果,三方使用同一套契约,沟通成本大幅降低。
定义非文本内容的类型体系
首先梳理IWAC中非文本内容的主要分类。我们可以用一个可辨识联合类型来建模,每种类别对应一组必填字段。下面是核心类型定义:
// 非文本内容类别的枚举,与IWAC条款一一对应
export enum NonTextKind {
InformativeImage = 'informative-image', // 内容图像
DecorativeImage = 'decorative-image', // 装饰性图像
FunctionalImage = 'functional-image', // 功能图像(按钮、链接中的图标)
ComplexGraphic = 'complex-graphic', // 复杂图表
AudioContent = 'audio-content', // 音频内容
VideoContent = 'video-content', // 视频内容
Captcha = 'captcha', // 验证码
}
// 基础接口:所有非文本内容共享的字段
interface NonTextBase {
id: string;
kind: NonTextKind;
src: string;
}
// 装饰性图像:替代文本必须为空字符串,表示对屏幕阅读器隐藏
interface DecorativeImage extends NonTextBase {
kind: NonTextKind.DecorativeImage;
alt: '';
}
// 内容图像:必须提供非空替代文本
interface InformativeImage extends NonTextBase {
kind: NonTextKind.InformativeImage;
alt: string;
}
// 复杂图表:除短替代文本外还需要长描述的指向
interface ComplexGraphic extends NonTextBase {
kind: NonTextKind.ComplexGraphic;
alt: string;
longDescriptionId: string;
}
// 音频内容:必须提供文字稿
interface AudioContent extends NonTextBase {
kind: NonTextKind.AudioContent;
transcriptUrl: string;
durationSeconds: number;
}
// 视频内容:必须提供字幕文件,手语可选
interface VideoContent extends NonTextBase {
kind: NonTextKind.VideoContent;
captionUrl: string;
transcriptUrl: string;
signLanguageUrl?: string;
}
// 验证码:必须提供至少一种无障碍替代方案
interface Captcha extends NonTextBase {
kind: NonTextKind.Captcha;
alternativeModes: ('audio' | 'text-question' | 'math')[];
}
// 可辨识联合类型
export type NonTextContent =
| DecorativeImage
| InformativeImage
| ComplexGraphic
| AudioContent
| VideoContent
| Captcha;</code>这个设计的核心思路是利用可辨识联合(discriminated union),以kind字段作为判别依据。TypeScript会根据kind的值自动收窄类型,例如当kind为DecorativeImage时,alt字段必须是空字符串字面量类型,任何其他写法都无法通过编译。这种字面量类型的约束看似严格,却恰好对应了IWAC的语义:装饰性图像的alt本就应该为空,写上文字反而是违规的。
编写类型守卫与运行时校验
类型系统只在编译期生效,而CMS或后端接口返回的数据在运行时没有类型保障。因此还需要一套与类型定义对齐的类型守卫函数,用于校验外部数据是否满足IWAC要求:
// 检查是否为合法的非空替代文本
function isValidAlt(alt: string): boolean {
return alt.trim().length > 0 && !/^(图片|image|photo)$/i.test(alt.trim());
}
// 通用的运行时守卫
export function isAccessibleContent(
content: unknown
): content is NonTextContent {
if (typeof content !== 'object' || content === null) {
return false;
}
const c = content as Record<string, unknown>;
switch (c.kind) {
case NonTextKind.DecorativeImage:
// 装饰图:alt 必须为空字符串
return c.alt === '';
case NonTextKind.InformativeImage:
// 内容图:alt 不能是空串,也不能是图片、image 这类无意义词
return typeof c.alt === 'string' && isValidAlt(c.alt);
case NonTextKind.ComplexGraphic:
return (
typeof c.alt === 'string' &&
isValidAlt(c.alt) &&
typeof c.longDescriptionId === 'string' &&
c.longDescriptionId.length > 0
);
case NonTextKind.VideoContent:
return typeof c.captionUrl === 'string' &&
typeof c.transcriptUrl === 'string';
case NonTextKind.Captcha:
return Array.isArray(c.alternativeModes) &&
c.alternativeModes.length > 0;
default:
return false;
}
}注意isValidAlt函数里对“图片”、“image”这类无意义替代文本做了拦截。这是实际项目中最常见的一类伪合规:开发者为了通过检查工具,给所有图片统一填上“图片”二字。检查工具确实不再报缺失alt的错误,但对屏幕阅读器用户来说毫无价值。把这类反模式写进校验逻辑,可以在数据源头就堵住漏洞。
封装React组件并落地到实际场景
有了类型体系,下一步是封装通用组件。以一个无障碍图像组件为例,它接收前面定义的联合类型作为属性,内部根据kind自动生成正确的标记结构:
import React from 'react';
import { NonTextContent, NonTextKind } from './types';
interface AccessibleMediaProps {
content: NonTextContent;
}
export function AccessibleMedia({ content }: AccessibleMediaProps) {
switch (content.kind) {
case NonTextKind.DecorativeImage:
// 装饰图:alt 为空,对辅助技术不可见
return <img src={content.src} alt="" aria-hidden="true" />;
case NonTextKind.InformativeImage:
return <img src={content.src} alt={content.alt} />;
case NonTextKind.ComplexGraphic:
return (
<figure>
<img src={content.src} alt={content.alt}
aria-describedby={content.longDescriptionId} />
<figcaption id={content.longDescriptionId}>
{/* 长描述内容 */}
</figcaption>
</figure>
);
case NonTextKind.VideoContent:
return (
<video src={content.src} controls>
<track kind="captions" src={content.captionUrl} default />
</video>
);
default:
return null;
}
}这个组件的价值在于收敛了所有分支逻辑。业务开发者不需要记住IWAC的每条细则,只需要构造一个符合NonTextContent类型的对象,剩下的事情交给组件处理。当团队规约更新时,例如要求视频必须同时提供手语翻译,只需要修改VideoContent接口并调整组件渲染,所有调用点会立即收到编译错误提示,逐一修复即可,不会遗漏。
在表单验证码场景中,这套模型同样有效。验证码是IWAC审查的重点对象,纯视觉验证码对盲人用户是彻底的障碍。通过Captcha接口的alternativeModes字段,团队可以强制每个验证码组件至少注册一种替代模式,并在CI流程中扫描配置文件,确保没有配置替代模式的验证码被合入主干分支。
持续校验与工程化建议
类型封装完成后,建议把它接入持续集成流程。一方面可以在构建阶段运行针对CMS导出数据的批量校验,输出一份不合规内容清单;另一方面可以配合axe等自动化无障碍测试工具,在端到端测试中复查运行时渲染结果,形成静态检查与动态检查的双保险。
需要注意的是,类型系统能保证结构正确,但不能保证替代文本的内容质量。一张折线图的alt写得好不好,机器暂时无法判断。因此建议在团队内部建立替代文本的写作规范和评审清单,把“描述数据传达的信息,而不是描述图像外观”这类原则沉淀为文档,与类型封装配合使用。
总的来说,用TypeScript封装IWAC非文本内容规范,本质上是把政策文档翻译成机器可执行的契约。这种做法投入不大,一次建模长期受益,尤其适合内容类型多、更新频繁的政务类和公共服务类网站,也为后续对接更完整的WCAG合规审计打下了坚实的技术基础。
TypeScriptIWAC网络无障碍修改时间:2026-09-05 10:50:42