
Vitest 类型测试实战指南用 expectTypeOf 与 assertType 做静态类型断言并理解底层 tsc 集成机制【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitestVitest 除了运行时测试还支持对 TypeScript 类型系统本身写测试通过expectTypeOf或assertType断言函数签名、属性类型再借助tsc/vue-tsc静态分析来验证类型是否符合预期。本文基于官方文档docs/guide/testing-types.md完整展开并结合packages/vitest/src/typecheck/中的源码与examples/typecheck示例工程讲清类型测试的文件约定、两套断言 API 的用法与报错机制、typecheck配置项以及--typecheck命令的底层执行流程帮助你把“类型正确性”纳入 CI 流水线。类型测试的基本约定哪些文件被视为类型测试Vitest 允许用expectTypeOf或assertType语法编写类型测试。默认情况下所有位于*.test-d.ts文件中的测试都被视为类型测试该模式由typecheck.include配置项控制。从源码默认值可以看到这一约定定义在 默认配置 中typecheck: { checker: tsc as const, include: [**/*.{test,spec}-d.?(c|m)[jt]s?(x)], exclude: defaultExclude, },也就是说默认会匹配*.test-d.ts、*.test-d.mts、*.spec-d.tsx等所有-d后缀的测试文件覆盖 TS/JS、CJS/ESM 与 React 风格文件。你可以通过typecheck.include改成任意 glob 模式例如把*.types.ts也纳入类型测试。一个最小示例官方示例工程 examples/typecheck 展示了最简配置。vite.config.ts 中只需开启 typecheck/// reference typesvitest/config / import { defineConfig } from vite export default defineConfig({ test: { typecheck: { enabled: true, }, }, })对应的类型测试文件 test/type.test-d.tsimport { expect, expectTypeOf, test } from vitest test(type, () { expectTypeOf(1).toEqualTypeOf(2) expect(1).toBe(2) // not executed })注意最后一行expect(1).toBe(2)虽然“写错”了但注释标明not executed—— 这正是类型测试的关键特性见下文“只做静态分析”一节。两套断言 APIexpectTypeOf 与 assertType完整示例函数签名与 ts-expect-error 技巧文档给出的标准写法如下测试文件为mount.test-d.tsimport { assertType, expectTypeOf } from vitest import { mount } from ./mount.js test(my types work properly, () { expectTypeOf(mount).toBeFunction() expectTypeOf(mount).parameter(0).toExtend{ name: string }() // ts-expect-error name is a string assertType(mount({ name: 42 })) })要点expectTypeOf(mount).toBeFunction()断言mount是一个函数.parameter(0).toExtend{ name: string }()断言第一个参数的类型至少包含name: string属性扩展关系assertType(mount({ name: 42 }))前标注ts-expect-errorname要求是string而传入了42编译器应当在此处报错ts-expect-error则把“此处必须有类型错误”变成断言本身——如果哪天类型被改对了这行反而会因“错误消失”而失败。可用的 matcher 完整列表见 expectTypeOf API 文档。核心原则类型错误即测试错误任何在类型测试文件内触发的类型错误都会被当作测试失败因此你可以使用任意“类型技巧”来检验项目中的类型。例如上面的ts-expect-error模式测试“通过”的前提是编译器恰好在那一行报错这使负向断言“这里必须不能赋值”也成为可能。关键特性只做静态分析不真正执行必须牢记Vitest不会运行*.test-d.ts文件它们只被编译器静态分析。这意味着测试体内不会有任何运行时执行如果使用动态测试名、test.each或test.for测试名不会被求值会按字面原样显示。另外文档给出了一个重要的版本兼容性警示Vitest 2.1 之前typecheck.include会覆盖include模式导致你的运行时测试实际上不会执行、只会被类型检查。Vitest 2.1 起当include与typecheck.include存在重叠时Vitest 会把类型测试和运行时测试报告为两个独立条目同一文件既运行又类型检查。因此如果你升级自 2.1 之前的版本需要检查 CI 中测试条目数量是否变化避免误以为“运行时测试没跑”。类型检查同样支持 CLI 标志--allowOnly与-t按名称过滤都可以用于类型检查。读懂类型断言的错误信息当断言失败时编译器报出的错误往往需要一点“解读”。这一部分决定你是否能高效调试类型测试。toEqualTypeOf / toExtendMismatchInfo 错误格式这类断言采用“流畅式”写法expectActual().toEqualTypeOfExpected()失败信息应指向Expected期望类型而非 Actual。为了让报错更可执行expectTypeOf使用了一个特殊的MismatchInfo辅助类型在报错中显式标注期望与实际expectTypeOf({ a: 1 }).toEqualTypeOf{ a: string }()该断言会失败{a: 1}的类型是{a: number}而非{a: string}错误信息类似test/test.ts:999:999 - error TS2344: Type { a: string; } does not satisfy the constraint { a: \\Expected: string, Actual: number\\; }. Types of property a are incompatible. Type string is not assignable to type \\Expected: string, Actual: number\\. 999 expectTypeOf({a: 1}).toEqualTypeOf{a: string}()解读方法不要逐字理解Types of property a are incompatible // Type string is not assignable to type Expected: string, Actual: number这句模板化文字只需看**属性名a**与消息Expected: string, Actual: number——这通常就能告诉你问题所在。极其复杂的类型当然需要更多实验。若错误信息确实具有误导性可向 expect-type 上游仓库反馈 issue。toBe 系列方法不可调用not callable错误toBeString、toBeNumber、toBeVoid等方法在 Actual 类型不匹配时会解析为一个不可调用的类型来触发错误。例如expectTypeOf(1).toBeString()失败时报错形如test/test.ts:999:999 - error TS2349: This expression is not callable. Type ExpectStringnumber has no call signatures. 999 expectTypeOf(1).toBeString() ~~~~~~~~~~This expression is not callable本身没什么信息量真正有意义的是下一行Type ExpectStringnumber has no call signatures——本质含义是“你传了 number 却断言它是 string”。文档也提到如果 TypeScript 未来支持 throw types这类报错可以显著改善在那之前需要读者稍加辨认。具体对象字面量 vs 类型参数typearg两种写法的报错友好度不同// 报错信息帮助较小需要编译器推断 typearg只能与泛型 Mismatch 类型比较 expectTypeOf({ a: 1 }).toEqualTypeOf({ a: }) // 报错信息更清晰直接给出期望类型 expectTypeOf({ a: 1 }).toEqualTypeOf{ a: string }()原因是 TypeScript 编译器需要为.toEqualTypeOf({a: })这种具体对象写法推断类型参数断言库只能将其与泛型Mismatch类型对比导致错误信息退化。因此建议尽量使用typearg 而非具体值进行.toEqualTypeOf与.toExtend断言。如果确实需要比较两个具体类型可以用typeofconst one valueFromFunctionOne({ some: { complex: inputs } }) const two valueFromFunctionTwo({ some: { other: inputs } }) expectTypeOf(one).toEqualTypeOftypeof two()备选方案更简单的 assertType如果你觉得expectTypeOf的报错难以理解可以退回到更简单的assertTypeconst answer 42 assertTypenumber(answer) // ts-expect-error answer is not a string assertTypestring(answer)警惕 ts-expect-error 的“假阳性”使用ts-expect-error时要确保没有拼写错误。一个实用技巧把类型测试文件也纳入test.include配置项让 Vitest真正运行这些文件——拼写错误会在运行时以ReferenceError暴露出来。// ts-expect-error answer is not a string assertTypestring(answr) // answr 拼错了但 ts-expect-error 仍会“通过”属于假阳性typecheck 配置项详解类型检查环境的完整配置见 typecheck 配置文档核心参数如下配置项类型默认值CLI说明typecheck.enabledbooleanfalse--typecheck、--typecheck.enabled在常规测试之外启用类型检查typecheck.onlybooleanfalse--typecheck.only只运行类型测试CLI 下会自动启用类型检查typecheck.checkertsc \| vue-tsc \| stringtsc—类型检查工具。Vitest 会以特定参数 spawn 进程以便解析输出自定义二进制需输出与tsc --noEmit --pretty false相同的格式。tsc需要安装typescript包vue-tsc需要vue-tsc包typecheck.includestring[][**/*.{test,spec}-d.?(c|m)[jt]s?(x)]—被视为类型测试文件的 glob 模式typecheck.excludestring[][**/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**]—不作为类型测试文件的 glob 模式typecheck.allowJsbooleanfalse—检查带ts-check注释的 JS 文件若 tsconfig 已启用则不会覆盖typecheck.ignoreSourceErrorsbooleanfalse—在测试文件之外发现错误时不失败为true时完全不显示非测试文件的错误。默认情况下发现源码错误会导致测试套件失败typecheck.tsconfigstring自动查找最近的tsconfig.json—自定义 tsconfig 路径相对项目根目录typecheck.spawnTimeoutnumber10000—启动类型检查进程的最小等待时间毫秒底层机制Vitest 根据配置调用tsc或vue-tsc并解析其输出若在测试文件之外的源码中发现类型错误默认会让测试套件失败可用typecheck.ignoreSourceErrors关闭该行为。运行类型检查--typecheck 与源码级执行流程启用方式在package.json中给 vitest 命令加上--typecheck标志{ scripts: { test: vitest --typecheck } }然后执行npm run test/yarn test/pnpm run test/bun test即可同时跑运行时测试与类型测试。也可以放在 Vite/Vitest 配置中见 examples/typecheck/vite.config.ts 的typecheck.enabled: true。只跑类型测试而不跑运行时测试则使用--typecheck.only——CLI 参数解析代码 cac.ts 中可以看到它会自动补上enabled: trueif (typeof argv.typecheck?.only boolean) { argv.typecheck.enabled ?? true }底层执行流程源码视角类型检查的核心实现在 Typechecker 类。从源码结构看其执行链条为收集测试collectTests()对每个类型测试文件做 AST 收集astCollectFileInformationpool 为typescript提取test/describe定义作为任务树spawn 编译器进程spawn()方法构造参数并启动 checker 进程typechecker.tsconst args [--pretty, false] if (typecheck.build) { args.unshift(--build) } else { args.push( --noEmit, --incremental, --tsBuildInfoFile, join( process.versions.pnp ? join(os.tmpdir(), this.project.hash) : distDir, tsconfig.tmp.tsbuildinfo, ), ) } // use builtin watcher because its faster if (watch) { args.push(--watch) } if (typecheck.allowJs) { args.push(--allowJs, --checkJs) } if (typecheck.tsconfig) { if (!typecheck.build) { args.push(-p) } args.push(resolve(root, typecheck.tsconfig)) }由此可以确认几个事实Vitest 实际执行的是tsc --noEmit --pretty false增量模式下追加--incremental与临时 buildinfo 文件watch 模式下追加--watchallowJs时追加--allowJs --checkJs因此你在流水线中已单独存在的tsc检查脚本可以移除避免重复工作找不到 tsconfig 时源码中内置了友好的诊断提示会检查typecheck.tsconfig配置与 tsconfig 合法性checker 为tsc/vue-tsc时ensurePackageInstalled()会确保typescript或vue-tsc包已安装对应配置文档中“需要安装对应包”的要求。解析与归因parse.ts中getRawErrsMapFromTsCompile()解析编译器输出把报错按文件归入对应测试任务测试文件内的类型错误即成为测试失败结果通过TypecheckResultsfiles、sourceErrors、time汇总source errors 即测试文件之外的错误是否失败由typecheck.ignoreSourceErrors决定。小结在*.test-d.ts可由typecheck.include调整中用expectTypeOf或assertType编写类型断言文件内的类型错误即测试错误类型测试只做静态分析、不执行代码动态测试名不会被求值注意 Vitest 2.1 前后include与typecheck.include重叠时的报告行为变化调试失败时重点阅读报错中的Expected: ..., Actual: ...信息或ExpectXxxActual has no call signatures一行优先用 typearg 而非具体对象字面量做.toEqualTypeOf/.toExtend用--typecheck或配置typecheck.enabled: true开启Vitest 底层以tsc --noEmit或vue-tsc --noEmit完成检查可据此精简流水线中重复的 tsc 脚本。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考