Uh oh!
There was an error while loading. Please reload this page.
fix(fields)!: FieldWidgetComponentProps 不再声称拥有全部键 (#3221) - #3230
Conversation
…key (#3221) `FieldWidgetComponentProps` ended in `[key: string]: any`. That is the objectstack#4075 mechanism: a type that claims to have every key can never be reported as missing one. Three consequences, all closed here: - `props.required` and `props.error` — both declared by the spec's `FieldWidgetPropsSchema`, neither declared here — were legal reads typed `any` and `undefined` at runtime forever; - a misspelled prop (`readOnly` for `readonly`) compiled and did nothing; - any structural/parity comparison against the type was useless in principle, which is why objectui#3161's batch-7 symbol guard was the only detector. The index signature is replaced by a closed set derived from the real call sites, not guessed: the controlled-input contract, the host plumbing the form renderer forwards (`schema`, `dataSource`, `dependentValues`, `dependsOn`, `emptyHint`, `compact`, `onSelectRecord`, `onCreateNew`), and DOM pass-through (`id`, `name`, `autoFocus`, `tabIndex`, focus/click handlers, `aria-*`, and `data-*` as a template-literal key so `keyof` stays finite). Every consumer in the monorepo compiles unchanged. Also read through the type instead of around it: ~20 `(props as any).x` reads of keys the type now declares. Leaving them would have kept the "a typo compiles" half of the defect alive at exactly the sites that matter. The three batch-7 tripwires written to go red on this change (`_IndexSignatureStillThere` / `_RequiredSilentlyReadsAsAny` / `_ErrorSilentlyReadsAsAny`) are replaced by their inverse, plus a new `__tests__/widget-props-contract.test.tsx` that pins the closed contract with `@ts-expect-error` and proves the pass-through behaviour still renders. Deliberately NOT resolved here: the `error` / `errorMessage` divergence (objectui#3222). This change only makes it visible to the compiler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRJtkgUAaVG11FsJQbvZWA
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
commented
Aug 2, 2026
PM 验收:ACCEPT,放行合并队列CI 15 项 13 绿 2 skipped、零红,已独立复核。范围边界守住: 定类型的方法是对的:先让编译器列缺口,再逐条判断没有猜该声明哪些键,而是先只删索引签名、跑 爆炸半径的证据也给对了:全仓 78/78 绿,且 排除
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#3221
packages/fields/src/widgets/types.ts里的[key: string]: any是 objectstack#4075 的机制:一个声称拥有全部键的类型,永远不可能被报告「缺了某个键」。本 PR 把它换成一个从真实调用点推导出来的封闭类型。三个后果,逐条关掉
props.required/props.error(spec 的FieldWidgetPropsSchema声明、本地没有)any,运行时恒undefinedreadOnlyvsreadonly、onchange)新类型怎么定的:先读调用点,再写类型
Issue 给了两个候选。
& React.HTMLAttributes< HTMLElement >被排除,原因是实测的:它声明的onChange?: FormEventHandler与本契约的onChange: (val: T) => void在交集下冲突(strictFunctionTypes逆变,赋值不成立),且不覆盖data-*,更不覆盖schema/dataSource/dependentValues这些真正在传的键。于是走「显式声明」。候选集不是猜的:先只删索引签名、跑
tsc,让编译器把真实缺口列出来,再逐条判断该不该存在。分三组:value/onChange/field/readonly/disabled/className/errorMessage/onUploadingChangerenderFieldComponent与内联编辑宿主确实转发的):schema、dataSource、dependentValues、dependsOn、emptyHint、compact、onSelectRecord、onCreateNewid、name、autoFocus、tabIndex、onBlur/onFocus/onClick、全部aria-*(React.AriaAttributes,闭集),以及data-*data-*用模板字面量索引签名而不是[key: string]。这是关键区别:模板键让keyof保持有限,所以props['data-testid']合法、props.required依然报错 —— 开放的只是 HTML 本来就开放的那一族。爆炸半径实测很小:全仓
turbo run type-check78/78 绿,plugin-detail内联编辑器、plugin-grid单元格编辑器、app-shell的 metadata inspectors、表单渲染器全部零改动通过 —— 因为封闭集就是从它们身上推出来的。顺带:让读取侧也说实话
约 20 处
(props as any).x改为直接读类型(compact、dataSource、disabled、name、id、onCreateNew、onSelectRecord、contextRecord、dependentValues)。留着它们等于保留了「拼错也能编译」那一半缺陷,而且恰恰留在最要命的位置上。未动的是
(field || (props as any).schema)这类元数据配置读取(约 25 处)—— 它们后面紧跟as any,收紧收益低、风险高,且属于field/schema双载体问题,不在本 issue 范围内。批次 7 的三条钉扎
_IndexSignatureStillThere/_RequiredSilentlyReadsAsAny/_ErrorSilentlyReadsAsAny就是为本次改动写的、会在删除索引签名时报红的钉扎(#3224 已合并)。它们按预期删除了 —— 但没有留空,而是换成反向断言(类型现在是封闭的、两个键确实缺失),再加一条data-*仍开放的钉扎。这样把类型重新放宽回[key: string]会失败一个测试,而不是悄悄通过。新增
packages/fields/src/__tests__/widget-props-contract.test.tsx:用@ts-expect-error钉住四类拒绝(required、error、readOnly、onchange),用一个正向字面量钉住每个透传键仍被接受(过度收紧会报红),再用两个渲染断言证明aria-*/data-*/disabled真的还能到达控件 —— 封闭类型不能是靠丢掉真实行为换来的。范围边界:没有碰 #3222
没有把
errorMessage改名为error,没有新增required。本 PR 只让类型说实话,好让 #3222 变成编译器可判定的问题。按要求汇报:移除索引签名后,
error/errorMessage的分歧没有在任何地方产生类型错误(全仓 typecheck 全绿)。查证下来原因比「命名分歧」更尖锐,这条直接关系到 #3222 的选型,已同步评论到该 issue:也就是说这个槽位不只是名字和 spec 不一致,它在两种拼写下都是死的。#3222 的决策因此不只是改名,而是「谁来生产它」。
验证
顺带发现(已另开 issue,不在本 PR 修)
emptyHint被每个选项 widget 丢弃,用户看到的是硬编码英文 #3231 —— 表单渲染器计算的emptyHint被四个选项 widget 全部丢弃,用户看到的是硬编码英文(绕过 i18n)。TextAreaField读取的mobileFullscreenprop 全仓无人生产,注释却说"宿主表单传入" #3232 ——TextAreaField读取的mobileFullscreen全仓无人生产,注释却声称「宿主表单传入」。两条都是同一类:索引签名在时,「读了但没人写」根本不可能被看见。
🤖 Generated with Claude Code
https://claude.ai/code/session_01PRJtkgUAaVG11FsJQbvZWA