冰岛网络无障碍指南(IWAC)对页面标题的要求并不只是“起个名字”那么简单。它要求标题清晰描述页面内容,站点名称和页面名称之间使用固定分隔符,并且不能出现空标题或仅由站点名称组成的标题。把这些规则直接写进TypeScript类型系统,可以让开发者在值赋给变量之前就发现错误,而不是等到上线后再用无障碍审计工具扫描。类型定义在这里扮演的是编译期守门员的角色。

下面从规则拆解、类型实现和工程维护三个角度展开。我们会先分析IWAC页面标题的构成规则,再用模板字面量类型和品牌化类型建立约束,最后讨论测试和维护这类类型定义时的注意点。
IWAC页面标题规则的约束拆解
IWAC的页面标题规则可以归纳为几个硬性条件。第一,标题必须包含页面特定名称,不能只写站点名称。第二,页面名称和站点名称之间需要用固定分隔符连接,指南中通常采用空格加短横线加空格的格式。第三,站点名称需要从允许的站点名集合中取值,避免同一项目里出现中英文名称混用的情况。
把这些约束映射到类型系统后,可以用联合类型表示站点名称,用模板字面量类型表示整个标题的模式。也就是说,如果一个字符串不满足模式,TypeScript会在编译阶段直接报错。这种做法的好处是错误反馈发生在IDE里,而不是在浏览器或CI报告里。
需要注意,无障碍指南中的规则有时比纯字符串模式更复杂。例如标题长度、是否包含隐藏的站点后缀、以及多语言环境下的站点名称可能不同。因此类型定义可以先覆盖结构合法性,剩余的动态规则用运行时校验函数补充。
用模板字面量类型构建基础标题类型
TypeScript从4.1版本开始支持模板字面量类型,这为字符串格式校验提供了直接手段。我们可以先定义站点名称和分隔符的字面量类型,然后用模板字面量类型拼出合法标题。比如下面这个最小实现。
type SiteName = 'Iceland Web Accessibility Centre';
type Separator = ' - ';
type PageTitle = `${string}${Separator}${SiteName}`;
const okTitle: PageTitle = 'Contact page - Iceland Web Accessibility Centre';
const badTitle: PageTitle = 'Iceland Web Accessibility Centre';
// Type error: Type '"Iceland Web Accessibility Centre"' is not assignable to type 'PageTitle'.
上面的类型只允许以站点名称结尾,并且在站点名称之前必须有一个分隔符。由于模板字面量中的${string}会匹配任意字符串,包括空字符串,因此' - Iceland Web Accessibility Centre'也会被认为是合法标题,但实际指南不允许页面名称为空。要解决这个问题,可以把string替换成非空约束,比如使用NonEmptyString或者直接用另一个模板字面量类型。
更严格的做法是定义页面名称的合法字符集或最小长度。但TypeScript模板字面量类型无法直接表示“非空字符串”或“长度大于N”的约束,只能通过品牌化类型和运行时函数配合完成。下一节会展示如何在不牺牲开发者体验的前提下做到这一点。
品牌化类型与构造函数封装
模板字面量类型虽然能约束格式,但无法约束标题的非空性和站点名一致性。这时可以引入品牌化类型。品牌化类型通过给基础类型附加一个唯一的品牌属性,让类型在结构上区别于普通字符串。即使底层仍然是字符串,编译器也会把普通字符串拒绝在品牌类型之外。
下面是一个结合了模板字面量类型和品牌化类型的封装。这里使用泛型来复用品牌逻辑。
type Brand<T, B extends string> = T & { readonly __brand: B };
type SiteName = 'Iceland Web Accessibility Centre';
type Separator = ' - ';
type HeaderPattern = `${string}${Separator}${SiteName}`;
type IWACPageTitle = Brand<HeaderPattern, 'IWACPageTitle'>;
function createIWACPageTitle(raw: string): IWACPageTitle | null {
const trimmed = raw.trim();
const suffix: SiteName = 'Iceland Web Accessibility Centre';
if (!trimmed.endsWith(`${Separator}${suffix}`)) {
return null;
}
const pagePart = trimmed.slice(0, -(Separator.length + suffix.length));
if (pagePart.length === 0) {
return null;
}
return trimmed as IWACPageTitle;
}
const title = createIWACPageTitle('Services - Iceland Web Accessibility Centre');
if (title) {
console.log(title);
}
函数createIWACPageTitle承担了运行时校验职责,返回值类型是IWACPageTitle | null。这样,只有通过校验的字符串才能被当作IWAC合法的页面标题使用。品牌属性__brand在运行时并不存在,它只是一个编译期标记,因此不会影响实际值的内容。
这种封装方式的好处是,外部代码无法直接通过赋值构造IWACPageTitle,必须调用工厂函数。这样就能把指南里的标题规则集中在一个模块中维护,而不需要各个页面组件各自判断。
多语言场景下的类型扩展
IWAC覆盖的站点往往不是只有冰岛语,很多项目会同时提供英语版本。页面标题中的站点名称可能随语言变化,例如冰岛语使用某个名称,英语使用另一个名称。类型定义如果只写死一个站点名称,就难以覆盖多语言场景。
这时可以使用映射类型或对象键值来维护语言与站点名称的对应关系,再通过keyof和模板字面量类型生成每个语言下的标题类型。核心思路没有变化:先定义站点名称集合,再拼接页面名称和分隔符。
type SiteNames = {
is: 'Vefurinn okkar';
en: 'Iceland Web Accessibility Centre';
};
type LanguageCode = keyof SiteNames;
type Separator = ' - ';
type LocalizedPageTitle<L extends LanguageCode> =
`${string}${Separator}${SiteNames[L]}`;
type IcelandicTitle = LocalizedPageTitle<'is'>;
type EnglishTitle = LocalizedPageTitle<'en'>;
const isTitle: IcelandicTitle = 'Þjónusta - Vefurinn okkar';
const enTitle: EnglishTitle = 'Services - Iceland Web Accessibility Centre';
这段代码将站点名称集中到一个对象类型中,语言代码作为键。需要新增语言时,只需在SiteNames里添加成员,所有引用它的标题类型会自动扩展。不过要注意,模板字面量类型中的${string}仍然允许空页面名,因此生产环境还是建议结合品牌化工厂函数使用。
多语言标题还涉及翻译一致性。类型系统无法判断页面名称的翻译是否准确,但可以要求每种语言都必须有对应标题,避免漏翻译。比较常见的做法是定义一个对象,键为语言代码,值为本地化标题,再让页面数据满足该对象类型。
测试与维护建议
类型定义一旦投入使用,就需要配套的编译期测试来防止规则被误改。TypeScript类型测试通常采用赋值断言的方式,利用@ts-expect-error注释确认非法值会被编译器拒绝。
// 合法标题应该可以赋值 const valid: EnglishTitle = 'Contact - Iceland Web Accessibility Centre'; // @ts-expect-error 缺少分隔符,应当报错 const invalid1: EnglishTitle = 'Contact Iceland Web Accessibility Centre'; // @ts-expect-error 站点名称不一致,应当报错 const invalid2: EnglishTitle = 'Contact - Another Organisation';
除了编译期测试,运行时工厂函数也需要单元测试,覆盖空字符串、没有分隔符、站点名称拼写错误、前后有空格等输入。这样可以保证类型定义和运行时校验的逻辑一致。若未来IWAC指南调整分隔符或站点名称,只需修改常量类型和工厂函数,并在测试中确认新旧行为符合预期。
维护这类类型定义时,建议把字符串常量集中到单独的constants.ts文件中,类型和值保持同步。可以使用as const让TypeScript推断出字面量类型,避免手写联合类型时漏项。定期阅读无障碍指南更新也是必要的,尤其是涉及多语言标题结构和站点名称变更的部分。

