圣多美和普林西比网络无障碍指南在非文本内容方面沿用了国际通行的原则:页面上的每一张图片、每一个音频或视频资源,都必须提供等价的替代形式,或者在确属装饰用途时显式声明,避免让辅助技术读到无意义的内容。在实际项目中,这类要求通常只能依赖人工审查或事后扫描工具来兜底,缺陷发现得晚、修复成本也高。如果我们在编码阶段就把这些约束写进类型系统,借助TypeScript的编译检查,缺失替代文本的资源在写代码的那一刻就会报错。下面以IWAC框架为例,讲解一套完整的封装思路。

一、为什么用判别联合来建模非文本内容
非文本内容最大的特点是分类明确但结构各异。一张信息图需要详细的alt描述,一段音频需要文字稿链接,一个装饰性图标则需要明确标记为可忽略。如果用一个大而全的接口把所有字段都设为可选,类型就失去了约束力——开发者很容易漏掉关键字段。判别联合(Discriminated Union)正是解决这个问题的利器:每个分类有自己的字面量类型作为标记,TypeScript可以在收窄类型后强制要求对应的必填字段。
先定义基础分类。根据指南要求,非文本内容通常分为信息性、装饰性、功能性、复杂内容和时基媒体五大类:
// 非文本内容的角色分类 export type NonTextRole = | 'informative' // 信息性:承载内容含义,必须有替代文本 | 'decorative' // 装饰性:无含义,显式标记为忽略 | 'functional' // 功能性:作为控件使用,需描述操作目的 | 'complex' // 复杂内容:如图表,需长描述 | 'time-based'; // 时基媒体:音视频,需字幕或文字稿
有了角色标记,接下来就可以为每个分支定义独立的接口,再合并成一个联合类型。这样写的类型在编辑器里的自动补全体验也会更好:当你把role写成decorative时,TypeScript会立刻提示你不需要填写alt字段;写成informative时则会强制你提供非空的替代文本。这种即时反馈比任何文档都直观。
二、核心类型设计与实现
下面是完整的类型定义。注意几个细节:替代文本使用非空字符串的品牌类型约束长度下限,功能性内容要求描述动作而非外观,时基媒体要求提供字幕或文字稿二者之一,复杂内容要求长描述的引用地址。
// 非空字符串类型,禁止空串或纯空格
type NonEmptyString = string & { readonly __brand: 'NonEmpty' };
export interface InformativeContent {
role: 'informative';
alt: NonEmptyString; // 简短替代文本,建议不超过125字符
longDescriptionUrl?: string; // 可选的长描述地址
}
export interface DecorativeContent {
role: 'decorative';
alt: ''; // 装饰性图片alt必须为空串
ariaHidden: true; // 必须对辅助技术隐藏
}
export interface FunctionalContent {
role: 'functional';
action: NonEmptyString; // 描述操作结果而非外观
alt: NonEmptyString;
}
export interface ComplexContent {
role: 'complex';
alt: NonEmptyString;
longDescriptionUrl: NonEmptyString; // 复杂内容必须有长描述
}
export interface TimeBasedContent {
role: 'time-based';
mediaType: 'audio' | 'video';
transcriptUrl: NonEmptyString; // 文字稿必须提供
captionsUrl?: string; // 视频还建议提供字幕
}
export type NonTextContent =
| InformativeContent
| DecorativeContent
| FunctionalContent
| ComplexContent
| TimeBasedContent;品牌类型NonEmptyString是这套设计的关键防线。普通的string类型允许空字符串,而空字符串正是无障碍缺陷最常见的来源——开发者填了alt字段,值却是空的。通过品牌类型,所有替代文本必须经过一个工厂函数产生,工厂函数内部做去空格、校验长度等运行时检查:
export function nonEmpty(value: string, maxLength = 125): NonEmptyString {
const trimmed = value.trim();
if (trimmed.length === 0) {
throw new Error('替代文本不能为空或纯空格');
}
if (trimmed.length > maxLength) {
throw new Error(`替代文本建议不超过${maxLength}字符,请考虑改用长描述`);
}
return trimmed as NonEmptyString;
}三、在IWAC组件层消费这些类型
类型定义好之后,要让它们真正发挥作用,需要接入IWAC的组件层。以一个图片组件为例,将NonTextContent作为props类型,并在渲染函数中按角色分支处理。装饰性内容渲染时输出空的alt属性并附加aria-hidden;功能性内容确保替代文本与控件语义绑定;时基媒体则负责注入字幕轨道。
import { h, render } from 'iwac';
export function AccessibleImage(props: NonTextContent) {
switch (props.role) {
case 'decorative':
return h('img', { src: props.src, alt: '', 'aria-hidden': 'true' });
case 'functional':
return h('img', { src: props.src, alt: props.alt, title: props.action });
case 'complex':
return h('figure', {},
h('img', { src: props.src, alt: props.alt }),
h('a', { href: props.longDescriptionUrl }, '查看详细描述')
);
default:
return h('img', { src: props.src, alt: props.alt });
}
}除了组件层,还可以提供一层守卫函数,在数据来源于CMS或接口时做运行时兜底。静态类型检查覆盖编译期,运行时校验覆盖数据流入口,两者结合才能形成完整闭环。守卫函数的实现可以借助类型谓词:
export function isDecorative(
content: NonTextContent
): content is DecorativeContent {
return content.role === 'decorative';
}
// 在数据处理管道中过滤与告警
function validateBatch(items: NonTextContent[]) {
return items.filter(item => {
const ok = item.role !== 'informative' || item.alt.trim().length > 0;
if (!ok) console.warn('发现缺失替代文本的资源', item);
return ok;
});
}这套方案的收益可以归纳为三点:其一,约束前置到编码阶段,无障碍缺陷的修复成本从上线后的工单降为编辑器里的一条红色波浪线;其二,类型即文档,新成员加入团队时通过类型定义就能理解非文本内容的分类规则;其三,判别联合结构天然适配序列化,与CMS的数据模型可以直接对接,不需要额外的转换层。当然也要注意,类型系统无法替代人工对替代文本质量的判断——一段敷衍的替代文本在类型上和一段精准的描述完全等价,最终的质量仍需结合审查流程与测试来保证。
TypeScriptIWAC无障碍修改时间:2026-09-06 12:46:35