在 #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;
思路是:名字被重新加回来 ⇒ 条件类型翻成 true ⇒ false 赋值报错 ⇒ tsc --noEmit 失败。
但这个 pin 永远不会失败,因为没有任何门禁会编译它:
packages/spec/tsconfig.json 里 "exclude": ["node_modules", "dist", "**/*.test.ts"] —— pnpm --filter @objectstack/spec typecheck(就是 tsc --noEmit)根本不编译 *.test.ts。
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
在 #4641(双源 C4)里落 regression pin 时发现的范围外问题,不在该 PR 修。
现象
双源清账手册推荐的回归 pin 是「编译期条件类型」(C1 #4581 首创,C3 #4610/#4638 沿用):
思路是:名字被重新加回来 ⇒ 条件类型翻成
true⇒false赋值报错 ⇒tsc --noEmit失败。但这个 pin 永远不会失败,因为没有任何门禁会编译它:
packages/spec/tsconfig.json里"exclude": ["node_modules", "dist", "**/*.test.ts"]——pnpm --filter @objectstack/spec typecheck(就是tsc --noEmit)根本不编译*.test.ts。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 导出对它不可见。实测:所以对裸类型名(
Session/Notification这种z.infer< typeof XSchema >派生的 type)写这类断言,即便文件被编译也是空转。影响面
现有 pin 只是文档,不是门禁 —— 而它们读起来像门禁,后来人会信:
OpenApiWebhookEvent相关 pinpackages/spec/src/ui/notification.test.ts、packages/spec/src/system/notification.test.tsrg -l "typeof import" packages/spec/src --glob '*.test.ts'命中的:kernel/metadata-plugin.test.ts、api/rest-server.test.ts真正兜住这些名字的其实是
check:dual-source-exports(读构建出来的 entry.d.ts,type 和 const 都枚举)。所以这不是「没人管」,而是「pin 本身没在管」。可选处置(择一,需 maintainer 定)
*.test.ts进类型检查 —— 加一个tsconfig.test.json(或去掉 exclude)并接进typecheck脚本。最贴合原意,但会一次性暴露测试代码里既有的类型问题,体量未知。typecheck.enabled—— 用expectTypeOf/assertType重写这些 pin,vitest 原生支持且只覆盖测试文件。代价是测试变慢。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