Skip to content

packages/spec 的「编译期 pin」是失效的:tsconfig 排除了 *.test.ts,vitest 也不做类型检查 #4642

Description

@os-zhuang

#4641(双源 C4)里落 regression pin 时发现的范围外问题,不在该 PR 修

现象

双源清账手册推荐的回归 pin 是「编译期条件类型」(C1 #4581 首创,C3 #4610/#4638 沿用):

type UiNotificationModule = typeof import('./notification.zod');
const hasNotificationSchema: 'NotificationSchema' extends keyof UiNotificationModule
  ? true
  : false = false;

思路是:名字被重新加回来 ⇒ 条件类型翻成 truefalse 赋值报错 ⇒ tsc --noEmit 失败。

但这个 pin 永远不会失败,因为没有任何门禁会编译它:

  1. packages/spec/tsconfig.json"exclude": ["node_modules", "dist", "**/*.test.ts"] —— pnpm --filter @objectstack/spec typecheck(就是 tsc --noEmit)根本不编译 *.test.ts
  2. packages/spec/vitest.config.ts 没有开 typecheck: { enabled: true },vitest 走 esbuild 直接剥类型,不做类型检查。

实测:把 export const SessionSchema = ... 重新加回 src/identity/identity.zod.ts,再跑 npx tsc --noEmit,identity.test.ts 里的 pin 零报错。换成运行时断言(Object.keys(mod) )后,同样的 sabotage 立刻红。

附带的第二个坑

即使这些文件被编译,keyof typeof import(...) 只枚举 value 导出,type-only 导出对它不可见。实测:

// mod.ts
export const Foo = 1;
export type Bar = { a: string };

// probe.ts
type M = typeof import('./mod');
const hasBar: 'Bar' extends keyof M ? true : false = false;  // 不报错 —— Bar 看不见
const hasFoo: 'Foo' extends keyof M ? true : false = false;  // 报错 —— 符合预期

所以对裸类型名(Session / Notification 这种 z.infer< typeof XSchema > 派生的 type)写这类断言,即便文件被编译也是空转。

影响面

现有 pin 只是文档,不是门禁 —— 而它们读起来像门禁,后来人会信:

真正兜住这些名字的其实是 check:dual-source-exports(读构建出来的 entry .d.ts,type 和 const 都枚举)。所以这不是「没人管」,而是「pin 本身没在管」。

可选处置(择一,需 maintainer 定)

  • A. 让 *.test.ts 进类型检查 —— 加一个 tsconfig.test.json(或去掉 exclude)并接进 typecheck 脚本。最贴合原意,但会一次性暴露测试代码里既有的类型问题,体量未知。
  • B. vitest 开 typecheck.enabled —— 用 expectTypeOf / assertType 重写这些 pin,vitest 原生支持且只覆盖测试文件。代价是测试变慢。
  • C. 统一改成运行时断言 —— 只能覆盖 value 导出,裸类型交给 check:dual-source-exports。最省,但语义比 A/B 弱。

倾向 B:expectTypeOf 是为这件事造的,失败信息比条件类型的 Type 'false' is not assignable to type 'true' 可读得多;而且它把「类型断言」显式标成测试,不会再被误当成免费的编译期检查。B 落地前,新 pin 建议按 C 写(#4641 已这么做,并在注释里写明了原因)。

关联:#4535(双源主账本)、#4581#4603#4638#4641

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions