Uh oh!
There was an error while loading. Please reload this page.
fix(types): StackSchema ships its declared members instead of collapsing under BaseSchema's index signature - #6267
Merged
Conversation
…ing under BaseSchema's index signature `StackSchema` was declared `extends Omit<FlexSchema, 'type'>` and shipped a declaration carrying exactly one property, `type`. `Omit<T, K>` is `Pick<T, Exclude<keyof T, K>>`, and `keyof T` on a type with a string index signature is `string | number` — the literal member names are absorbed. `FlexSchema` inherits `BaseSchema`'s `[key: string]: any` (#5155), so the `Pick` rebuilt a type with the index signature and none of the 25 named members. Nothing errored, because the index signature answers every absent key with `any`. The cost fell on the tools that read the declaration: editor completion on a stack node offered `type` alone, and #6143's docs sweep read `stack.mdx`'s `gap`/`children`/`className` as documenting keys that do not exist. Fixed at the mechanism: the six flex/stack members move to a new exported interface `FlexLayoutProps`, which does not inherit `BaseSchema`. `FlexSchema` and `StackSchema` each extend `BaseSchema, FlexLayoutProps`, so no `Omit` crosses the index signature and the members are declared once. Extending `FlexSchema` directly is unavailable — `'stack'` is not a subtype of `type: 'flex'` (TS2430, measured). `FlexSchema` is unchanged: its member declarations moved byte-identically and its emitted member set is the same 25 names. Guarded by a pin that reads the EMITTED declaration, not the source — a source-level assertion passes on the broken code because the index signature answers with `any`, and that gap is the defect. Part of #6151
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This was referenced Aug 25, 2026
This was referenced Aug 25, 2026
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-litant
marked this pull request as ready for review
August 25, 2026 10:26
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6151
Omit< T, K >) throughout. GitHub's bodysanitizer eats a fragment that opens with an identifier in angle brackets — it parses as an
HTML tag — which is what mangled the original card and forced its rewrite. The spaced and
elided spellings survive. (Proven again on this very body: the first revision used one
unspaced
Omit<FlexSchema, 'type'>in the last bullet, and GitHub silently truncated it toOmit. Every generic here is spaced for that reason.) The exact spellings below werere-derived from the repo, not from the card's prose.
The defect, reproduced on today's
mainpackages/types/src/layout.ts:226declared:Measured against the built
dist/layout.d.tsatef2a3bd8d, through the TypeScript checker:checker.getPropertyOfType(StackSchema, 'gap')returnedfalse; likewisechildren,align,justify,direction,wrap,className. The card's measurement reproducesexactly.
The cause is confirmed to be the index signature
Omit< T, K >isPick< T, Exclude< keyof T, K > >, andkeyof Ton a type carrying astring index signature is
string | number— the literal member names are absorbed. Soexcluding
'type'still leavesstring | number, and thePickrebuilds a type holdingthe index signature and none of the named members.
Isolated to that one variable, with everything else held constant:
The index signature is
BaseSchema's[key: string]: any(#5155), inherited byFlexSchema. That is the whole cause.Which members are supposed to survive
All 25 — the six flex members (
direction,justify,align,gap,wrap,children)plus
BaseSchema's 19 others, plustypenarrowed to'stack'.StackSchemaisdocumented and rendered as "a Flex that defaults to column":
stack.tsxreadsschema.direction,schema.justify,schema.align,schema.gapandschema.wrapdirectly, and the Zod mirror
StackSchemainzod/layout.zod.tsalready declares all six.The fix — at the mechanism
The six shared members move into a new exported interface,
FlexLayoutProps, which doesnot inherit
BaseSchema.FlexSchemaandStackSchemaeach extendBaseSchema, FlexLayoutProps. NoOmitcrosses the index signature any more, and themembers are declared once rather than duplicated — the index signature is lifted out of the
inheritance path the derivation crossed.
Extending
FlexSchemadirectly was measured unavailable, resolving the open question inthe card: an interface may narrow an inherited property only to a subtype, and
'stack'isnot a subtype of
FlexSchema'stype: 'flex'—After the fix, measured on the rebuilt
dist/layout.d.ts:StackSchema -> 25 declared properties, the identical set toFlexSchema.FlexSchemais unchanged. Its six member declarations moved byte-identically (diffof the extracted block against
ef2a3bd8d, exit 0), and its emitted member set is the same25 names before and after. The only declaration whose shape changes is
StackSchema.The pin, and what survives a revert
packages/types/src/__tests__/stack-schema-emitted-members.test.ts— 13 assertions.on the broken code: the index signature answers for the missing key with
any, andanysatisfies everything. That gap is the bug, so a guard that never opens the
.d.tscannotsee it.
It also does not read
dist/. This repo's per-PRtestjob runspnpm testwith nobuild step (turbo's
testtask onlydependsOn: ["^build"]— the dependency closure,never the package's own build), and
packages/typeshas no workspace dependencies, sonothing builds it. A guard reading
dist/layout.d.tswould be absent-or-stale on a cold CIcache — vacuous exactly where it is needed. The same trap is recorded in
package-exports-manifest.test.ts's header. So the guard runs the package's own tsconfigthrough the compiler API, emits declarations to a scratch dir under
node_modules/, andmeasures that.
Ablation — the fix reverted on disk (mutation proven both ways by grep: the
Omitdeclaration back at 1 occurrence, the
FlexLayoutPropsheritage clause at 0), restored undera
trap … EXIT INT TERMwith a cwd-independent command,git diff HEAD --statempty after.Predicted 9 red / 4 green before running; observed exactly that:
StackSchemadeclares EXACTLY whatFlexSchemadeclaresgap/children/align/justify/direction/wrap/className(7)LayoutSchemamember losesBaseSchema's membersFlexSchemacarries its six membersBaseSchemamember set is plausibletypeis still the'stack'literalThe four survivors are the ones that should survive: three are the non-vacuity guards that
exist to prove the population was read at all, and the fourth pins union discrimination,
which the defect never broke. The tripwire named the damage itself:
That third assertion is the class guard: every
LayoutSchemamember extendsBaseSchema,so any future heritage clause that collapses under the index signature reds here — not
only this one interface. Sixteen union members satisfy it today;
StackSchemawas the onethat did not.
Scope check — the rest of the
OmitpopulationEvery
Omitinpackages/types/srcwas measured against the builtdist, since the cardasks for the bound rather than asserting it:
DashboardWidgetSchema,ActionParam,NavigationArea) and all three are healthy — theyOmitover@objectstack/spectypes, which carry no index signature (25, 17 and 5 declared properties respectively).
StackSchemawas the only collapsed heritage clause, so this PR closes the class the cardscopes.
Pickwith literal keys is not affected — it never computeskeyof T. Thepackages/plugin-gridandplugin-dashboardsites are DOM types anyway.Omits DO collapse, same mechanism, different surface:objectql.ts:1480table?: Partial< Omit< ObjectGridSchema, … > >and:1486form?: Partial< Omit< ObjectFormSchema, … > >both measure 0 declared properties(
ObjectGridSchemaalone has 61). Not fixed here — unlikeStackSchemathe correctreplacement shape is a design question rather than a mechanical move, so it is filed
separately rather than smuggled into this PR.
data-protocol.ts:933(BaseValidation) is healthy —ScriptValidationhas no indexsignature (16 of 18 members survive, exactly as written).
Verification
Run at
7fe11e01a, the final commit.pnpm --filter @object-ui/types build— exit 0pnpm --filter @object-ui/types type-check— exit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)pnpm test packages/types/from the repo root —Test Files 59 passed (59),Tests 645 passed (645). Root-only, per the objectui#3378 guard: thepnpm --filter … exec vitestandcd packages/x && pnpm exec vitestforms either refuseloudly or silently run
apps/console's files and report a meaningless green.pnpm --filter @object-ui/types lint— exit 0, 0 errors (240 pre-existing warnings, nonein the touched files).
check-control-bytes,check-type-check-coverage,check-changeset-presence,check-changeset-no-major,check-vi-mock-specifiers,check-package-self-import— allexit 0.
Downstream. This is a tightening — keys that the index signature used to answer as
anyare now typed — so consumers were checked rather than assumed. The sweep was narrowed with
evidence, not by guess:
FlexSchema's emitted member set is identical before and afterand its members moved byte-identically, so the only declaration whose shape changed is
StackSchema, and only code naming it can be affected. Enumerating that population by grepover all
packages/*/src,apps/*/srcandexamples/gives three packages, and all threetype-check green unchanged, with the script name echoed in each run (guarding the
zero-match-
--filter-exits-0 trap):packages/components' two stack/layout test files also pass (10 tests).layout.tsistypes-only and emits an empty
.js, so there is no runtime surface to change.check-readme-exportsexits 1 locally, and it is not this change: all 53 findings aretype entry ./dist/index.d.ts is not on disk — run pnpm build firstfor packages outsidethe closure I built (
app-shell,auth,cli,collaboration,plugin-ai,plugin-designer). Zero mentionpackages/types, and zero are of the fabricated-or-wrong-path kind the gate exists to catch. CI builds everything.
Changeset
minor, nevermajor. It ships asminorrather thanpatchbecause it can newly error aconsumer:
gap: 'large'on a stack node used to type-check through the index signature andis now a
tscerror. Every value it newly rejects is one the renderer never honoured —stack.tsxfeedsgapto a Tailwind numeric scale — so this reports a defect that alreadyexisted rather than removing a capability. Downgrade to
patchif you read that as a fixrather than a break; nothing else in the change depends on it.
Notes for review
FlexLayoutPropsis exported fromlayout.tsand added to the rootindex.tsexportlist, matching how
StyleProps/EventHandlers/ComponentConfigare surfaced frombase.ts. It is not a spec-owned name (checked against@objectstack/spec) and collideswith nothing in-repo.
layout.zod.ts#StackSchemacarries no ledger entry inzod-mirror-parity.test.tsand passed before this change vacuously — every mirror keycompared against
any. It now compares against real declared types and still passes,which is a strengthening of an existing guard rather than a new one.
packages/components/src/__tests__/stack-spacing-alias-removed.test.tsx:12carries aprose comment reading
StackSchema extends Omit< FlexSchema, 'type' >(spaced here so itsurvives the sanitizer), now stale. Left untouched —
packages/componentsis anotheragent's collision bound in this batch. Worth a follow-up.
Generated by Claude Code