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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions