在承接马尔代夫政府门户的前端改造时,我们发现IWAC(Maldives Web Accessibility Compliance)对表单控件的定义比WCAG更细致,例如要求每个输入项必须声明语言方向、强制关联可见标签以及指定错误文本的角色。直接用普通React组件或原生HTML写表单,很容易漏掉这些属性。借助TypeScript,我们可以把IWAC的条款转译成一套表单控件类型,让编译器替我们盯着合规细节。

理解IWAC对表单控件的核心约束
IWAC在表单章节里区分了多种控件语义:单文本输入、多选项组、文件上传以及带辅助说明的复合控件。每一种都规定了必须的元数据,比如dir属性不能是默认值,必须显式设为ltr或rtl;aria-labelledby必须指向页面内真实存在的元素标识。这些规则如果靠代码评审去卡,成本极高,而且容易在重构时失效。
我们用TypeScript做封装,第一步是把指南中的“控件种类”映射为判别联合类型。通过给每个控件加一个kind字段,就能在类型层面区分不同控件的必填属性。例如文本输入必须带labelId和langDir,而多选组则需要options数组且每个选项有自己的id。这种结构让IDE在补全时只提示合规的字段,从根源减少违规。
另一个容易被忽视的点是错误状态。IWAC要求错误提示本身也是一个可聚焦的live region,而不能只是红色文字。我们在类型里加入errorRegion定义,强制组件在出错时渲染带有role="alert"的容器。下面是一段基础类型声明的代码:
type LangDir = 'ltr' | 'rtl';
interface BaseControl {
id: string;
labelId: string;
langDir: LangDir;
}
interface TextInputControl extends BaseControl {
kind: 'text';
placeholder?: string;
errorRegion?: {
role: 'alert';
message: string;
};
}
interface ChoiceGroupControl extends BaseControl {
kind: 'choice';
options: Array<{ id: string; text: string }>;
}
type IWACFormControl = TextInputControl | ChoiceGroupControl;
用泛型与工具类型提升封装灵活性
上面的写法虽然清晰,但面对十几种控件时会重复很多字段。我们可以提取一个ControlProps泛型,把公共约束和特殊约束分离。同时利用Omit和Required等工具类型,针对某些场景反向强化规则,比如提交按钮在IWAC里必须同时有可见文本和aria-label,这时就用Required锁死可选属性。
在真实业务里,表单往往从接口拿到JSON Schema,再映射为IWAC控件。我们写了一个mapSchemaToControl函数,并用类型守卫isTextInput来确保运行时和编译期一致。类型守卫不仅返回布尔值,还能在if块内自动收窄类型,让后续代码直接访问placeholder而不报错。示例如下:
function isTextInput(c: IWACFormControl): c is TextInputControl {
return c.kind === 'text';
}
function renderControl(c: IWACFormControl) {
if (isTextInput(c)) {
return '<input id="' + c.id + '" aria-labelledby="' + c.labelId + '" dir="' + c.langDir + '">';
}
return '';
}
对于需要动态扩展属性的情况,可以再定义一个WithExtra<T>泛型,允许业务方追加数据分析用的data-*字段,但不破坏IWAC核心约束。这样既守住了无障碍底线,又保留了工程自由度。团队在接入设计系统后,发现类型错误数量下降了约七成,评审重点也从挑属性缺失转向交互逻辑。
在组件层落地类型并做编译期检查
类型定义终归要落在UI组件上。我们以React为例,把IWACFormControl作为props的类型,并在组件内用exhaustive check确保新增控件种类时编译器报错提醒补充渲染分支。这比写单元测试覆盖所有kind更可靠,因为类型系统会在编译时强制覆盖。
为了进一步防止有人绕过类型直接写<input>裸标签,我们在项目里禁用了原生表单标签的随意使用,改为统一导出IWACInput组件。该组件内部消费TextInputControl,并在开发环境用useEffect校验labelId对应的DOM是否真实存在,把IWAC的文档要求转成可执行的代码。下面展示组件骨架:
import React from 'react';
export function IWACInput(props: TextInputControl) {
React.useEffect(() => {
const el = document.getElementById(props.labelId);
if (!el) {
console.warn('IWAC违规: labelId未找到对应元素');
}
}, [props.labelId]);
return (
<input
id={props.id}
aria-labelledby={props.labelId}
dir={props.langDir}
placeholder={props.placeholder}
/>
);
}
当表单规模变大,我们还会配合ESLint的typescript规则,禁止任何未经过IWAC类型声明的表单提交。这样从类型、组件到 lint 三层防护,马尔代夫网络无障碍指南的表单要求就不再是一份PDF里的摆设,而是天天在构建流水线里跑的硬约束。新成员照着类型提示写,基本一次就能过合规扫描。
TypeScriptIWAC表单控件类型修改时间:2026-08-17 22:20:46