广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何用TypeScript为IWAC封装波多黎各网络无障碍指南的表单标签类型?

在 TypeScript 组件库里,表单标签不应该只是一个 string。如果 IWAC 的输入框、单选框或文本域只接收一个 label 字符串,开发者把 legend 或 aria-describedby 的文本误传进去时,编译不会报错,最终屏幕阅读器读到的是错误信息。波多黎各网络无障碍指南要求每个表单控件都有明确的可编程标签,并且标签的类型要与控件的语义匹配。因此封装专门的标签类型是第一步。

如何用TypeScript为IWAC封装波多黎各网络无障碍指南的表单标签类型?

一、字符串标签带来的类型盲区

很多组件库在定义表单控件时,习惯把 label 直接写成 string 类型。比如一个输入框组件只需要接收 label 属性,调用方传什么文本都可以,这看起来灵活,实际上丢失了无障碍层面最关键的语义区分。波多黎各网络无障碍指南并不把所有文本都当作同一种标签,它要求可见标签使用 <label> 元素关联控件,分组信息使用 <legend> 元素,补充描述使用 aria-describedby,而无可见文本时使用 aria-label 或 aria-labelledby。

如果这些差异全部混在一个字符串里,IWAC 组件就无法判断开发者到底想表达哪种语义。比如一个单选框组的标题应该用 legend,但开发者可能误把它传给普通 label 属性;一个错误提示本来应该绑定 aria-describedby,结果变成了可见的 label 文本。这些错误在运行时不会导致崩溃,却会让屏幕阅读器用户听到混乱的信息,最终无法完成表单填写。

更麻烦的是,字符串类型没有任何编译期约束。当 IWAC 组件库被多个项目复用时,新加入的开发者很难从接口定义中看出每个字段应该如何配置。只能依靠文档或猜测,这正好是类型系统应该解决的问题。

type LegacyProps = {
  label: string;
  error?: string;
  description?: string;
};

上面这种定义虽然短,但带来了大量隐性规则:error 要不要与控件关联?description 是可见文本还是辅助文本?label 是否允许为空?这些规则都没有体现在类型上。因此需要把表单标签从 string 提升为强类型。

二、用联合类型拆解无障碍指南中的标签语义

波多黎各网络无障碍指南对表单标签的分类可以归纳为五类:可见标签、分组标签、可见替代标签、引用标签和补充说明。对应到 HTML 与 ARIA 中,分别是 label、legend、aria-label、aria-labelledby 和 aria-describedby。先定义一个基础的 kind 联合类型,让 IWAC 的调用方只能在限定范围内选择。

type FormLabelKind =
  | 'label'
  | 'legend'
  | 'aria-label'
  | 'aria-labelledby'
  | 'aria-describedby';

这样做的好处是,任何拼写错误都会立刻被 TypeScript 发现。例如有人写成 'aria-describeby',编译器会提示该字符串字面量不满足联合类型。并且 IDE 也能给出自动补全,调用方不需要记住完整拼写。但仅靠 kind 还不够,因为不同 kind 需要携带的数据不同。

label 需要关联控件的 id,所以必须有一个 forId 字段;legend 是分组标题,通常不需要 forId;aria-label 只需要文本内容;aria-labelledby 需要引用其他元素的 id;aria-describedby 也一样。这些字段的必填和可选关系如果都放在一个接口里,就会变成一个所有字段都可选的大杂烩,仍然无法阻止错误配置。

所以更好的做法是使用判别联合,让 TypeScript 根据 kind 字段自动收窄类型。这样调用方在写 kind: 'label' 时,如果没有提供 forId,就会直接报错。这也符合 IWAC 组件库希望达到的编译期无障碍约束目标。

三、判别联合让错误配置在编译期暴露

判别联合的关键是给每个成员设置一个唯一的 kind 字面量。TypeScript 会把这个字段作为判别依据,在 switch 或 if 分支里自动推断出当前对象的具体形状。下面是一个可直接用于 IWAC 表单控件的 FormLabelConfig 类型定义。

type FormLabelConfig =
  | { kind: 'label'; text: string; forId: string; required?: boolean }
  | { kind: 'legend'; text: string; required?: boolean }
  | { kind: 'aria-label'; text: string }
  | { kind: 'aria-labelledby'; id: string }
  | { kind: 'aria-describedby'; id: string };

这个定义明确了不同标签的必填字段。比如 kind: 'label' 必须有 text 和 forId,而 kind: 'aria-labelledby' 只需要 id,不需要 text。这意味着开发者无法再写出一个没有关联 id 的 label 标签,也无法给 aria-describedby 塞一段错误文本而不是引用元素 id。

判别联合还能让类型收窄变得非常自然。比如在 IWAC 内部处理标签时,可以写一个 switch 函数,根据 config.kind 分别渲染不同的 HTML 结构。由于 TypeScript 知道每个分支里 config 的具体类型,访问 config.forId 或 config.id 都不需要额外的非空断言。这样既能保持代码简洁,又能避免运行时因为字段缺失而报错。

对调用方来说,这种约束并不会增加太多负担。虽然需要多写一个 kind 字段,但换来的收益是每个表单标签的语义都清晰可见。特别是在多人协作的项目中,一个刚接手代码的人只要看到 kind: 'legend',就能立刻明白这段文本会作为分组标题渲染,不需要再去翻组件文档。

四、将标签类型接入 IWAC 表单组件

有了 FormLabelConfig 类型之后,下一步是把它作为 IWAC 表单控件 props 的一部分。输入框、文本域、下拉框和单选框组等组件都可以复用这个类型。以输入框组件为例,props 可以这样定义。

interface IWACInputProps {
  name: string;
  value: string;
  onChange: (value: string) => void;
  labelConfig: FormLabelConfig;
  errorConfig?: Extract<FormLabelConfig, { kind: 'aria-describedby' }>;
}

这里 labelConfig 负责声明主要的标签,errorConfig 则强制错误提示必须通过 aria-describedby 与控件关联。这样做的原因是波多黎各网络无障碍指南要求错误信息不能只是视觉上的红色文本,必须能被屏幕阅读器读取。使用 Extract 从 FormLabelConfig 中筛出 aria-describedby 成员,可以避免开发者把错误信息误配成 legend 或 aria-label。

在 IWAC 组件内部,渲染标签时需要根据 kind 调用不同的 DOM API。对于 label,除了设置文本内容,还要设置 htmlFor 属性;对于 legend,需要在外层 fieldset 中渲染;对于 aria-label,直接设置控件的 aria-label 特性;对于 aria-labelledby 和 aria-describedby,则设置对应的 ARIA 属性。

这种接入方式让 IWAC 的每个表单控件都具备一致的标签行为。不管是在 React、Vue 还是原生 Web Component 中实现,组件内部只需要一个统一的解析函数,就能把类型化配置转换成正确的 HTML 结构。调用方不需要关心底层用什么框架,只需要提供符合指南的标签配置对象。

五、运行时校验与边界处理

TypeScript 的类型约束只在编译期生效,如果组件库需要支持 JavaScript 调用方,或者数据来自外部接口,就可能绕过类型检查。因此 IWAC 还应该提供轻量的运行时校验函数,确保传入的标签配置在运行时也符合预期。

const ALLOWED_KINDS = ['label', 'legend', 'aria-label', 'aria-labelledby', 'aria-describedby'] as const;
function isFormLabelConfig(value: unknown): value is FormLabelConfig {
  if (typeof value !== 'object' || value === null) return false;
  const config = value as Record<string, unknown>;
  return typeof config.kind === 'string'
    && ALLOWED_KINDS.includes(config.kind as FormLabelConfig['kind']);
}

这个函数可以作为 IWAC 表单控件的入口校验。如果传入的对象 kind 不在允许列表中,可以输出一条警告并回退到最基础的 label 行为。虽然不能完全替代编译期检查,但对动态数据场景很有帮助。

边界处理还包括 required 标记。波多黎各网络无障碍指南建议必填字段除了使用星号等视觉提示外,还要在标签文本中加入“必填”说明,或者通过 aria-required 属性声明。FormLabelConfig 中的 required 字段可以帮助 IWAC 自动添加 aria-required 并调整标签文本,避免不同项目各自实现导致不一致。

总的来说,把表单标签从 string 提升为强类型,是 IWAC 向无障碍规范靠拢的关键一步。它不会增加运行时开销,却能把大量无障碍错误拦截在开发阶段。对于需要长期维护的组件库来说,这种投入非常值得。

广告位 · 文章底部(后台-模块管理替换此区域)