Skip to content

types: RecordDetailsComponentProps.sections[] omits six member keys @objectstack/spec 17.3.0 declares, and declares one (collapsed) the spec refuses — a spec-valid section does not type-check #8583

Description

@os-justin

Found while implementing objectui#8497 (the sections[].group reference form). Filed unassigned; not fixed there — #8497's PR moves only the two members its own change needs (fields to optional, group added), because the rest is a pre-existing divergence that predates it and deserves its own decision.

The divergence

@object-ui/types packages/types/src/record-components.ts declares the record:details section entry as:

sections?: Array<{
  name?: string;
  label?: string;
  fields?: string[];   // optional as of #8497
  group?: string;      // added by #8497
  collapsible?: boolean;
  collapsed?: boolean;
}>;

@objectstack/spec 17.3.0 (packages/spec/src/ui/component.zod.ts, RecordDetailsProps.sections) declares these members on the same entry:

name · label · columns · group · fields · hideEmpty · collapsible · showBorder · defaultCollapsed · icon · description · headerColor

Set difference, both directions:

direction keys
spec declares, this type omits columns, icon, description, showBorder, defaultCollapsed, headerColor
this type declares, spec refuses collapsed

Why it is not cosmetic

RecordDetailsRenderer honours the omitted keys — it spreads every authored member through to DetailSection, which reads columns, icon, description, showBorder, defaultCollapsed and headerColor. So the TypeScript face is the only layer that says no.

Measured while writing #8497's type pin, and it is how this was found: the literal

const props: RecordDetailsComponentProps = { sections: [{ group: 'terms', columns: 2 }] };

is accepted by @objectstack/spec, rendered correctly by this repo's renderer, and refused by tsc:

error TS2353: Object literal may only specify known properties,
and 'columns' does not exist in type '{ name?: ...; label?: ...; fields?: ...; group?: ...; collapsible?: ...; collapsed?: ... }'

⚠️ Worth recording separately: vitest was green on that same literal — it strips types — and only type-check saw it. A pin for a type-level claim that runs under the test project alone measures nothing.

collapsed runs the other way: it is declared here, no producer can emit it through a spec parse (the section object answers unrecognized_keys for it), and the spec's spelling for that state is defaultCollapsed, which this type does not have. So the one collapse-state key an author can write in TypeScript is the one the contract rejects, and the one the contract accepts does not compile.

Not a duplicate of

Suggested shape (not a ruling)

Converge the entry on the spec's member set: add the six, and decide collapsed deliberately — retire it, or keep it as a recorded legacy alias with the reason written down. ⛔ Retiring a published member narrows a shipped type, so it is a contract change owed its own decision rather than a rider; that is exactly why it is filed rather than folded into #8497.

Refs: objectui#8497 (where this was measured) · objectstack#13855 (the group-reference form) · objectui#7716 · objectui#6955

Filed by the domain:ui dev seat while working objectui#8497, session session_01YBWFb5YgMU5dw8p2VKj16S. Generated with Claude Code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepackage: typespriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions