ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入解析 @ark/attest 演进史:ArkType 的类型级断言、快照与性能基准测试利器

深入解析 @ark/attest 演进史:ArkType 的类型级断言、快照与性能基准测试利器 后端【免费下载链接】arktypeTypeScripts 1:1 validator, optimized from editor to runtime项目地址https://gitcode.com/gh_mirrors/ar/arktype点击查看免费下载导读ark/attest是 ArkType 生态中的测试基础设施它让 TypeScript 类型在运行时可见提供类型级断言attestexpected(value)、类型字符串快照type.toString.snap()、自动补全快照、JSDoc 断言、错误断言以及确定性的类型实例化数instantiations基准测试。本篇文章以 ark/attest/CHANGELOG.md 为脉络结合 ark/attest/README.md 与仓库源码梳理 attest 从早期版本到 0.51.0 的关键能力演进——读完你将掌握它的配置体系CLI 参数 / 环境变量 / setup 函数三通道、断言与快照的底层实现以及如何在自己的项目或库中集成这套类型测试能力。说明该 CHANGELOG 自身注明并不完整很多更新只是随arktype版本一起 bump 的断言调整但它完整记录了 attest 独有的重要变更配合源码可以还原出相当完整的图谱。核心断言能力从equals到satisfies、toString与正则匹配equals 的健壮性改造0.41.0避免深比较导致的 OOM在 0.41.0 之前attest(...).equals(...)会直接依赖 Node 的deepStrictEqual做深比较。对带有递归引用例如对象属性引用了Type实例的值做深比较时会直接导致OOM内存耗尽崩溃这正是 issue #1287 描述的问题。0.41.0 的短期方案是先做构造函数constructor级别的浅比较避免常见的病态深比较。这在 assertions.ts 的unversionedAssertEquals中可以看到具体逻辑当expected与actual都是对象且构造函数相同时才走deepStrictEqual当构造函数不同时直接抛出AssertionError并提示Objects did not have the same constructor同时用printable序列化两侧内容辅助排错当两侧至少有一方是函数或对象非引用相等时也直接报错而不是递归比较。于是此前会 OOM 的用例现在会得到浅显的失败结果// 之前可能触发 OOM 异常现在会浅显失败并给出简单错误 attest(type.string).equals(type.boolean)这一改造在测试 assertions.test.ts 中有对应验证attest(() attest(type.string).equals(type.number)).throws.equals(...)断言其抛出的是not between reference equal items风格的错误而ArkError与普通对象比较时则会给出Objects did not have the same constructor的信息。CHANGELOG 同时注明未来会引入真正的字符串 diff 逻辑来给出更友好的差异展示。satisfies用 ArkType 定义直接断言任意值0.8.1 / 0.11.00.8.1 引入了.satisfies断言它把一个 ArkType 定义当作约束来校验当前断言的值attest({ foo: bar }).satisfies({ foo: string }) // Error: foo must be a number (was string) attest({ foo: bar }).satisfies({ foo: number })在源码中satisfies通过type.raw(def)将传入的定义编译为 ArkType 类型再调用其assert方法见 chainableAssertions.ts。0.11.0 又允许对type.toString这类类型断言结果使用satisfies进行正则/局部匹配// ok attest({ ark: type }).type.toString.satisfies(/^{.*}$/) // AssertionError: ark must be a number (was string) attest({ ark: type }).satisfies({ ark: number })从类型层看satisfies还做了约束重叠校验如果期望类型与约束类型无交集会直接触发This type has no overlap with your satisfies constraint的类型错误nonOverlappingSatisfiesMessage见 chainableAssertions.ts把错误前置到编译期。toString断言支持正则/局部匹配0.11.00.11.0 起type.toString不再只能精确等于一个字符串还支持传入正则进行匹配实现函数为assertEqualOrMatching见 assertions.ts期望值是字符串时检查实际字符串是否包含该子串actual.includes(expected)期望值是RegExp时检查是否匹配失败时报Actual string ... did not match regex ...实际值不是字符串时直接报错。// ok attest({ ark: type }).type.toString(/^{.*}$/) // AssertionError: Actual string string[] did not match regex ^{.*}$ attest([ark, type]).type.toString(/^{.*}$/)这也解释了为什么ChainableAssertions中大量断言携带allowRegex标志type.toString、type.errors、jsdoc、throws都允许用字符串子串或正则做宽松匹配。用 prettier 格式化类型序列化0.10.0 与 0.11.0 的typeToStringFormat0.10.0 是一个重要的可读性里程碑序列化类型字符串改用 prettier 格式化。在引入格式化之前长的对象类型被序列化成一行// old单行难以阅读 attest({ ark: type, type: script, vali: dator, opti: mized, from: editor, to: runtime }).type.toString.snap( { ark: string; type: string; vali: string; opti: string; from: string; to: string; } ) // new多行、缩进可读性大幅提升 attest({ ark: type, type: script, vali: dator, opti: mized, from: editor, to: runtime }).type.toString.snap({ ark: string type: string vali: string opti: string from: string to: string })该格式化逻辑在 chainableAssertions.ts 的formatTypeString中实现它把类型字符串包成type T typeString交给prettier.format再截掉声明前缀默认配置为{ semi: false, printWidth: 60, trailingComma: none, parser: typescript }其中printWidth: 60是专门针对类型序列化优化过的宽度。破坏性影响与迁移路径由于格式化规则变化0.10.0 之后已有的类型快照大概率会因格式不一致而失败。CHANGELOG 给出了两种重建快照的方式# 方式一测试时带 --updateSnapshots 标志 # 方式二设置环境变量 ATTEST_updateSnapshots1 vitest run对于非快照的type.toString断言例如硬编码的字符串期望则需要手动更新——CHANGELOG 建议临时把它们改成快照方便直接看到正确值再固化。0.11.0 进一步提供了typeToStringFormat配置用于覆盖 prettier 的序列化选项。除默认值外任何你提供的选项都会覆盖对应默认项最便捷的提供方式是传入setupimport * as attest from ark/attest export const setup () attest.setup({ // 例如收紧类型序列化宽度 typeToStringFormat: { printWidth: 40 } })它也可以作为 JSON 序列化字符串通过--typeToStringFormatCLI 参数或ATTEST_typeToStringFormat环境变量传入。底层支持在 config.ts 的getParamValue中可以看到typeToStringFormat与compilerOptions一样会被JSON.parse解析后并入配置。JSDoc 断言0.44.00.44.0 为被断言的值增加了 JSDoc 关联内容断言能力——你可以匹配或快照与某个值关联的 JSDoc 注释const T type({ /** FOO */ foo: string }) const out T.assert({ foo: foo }) // match or snapshot expected jsdoc associated with the value passed to attest attest(out.foo).jsdoc.snap(FOO)在源码 chainableAssertions.ts 中jsdoc把实际值替换为一个TypeAssertionMapping其actual取缓存数据中的data.jsdoc ?? 并经过formatTypeString格式化同时开启allowRegex因此你既可以.snap(FOO)精确快照也可以.jsdoc.equals(FOO)或传入正则做宽松匹配。测试 assertions.test.ts 验证了attest(o.foo).jsdoc.equals(FOO)通过、而attest(o.bar).jsdoc.equals(BAR)抛出AssertionError。实例化数基准attest.instantiations与默认抛错0.8.0阈值超限默认抛错0.8.0 之前当attest.instantiations()超过benchPercentThreshold指定的百分比时只是返回非零退出码。0.8.0 起改为默认直接在测试内抛出异常it(can snap instantiations, () { type Z makeComplexTypeasbsdfsaodisfhsda // 实际实例化数比快照值高出 20% 以上时会在此处直接抛错 attest.instantiations([1, instantiations]) })实现层面attest.instantiations通过instantiationDataHandler完成当从 bench 上下文调用时它用 TSServer 解析调用位置并计算该bench调用贡献的实例化数getContributedInstantiations然后与期望值比较并和基线对照见 type.ts。默认阈值为 20%且benchErrorOnThresholdExceeded默认为true见 config.ts。快照自动补全按字母序稳定化0.8.00.8.0 同时规定快照的自动补全列表将按字母序排列。这对像 ArkType 关键字补全这样的大列表尤其重要CHANGELOG 自嘲更新到不想再更新。例如对type([])的补全快照形如attest(() type([])).completions({ : [ ..., , Array, Date, Error, Function, Map, Promise, Record, RegExp, Set, WeakMap, WeakSet, alpha, alphanumeric, any, bigint, boolean, creditCard, digits, email, false, format, instanceof, integer, ip, keyof, lowercase, never, null, number, object, parse, semver, string, symbol, this, true, undefined, unknown, uppercase, url, uuid, void ] })completions的底层实现把实际值替换为TypeAssertionMappingactual取data.completions如果补全因歧义例如两个相同的字符串字面量无法确定缓存会写入一条错误消息并直接抛出见 chainableAssertions.ts。在skipTypes模式下它退化为chainableNoOpProxy不执行任何断言。快照体系与failOnMissingSnapshots0.47.00.47.0 新增failOnMissingSnapshots配置项默认值取决于环境变量CI设置了CI时为true否则为false。这一逻辑在 config.ts 中直接可见failOnMissingSnapshots: CI in process.env。它的作用体现在snap快照流程见 chainableAssertions.ts当快照尚未填充无参数调用、或updateSnapshots开启时如果failOnMissingSnapshots为true会抛出MissingSnapshotError.snap() at 位置 must be populated.避免 CI 上出现悄悄生成快照导致的假绿。对应测试在 assertions.test.tsassert.throws( () attestInternal(, { cfg: { failOnMissingSnapshots: true } }).snap(), MissingSnapshotError )快照更新在进程退出时统一落盘cleanup/teardown→writeSnapshotUpdatesOnExit见 fixtures.ts这也是 attest 需要 globalSetup/globalTeardown 的原因之一。配置三通道与完整默认值Attest 配置可通过三种方式指定参见 README.md进程参数例如--skipTypes、--benchPercentThreshold 10布尔型直接以--flag形式出现--updateSnapshots还有-u/--update别名环境变量带ATTEST_前缀例如ATTEST_skipTypes1、ATTEST_benchPercentThreshold10值会被JSON.parse解析setup 函数参数例如attest.setup({ skipTypes: true, benchPercentThreshold: 10 })。三条通道的合并逻辑在 config.ts先读环境变量再用 CLI 参数覆盖最后setup通过ATTEST_CONFIG环境变量把传入的选项并入见 fixtures.ts。当前默认配置config.tsexport const getDefaultAttestConfig (): BaseAttestConfig ({ tsconfig: existsSync(fromCwd(tsconfig.json)) ? fromCwd(tsconfig.json) : undefined, compilerOptions: {}, attestAliases: [attest, attestInternal], failOnMissingSnapshots: CI in process.env, updateSnapshots: false, skipTypes: false, skipInlineInstantiations: false, tsVersions: default, benchPercentThreshold: 20, benchErrorOnThresholdExceeded: true, filter: undefined, testDeclarationAliases: [bench, it, test], formatCmd: npm exec --no -- prettier --write, shouldFormat: true, typeToStringFormat: {} })其中几个关键项值得展开skipTypes跳过类型检查与所有依赖类型信息的断言适合 watch 模式或 VSCode Test Explorer 下的快速迭代。官方推荐的脚本组合是{ test: ATTEST_skipTypes1 vitest run, testWithTypes: vitest run }类型改动需要复验或跑 CI 时用testWithTypes日常开发与 watch 用test。tsconfig/compilerOptions默认探测仓库根目录的tsconfig.json也可通过 JSON 参数覆盖编译器选项。attestAliasesattest 只从这些名字的函数调用中收集类型数据是集成自定义断言库的关键。testDeclarationAliases[bench, it, test]用于定位包含基准调用的外层测试/bench 声明计算其贡献的实例化数见 type.ts。filter可按名称过滤要执行的 bench支持字符串或分段数组。Benches类型与运行时混合基准0.9.x 修复与 baseline 机制Attest 的bench与测试分离运行不需要特殊 setup直接用tsx benches.ts或ts-node benches.ts即可。其.types([n, instantiations])会确定性地报告 bench 内容贡献的 TypeScript 类型实例化数// 组合模板字面量常产生昂贵类型——来给这个做个基准 type makeComplexTypes extends string s extends ${infer head}${infer tail} ? head | tail | makeComplexTypetail : s bench(bench type, () { return {} as makeComplexTypedefenestration // 这是内联快照运行文件时会被填充或比对 }).types([169, instantiations]) bench( bench runtime and type, () { return {} as makeComplexTypeantidisestablishmentarianism }, fakeCallOptions ) // 函数执行平均耗时 .mean([2, ms]) // 类型看起来对输入长度呈 O(n)——还不错 .types([337, instantiations])运行时基准在 bench.ts 的ResultCollector中实现默认以5 秒或 100_000 组调用每组 1000 次显式调用先到者为准通过call1K显式循环 1000 次以规避 V8 对循环的优化mean/median统计分别见stats对象。Baseline基线表达式机制如果基准对象是 API 的首次调用其初始实例化会造成噪音需要先放一个基线表达式import { bench } from ark/attest import { type } from arktype // baseline expression type(boolean) bench(single-quoted, () { const _ type(nineteen characters) // 没有基线时会是 2697 }).types([610, instantiations]) bench(keyword, () { const _ type(string) // 没有基线时会是 2507 }).types([356, instantiations])[!WARNING] 基线表达式绝不能与某个 bench 中的表达式相同否则 bench 会复用其缓存类型导致实例化数被低估甚至为 0。CHANGELOG 中 0.9.2 修复了连续多次运行 bench 无法内联填充快照的 bug0.9.4 则改进了基准源码提取并补充了基线表达式说明——可见这套机制是逐步打磨出来的。若要在 CI 上对超出阈值失败可以这样跑tsx ./p99/within-limit/p99-tall-simple.bench.ts --benchErrorOnThresholdExceeded --benchPercentThreshold 10CLIstats与trace0.44.x 解耦 pnpmAttest 内置attestCLI子命令包括precache、trace、stats分发逻辑见 cli.ts# 汇总各包的关键类型性能指标check 时间、实例化数、类型数量 npm run attest stats packages/* # 生成可被 perfetto 等工具查看的类型性能热力图 trace.json npm run attest trace .stats接受任意数量的包目录参数支持 glob如packages/*不给参数时默认检查 CWD见 cli/stats.ts。trace接受单个根目录参数在.attest/trace下生成trace.json可导入 ui.perfetto.dev 查看同时用typescript/analyze-trace汇总热点。0.44.3 的Decouple attest trace/stats from pnpm意味着这些命令不再强依赖 pnpm——从实现看只有当 attest 源码以.ts形式运行时才调用pnpm attest precache否则走npm exec -c attest precache ...见 fixtures.ts。多 TypeScript 版本测试与自定义断言集成tsVersions一次测试多个 TS 版本tsVersions允许同时测试多个 TypeScript 版本别名别名必须以typescript开头的 package.jsondevdependency 形式声明{ typescript: latest, typescript-next: npm:typescriptnext, typescript-1: npm:typescript5.2, typescript-2: npm:typescript5.1 }传入*会运行所有发现的typescript*版本setup({ tsVersions: * })底层 tsVersioning.ts 会把当前node_modules/typescript临时重命名为typescript-temp再依次软链目标版本并运行最后无论成败都会恢复原版本版本发现逻辑findAttestTypeScriptVersions会扫描当前包及所有父包的node_modules同时识别typescript-*目录与ark/attest-ts-*目录。多版本断言通过versionableAssertion逐版本执行任一版本失败都会累积错误并抛出见 assertions.ts。库作者集成attestAliases与底层 API如果你在写库并想把自己的断言函数接入 attest 的类型数据收集需要在第一个测试运行前调用setup并列出自定义断言名// attest 只会从 attestAliases 中列出的函数调用收集类型数据 setup({ attestAliases: [yourCustomAssert] })setup 过程中 attest 会搜索这些断言调用并把类型缓存到临时文件writeAssertionData→analyzeProjectAssertions见 fixtures.ts从而避免为每个测试进程都启动新的 TSServer。最灵活的底层 API 是getTypeAssertionsAtPosition与caller均从 index.ts 导出import { getTypeAssertionsAtPosition, caller } from ark/attest const yourCustomAssert expectedType(actualValue: expectedType) { const position caller() const types getTypeAssertionsAtPosition(position) // 断言 actualValue 的类型与 expectedType 的类型一致 const relationship types[0].args[0].relationships.typeArgs[0] if (relationship undefined) { throw new Error( yourCustomAssert requires a type arg representing the expected type, e.g. yourCustomAssertfoo(foo) ) } if (relationship ! equality) { throw new Error( Expected ${types.typeArgs[0].type}, got ${types.args[0].type} with relationship ${relationship} ) } }用户即可获得友好的类型关系诊断test(my code, () { // Ok yourCustomAssertfoo(${f}oo as const) // Error: Expected boolean, got true with relationship subtype yourCustomAssertboolean(true) // Error: Expected 5, got number with relationship supertype yourCustomAssert5(2 3) })从 chainableAssertions.ts 可以看到attest内部正是把TypeAssertionMapping如typeEqualityMapping作为可版本化实际值交给assertEquals从而做到跨 TypeScript 版本的一致断言。其余值得注意的修复0.46.0 与 0.51.00.51.0修复部分 tsconfig 路径解析问题感谢 LukeAbby 的贡献。0.46.0修复某些 bench 文件解析不正确、导致报错或实例化计数为 0 的问题——这正是基准测试确定性目标的延续。总结从 0.7.x 到 0.51.0ark/attest的核心演进可以归纳为三条主线断言表达力的扩展equals→satisfies、type.toString正则匹配、type.errors、JSDoc 断言、补全快照以及构造器级浅比较对 OOM 的防御快照体验的工程化prettier 多行格式化 typeToStringFormat、字母序补全快照、failOnMissingSnapshotsCI 感知默认值、--updateSnapshots/ATTEST_updateSnapshots一键重建类型性能基准的确定性实例化数基准、基线表达式、阈值默认抛错、stats/traceCLI 与 pnpm 解耦。如果你想在现有 Vitest/Jest/Mocha 测试中引入类型级断言或为自己的类型库建立跨版本、可量化的类型性能回归护栏这套配置与源码路径config.ts、assert/chainableAssertions.ts、bench、tests就是最好的起点。赞分享后端【免费下载链接】arktypeTypeScripts 1:1 validator, optimized from editor to runtime项目地址https://gitcode.com/gh_mirrors/ar/arktype点击查看免费下载相关推荐ArkType Attest 完全指南在运行时断言 TypeScript 类型与性能基准ArkType Attest 完全指南在运行时断言 TypeScript 类型与性能基准 导读 Attest 是 ArkType 仓库中随 ark/atte后端Roc 语言 import exposing 类型导入语法深度解析基于编译器快照测试的作用域与类型诊断实战Roc 语言 import exposing 类型导入语法深度解析基于编译器快照测试的作用域与类型诊断实战 本篇技术指南以 Roc 编译器仓库中的快照测试 t深入 Roc 类型系统用快照测试剖析嵌套类型变量的解析、规范化与类型推断深入 Roc 类型系统用快照测试剖析嵌套类型变量的解析、规范化与类型推断 导读 本文以 Roc 编译器仓库中的快照测试 test/snapshots/type创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进