EARL(Evaluation and Report Language)是W3C发布的用于表达测试结果的标准词汇表,在无障碍合规检测、自动化质量评估等场景中广泛应用。一份EARL报告通常包含多个断言(Assertion),每个断言描述一个测试主体、测试标准以及对应的结果。结果部分通过结果谓词(Result Predicate)来标记,这些谓词包括 earl:passed、earl:failed、earl:cantTell、earl:inapplicable 和 earl:untested。直接用字符串承载这些谓词虽然简单,但缺乏类型约束,拼写错误、大小写不一致、非法值混入等问题会让后续统计和报告生成变得脆弱。借助TypeScript的字符串字面量类型和联合类型,我们可以将结果谓词封装成编译期可校验的类型定义,使无障碍评估数据处理更加稳健。下面从EARL结果谓词的概念出发,逐步实现一套完整的类型封装方案。

一、EARL结果谓词的核心概念
EARL采用了面向测试结果的语义模型,一个典型的断言由被测主体、测试标准、结果和模式等组成。其中结果部分包含两个关键属性:一个是结果值本身,另一个是结果谓词。结果谓词用来区分测试是否通过、失败、无法判断、不适用或尚未测试。这五种结果谓词在EARL规范中都有明确的含义:passed 表示测试通过,failed 表示测试失败,cantTell 表示由于信息不足无法确定结果,inapplicable 表示测试条件不适用于当前主体,untested 表示尚未执行测试。
在JSON-LD或RDF表示中,结果谓词通常以完整的IRI字符串出现,例如 http://www.w3.org/ns/earl#passed。不少实现为了方便,会直接使用 earl:passed 这样的紧凑形式,但这两种表示在字符串层面并不相同,容易发生混淆。因此,在TypeScript层面对结果谓词进行封装时,需要同时考虑IRI全称和前缀缩写两种场景,并且保证类型安全不会因为字符串格式差异而被破坏。
如果仅仅把结果谓词当作普通字符串,那么在函数参数、对象属性、数据解析等环节中,任何字符串都可以被赋值,编译器无法发现诸如 earl:pass 或 earl:Failed 这样的错误。一旦这些非法值进入评估报告,后续的统计逻辑就可能静默失败,产生错误的合规结论。将结果谓词定义为字符串字面量联合类型,可以从根上限制允许的取值范围,让非法值在编译阶段就被拦截下来。
二、用TypeScript定义结果谓词类型
TypeScript的字符串字面量类型允许我们将一个字符串的精确取值作为类型来使用。对于EARL结果谓词,可以首先定义五种核心谓词对应的字符串字面量类型,然后用联合类型把它们组合起来。这样做的好处是,编辑器会自动提示可选值,而且任何拼写错误都会导致类型检查失败。下面是一个基础的类型定义示例:
type EarlPassed = 'earl:passed'; type EarlFailed = 'earl:failed'; type EarlCantTell = 'earl:cantTell'; type EarlInapplicable = 'earl:inapplicable'; type EarlUntested = 'earl:untested'; type ResultPredicate = EarlPassed | EarlFailed | EarlCantTell | EarlInapplicable | EarlUntested;
上面的代码定义了五个独立的字符串字面量类型,然后用联合类型 ResultPredicate 将它们合并。这样,一个变量如果声明为 ResultPredicate 类型,就只能被赋值为这五种字符串之一。例如,尝试把 'earl:pass' 赋值给该类型的变量,TypeScript会立即报错,提示该值不属于联合类型的成员。
为了在运行时也能方便地使用这些谓词,可以创建一个常量对象,将谓词字符串作为属性值保存。这样既可以在代码中通过对象属性来引用谓词,避免手写字符串,又可以根据对象键值生成类型。TypeScript的 as const 断言可以保留字面量类型,配合 keyof typeof 和索引访问类型,可以自动推导出联合类型。示例代码如下:
const EARL_RESULT_PREDICATES = {
PASSED: 'earl:passed',
FAILED: 'earl:failed',
CANT_TELL: 'earl:cantTell',
INAPPLICABLE: 'earl:inapplicable',
UNTESTED: 'earl:untested',
} as const;
type ResultPredicate = typeof EARL_RESULT_PREDICATES[keyof typeof EARL_RESULT_PREDICATES];
function getResultPredicate(value: string): ResultPredicate | undefined {
const predicates: ReadonlyArray<string> = Object.values(EARL_RESULT_PREDICATES);
return predicates.includes(value) ? (value as ResultPredicate) : undefined;
}
这段代码首先用 as const 将对象的所有属性值锁定为字面量类型,然后通过 keyof typeof EARL_RESULT_PREDICATES 获取属性键的联合类型,再通过索引访问得到属性值的联合类型,最终得到与之前手动定义完全相同的 ResultPredicate。同时,getResultPredicate 函数用来在运行时校验一个任意字符串是否属于合法的结果谓词,如果合法则返回谓词本身,否则返回 undefined。这种模式兼顾了编译期类型安全与运行时数据校验。
还需要注意,EARL结果谓词在实际数据中可能以完整的IRI形式出现,例如 http://www.w3.org/ns/earl#passed。为了兼容这种表示,可以扩展类型定义,同时支持前缀形式和IRI全称。定义两个联合类型,再合并成一个更宽泛的结果谓词类型,或者建立映射函数进行规范化。推荐的做法是统一在数据入口处将IRI全称转换为紧凑的 earl:前缀 形式,这样内部逻辑只需处理一种格式,减少类型分支。
三、封装成可复用的类型模块
将结果谓词类型定义分散在各个文件中容易造成重复和不一致。更合理的做法是创建一个独立的TypeScript模块,集中导出类型、常量、类型守卫和转换函数。模块内部使用 as const 常量对象作为单一数据源,同时导出由该对象推导出的类型。类型守卫函数可以帮助其他代码在运行时安全地收窄类型,例如在解析未知来源的JSON数据时。
下面是一个完整的模块示例,展示了如何将前面讨论的内容组织起来。模块导出了 ResultPredicate 类型、EARL_RESULT_PREDICATES 常量、isResultPredicate 类型守卫以及一个用于规范化IRI的函数:
export const EARL_RESULT_PREDICATES = {
PASSED: 'earl:passed',
FAILED: 'earl:failed',
CANT_TELL: 'earl:cantTell',
INAPPLICABLE: 'earl:inapplicable',
UNTESTED: 'earl:untested',
} as const;
export type ResultPredicate = typeof EARL_RESULT_PREDICATES[keyof typeof EARL_RESULT_PREDICATES];
const IRI_TO_PREDICATE_MAP: Record<string, ResultPredicate> = {
'http://www.w3.org/ns/earl#passed': EARL_RESULT_PREDICATES.PASSED,
'http://www.w3.org/ns/earl#failed': EARL_RESULT_PREDICATES.FAILED,
'http://www.w3.org/ns/earl#cantTell': EARL_RESULT_PREDICATES.CANT_TELL,
'http://www.w3.org/ns/earl#inapplicable': EARL_RESULT_PREDICATES.INAPPLICABLE,
'http://www.w3.org/ns/earl#untested': EARL_RESULT_PREDICATES.UNTESTED,
};
export function normalizePredicate(value: string): ResultPredicate | undefined {
if (isResultPredicate(value)) {
return value;
}
return IRI_TO_PREDICATE_MAP[value];
}
export function isResultPredicate(value: unknown): value is ResultPredicate {
if (typeof value !== 'string') {
return false;
}
return Object.values(EARL_RESULT_PREDICATES).includes(value as ResultPredicate);
}
这个模块的核心思想是:EARL_RESULT_PREDICATES 常量对象既是运行时数据,也是类型推导的来源。类型守卫 isResultPredicate 在运行时检查一个值是否为合法的结果谓词,并在条件为真时将值的类型收窄为 ResultPredicate。这对于处理外部输入非常有用,因为外部数据通常是 unknown 或 any 类型,必须经过运行时验证才能安全使用。
规范化函数 normalizePredicate 先检查输入是否已经是紧凑形式的结果谓词,如果不是,再尝试从IRI映射中查找对应的谓词。这样,无论数据源返回的是 earl:passed 还是完整的IRI,模块都能统一转换为内部使用的紧凑形式。映射表使用了 Record<string, ResultPredicate> 类型,确保键值对中的值只能是合法的结果谓词,避免映射错误。
四、在无障碍评估工具中应用
实际的无障碍评估工具通常会从测试引擎接收JSON格式的评估报告,报告中的每个断言包含 result 字段,该字段内部有 outcome 或 predicate 属性。如果这些JSON数据没有经过TypeScript类型约束,那么后续的统计、过滤和展示逻辑很容易因为非法谓词而出错。引入前面封装好的结果谓词模块后,可以在数据解析层对每一条断言进行校验和规范化。
例如,假设我们收到了一个包含多条断言的数组,每条断言的结构大致如下:
interface RawAssertion {
subject: string;
test: string;
result: {
predicate: string;
message?: string;
};
}
function parseEarlReport(rawData: unknown): RawAssertion[] {
if (!Array.isArray(rawData)) {
throw new Error('EARL报告数据必须是数组');
}
return rawData as RawAssertion[];
}
在拿到原始的 RawAssertion 数组后,我们可以对每条断言的 result.predicate 调用 normalizePredicate,并丢弃无法识别谓词的条目。同时,将原始断言映射为内部使用的强类型结构,其中 predicate 字段的类型被收窄为 ResultPredicate。示例代码如下:
interface TypedAssertion {
subject: string;
test: string;
predicate: ResultPredicate;
message?: string;
}
function buildTypedAssertions(rawAssertions: RawAssertion[]): TypedAssertion[] {
const typed: TypedAssertion[] = [];
for (const raw of rawAssertions) {
const normalized = normalizePredicate(raw.result.predicate);
if (normalized === undefined) {
console.warn('忽略未知的结果谓词:', raw.result.predicate);
continue;
}
typed.push({
subject: raw.subject,
test: raw.test,
predicate: normalized,
message: raw.result.message,
});
}
return typed;
}
经过 buildTypedAssertions 处理后,得到的 TypedAssertion 数组中每个对象的 predicate 都是合法的 ResultPredicate 类型。这样在后续编写统计逻辑时,就可以直接使用 EARL_RESULT_PREDICATES.PASSED 等常量进行比较,而不必担心字符串拼写错误。例如,统计通过率和失败率时可以这样写:
function calculatePassRate(assertions: TypedAssertion[]): number {
const applicable = assertions.filter(a => a.predicate !== EARL_RESULT_PREDICATES.INAPPLICABLE);
const passed = applicable.filter(a => a.predicate === EARL_RESULT_PREDICATES.PASSED);
return applicable.length === 0 ? 0 : passed.length / applicable.length;
}
类型安全不仅体现在编译阶段,还能提升代码可读性和可维护性。当其他开发者阅读统计逻辑时,看到 EARL_RESULT_PREDICATES.PASSED 会比看到硬编码的字符串 'earl:passed' 更直观,也更容易理解该值在EARL语义中的含义。此外,如果未来EARL规范新增了结果谓词,只需要在常量对象中添加一个属性,联合类型会自动扩展,所有使用该类型的地方都会获得新值提示,而不会出现遗漏更新的问题。
五、处理扩展谓词与自定义结果
某些无障碍评估场景可能需要使用EARL规范之外的扩展谓词,例如某些自动化工具自定义的 earl:notTested 或 earl:partiallyPassed。这些谓词虽然不属于EARL核心结果集,但在实际数据中可能出现。对于这种情况,可以采用两种策略:一种是严格模式,只接受EARL定义的五个核心结果谓词,遇到扩展谓词就跳过或报错;另一种是宽松模式,允许扩展谓词作为附加字符串类型存在,但核心逻辑仍然使用标准谓词类型。
宽松模式可以通过在基础类型之外增加一个可选的扩展类型来实现。例如,定义一个 ExtendedResultPredicate 类型,包含核心谓词以及自定义字符串的后缀。但这样会削弱类型安全,因为几乎任何字符串都可以被接受。更好的做法是在数据入口处进行区分,把无法识别为标准谓词的值单独存储,而不是混入核心统计逻辑。示例代码如下:
type KnownResultPredicate = ResultPredicate;
type UnknownResultPredicate = string;
interface AssertionWithFlexiblePredicate {
subject: string;
test: string;
predicate: KnownResultPredicate | UnknownResultPredicate;
isStandardPredicate: boolean;
}
function classifyPredicate(value: string): AssertionWithFlexiblePredicate['predicate'] {
const normalized = normalizePredicate(value);
return normalized ?? value;
}
这种方式的优点是保留了标准谓词的类型安全,同时不会因为未知谓词而丢失数据。业务代码可以根据 isStandardPredicate 标志来决定是否将该断言纳入合规统计。如果需要对扩展谓词进行特殊处理,可以在单独的映射表中添加配置,而不会影响核心类型定义。
此外,当EARL数据以JSON-LD格式存储时,结果谓词可能出现在JSON-LD的 @context 中,通过上下文将 earl 前缀映射到完整的命名空间IRI。在解析JSON-LD数据时,最好使用专业的JSON-LD解析库来展开IRI,然后将展开后的完整IRI传递给 normalizePredicate 函数。这样可以确保前缀缩写和完整IRI都能被正确识别,从而避免因为上下文缺失而导致的解析失败。
六、总结与最佳实践
使用TypeScript为EARL结果谓词封装类型定义,本质上是在编译期和运行时之间建立一道安全屏障。字符串字面量联合类型让非法值无法通过类型检查,常量对象和类型守卫提供了运行时校验能力,映射函数则解决了IRI全称与前缀缩写之间的兼容问题。这些技术手段组合在一起,能够显著提升无障碍评估工具的数据处理可靠性。
在实际项目中,建议将结果谓词相关的类型和函数集中在一个模块中,避免在多个文件之间复制粘贴字符串字面量。同时,对于外部输入的EARL数据,始终先进行运行时校验和规范化,再进入业务逻辑。如果项目使用JSON-LD,尽量在数据入口处完成IRI展开,保证后续处理只面对统一的紧凑形式。最后,当需要支持扩展谓词时,保持核心类型不变,将扩展值作为补充数据单独管理,以避免破坏类型安全。
通过这套类型封装方案,开发团队可以在无障碍评估报告的生成、解析、统计和展示全流程中享受到TypeScript带来的好处:更少的运行时错误、更清晰的代码意图以及更易于维护的评估逻辑。这也是TypeScript在数据密集型应用中的一个典型落地场景。
TypeScriptEARL无障碍评估修改时间:2026-08-25 09:45:46