Skip to content

finding(types): BaseSchema's [key: string]: any leaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155

Description

@os-zhuang

Found while implementing #5018 (ObjectMapSchema alignment). Filed unassigned, not claiming. Observation-class: nothing is broken today that a user hits directly — what is recorded is a structural ceiling on what "declared = enforced" can mean for component schemas.

The fact

packages/types/src/base.ts closes BaseSchema with:

/** * Additional properties specific to the component type. * This index signature allows type-specific extensions. */[key: string]: any;

Every component schema in the repo extends BaseSchema (ObjectGridSchema, ObjectFormSchema, ObjectMapSchema, …). An index signature disables TypeScript's excess-property check, so a misspelled top-level key type-checks on every one of them:

constschema: ObjectMapSchema={type: 'object-map',objectName: 'stores',enableClusterng: true,// accepted — no error, no hint};

This was measured, not inferred: #5018's first draft pinned the opposite with @ts-expect-error, and tsc answered TS2578: Unused '@ts-expect-error' directive on exactly those two pins. The pins were corrected to record the real boundary (packages/plugin-map/src/ObjectMap.schemaAlignment.test.tsx, the "does NOT reject a misspelled TOP-LEVEL key" case).

Why it is worth recording

  1. It silently bounds a whole card class.ObjectMapSchema(types)声明的键几乎与 ObjectMap 实际读的键无交集:map 块整体未建模,组件 props 类型写的是 ObjectGridSchema,每处 map 读取都走 as any #5018 was ruled precisely to make a misspelling loud — "latitudeFieId currently renders a silent empty map". The fix does achieve that, but only because the map BLOCK is a plain interface with no index signature. The same card's top-level half is unenforceable, and the next "declare what the renderer reads" card will discover this again from scratch unless it is written down. The finding cards already on this shape (finding(plugin-grid): ObjectGrid 在 ObjectGridSchema.columns 上容忍未声明的 accessorKey / header 拼写 —— 声明类型是 strict 的 ListColumn,#3104 的列身份门禁结构上看不见这条支路 #5068, finding(types): MobileOverrides.navigation 发布了三值导航词表,全仓零读点 —— 与 #3985 同族的另一处「声明即全部」 #4919, finding(docs/types): content/docs/blocks/block-schema.mdx 教的整族 block 词汇表(含节点位置的 slot)没有任何 renderer #4895, [finding] Studio 的导航消费端读了六种 app nav schema 拒绝的拼写,外加一个不存在的 view 变体 #4881) are all about declaration-vs-consumption forks; this is the reason one whole direction of them cannot be closed by declaring.

  2. It is the exact hiding place the platform ranks as most expensive. Loose declaration plus tolerant consumption is where AI-authored metadata errors survive, and an index signature is the loosest possible declaration — it accepts a key the author invented, at the layer that is supposed to reject it.

  3. The zod half does not cover for it either.packages/types/src/zod/objectql.zod.ts builds component schemas with BaseSchema.extend({...}), and those are not .strict() — so an unknown top-level key survives a safeParse too. Neither layer says anything.

What is NOT being claimed

That the index signature should simply be deleted. It is load-bearing for the schema-driven model — renderers do carry keys the shared base cannot enumerate, and the doc comment says so. The costs of the alternatives are real and want measuring before anything is ruled:

  • Delete it, declare per-component: strongest authoring-time guarantee; needs every renderer's actually-read key set declared first (that is the ObjectMapSchema(types)声明的键几乎与 ObjectMap 实际读的键无交集:map 块整体未建模,组件 props 类型写的是 ObjectGridSchema,每处 map 读取都走 as any #5018 shape, repeated ~20 times), and would break any consumer passing through extra keys.
  • Keep it on BaseSchema, drop it from the leaf component interfaces (Omit-and-restate, or a Strict<T> helper applied at the leaves): closes authoring while leaving the base's extensibility story intact; costs a mechanical change at every leaf and a gate to keep new leaves in line.
  • Move the enforcement to the zod layer (.strict() on the component schemas, publish-time validation rejecting loudly): catches the AI-authored-metadata case where it actually lands, without touching the TS surface; does nothing for a TypeScript author's editor.

Triage decides whether this is worth a decision at all — it may well be correct to keep the index signature and stop implying that declared surfaces are closed. In that case the useful output is a documented statement of the ceiling, so cards stop being written as if declaring a key made its neighbours illegal.

Provenance

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions