Skip to content

finding(types): StackSchema, declared as Omit-of-FlexSchema-minus-type, erases every named member — it ships declaring only type, because Omit collapses under BaseSchema's index signature #6151

Description

@yinlianghui-tw

⚠️Rewritten 2026-08-24 because GitHub's body sanitizer silently ate every generic argument in the first version — including inside a fenced code block — which inverted the finding's meaning (extends Omit-of-FlexSchema-minus-type was rendered as extends Omit). Angle-bracket generics are therefore written in words throughout. The sanitizer strips a fragment that opens with an identifier in angle brackets, because it parses as an HTML tag; the escaped and elided spellings survive. Same hazard as AGENTS.md's "GitHub mutates body BYTES" clause.

Measured while sweeping #6143. Filed unassigned, not fixed there.

The fact

packages/types/dist/layout.d.ts:215 declares StackSchema as extending Omit of FlexSchema minus the key type, then redeclaring type: 'stack'. In words, because the literal spelling does not survive here:

export interface StackSchema extends Omit<FlexSchema, 'type'> { type: 'stack'; }

FlexSchema declares 25 members — gap, children, align, justify, direction, wrap, plus everything inherited from BaseSchema. StackSchema is intended to be all of them minus type.

It is not. Measured against the built dist through the gate's own derivePackageTypePaths():

FlexSchema -> 25 declared properties: align ariaLabel body children className data
description direction disabled disabledOn gap hidden hiddenOn id justify
label name placeholder style testId type visible visibleOn visibleWhen wrap
StackSchema -> 1 declared property: type

checker.getPropertyOfType(StackSchema, 'gap') returns false. Same for children, align, justify.

Why

FlexSchema inherits BaseSchema's [key: string]: any (#5155).

Omit of T minus K is defined as Pick of T over Exclude of keyof T minus K. And keyof T, on a type carrying a string index signature, is string | number — the literal member names are absorbed. So excluding 'type' from string | number still leaves string | number, and the Pick reconstructs a type that has the index signature and none of the named members.

This is the same absorption #5155's clarification comment measures, in its REJECT half — but here it does not merely fail to reject an unknown key, it deletes known ones from the shipped declaration.

What it costs

Nothing at runtime, and nothing that produces an error — which is why it has survived. What it costs is every tool that reads the declaration:

Scope

StackSchema is the one measured. ⚠️Any other interface whose heritage clause is an Omit, a Pick, or any mapped type over a schema type carries the identical erasure, and enumerating them is the first step of a fix rather than something this card asserts. Grepping for extends Omit and extends Pick across packages/*/src/**/*.ts would bound it.

Candidate directions, none ruled here

  • Restate the members on StackSchema instead of deriving them — honest and immediate, but a duplicate to keep in sync.
  • Drop the Omit and let StackSchema extend FlexSchema directly, narrowing type. An interface may narrow an inherited property only to a compatible subtype, and 'stack' is not a subtype of FlexSchema's type unless that is widened — so this needs checking rather than assuming.
  • Fix it at the root by removing the index signature from BaseSchema — which is 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, already ruled (option C, phased zod .strict()). That ruling targets the rejection layer, not this erasure, so it is worth confirming whether its program incidentally closes this or leaves it standing.

The third is why this is filed rather than fixed: it may want sequencing behind #5155 rather than a local patch.

Refs: #5155 (the index signature, and the keyof absorption its clarification comment measures) · #6143 (where this surfaced) · #6150 (sibling finding from the same sweep, different mechanism).


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions