fix(row-model): ColumnValueOf resolves to unknown, not never - #462
Merged
Conversation
`ColumnValueOf` fell back to `never` for any column that declares no
`accessor`. `never` is assignable to everything, so every runtime guard
written against such a value compiled green while checking nothing:
if (columnId === "quantity" && typeof value === "number" && value < 0)
That is the guard shipped in the editing page's worked example, and it
could never have been wrong. No example in the docs corpus uses accessor
columns, so `value` was `never` in every rows-mode `onRowChange` the docs
teach.
`unknown` is the honest fallback: it forces the guard instead of
accepting it.
`never` is still load-bearing INSIDE the distribution — it is the union
identity, so non-matching column members must vanish rather than widen
every answer for a mixed tuple. The fallback is applied outside the
distribution via `[X] extends [infer TResolved]`; a naked `extends infer`
would distribute, and distributing over `never` short-circuits the whole
conditional to `never`, which is precisely the case the fallback exists
to catch. Both behaviors are pinned in column-value-of.test.ts.
Fallout:
- apps/bench passed `columnId as never` to `distinctValues`, which forced
the id to match no column. `BenchColumn` declares an accessor, so plain
`columnId` resolves correctly; the cast was papering over this hole.
- grid/editing.mdx's `value` row claimed "inferred from `columnId`",
true only for accessored columns. Replaced with an accurate note.
- The example passed `<PretableSurface<StockItem>`, which bought nothing
(TRow is inferred from `rows`) and read as broken JSX.
- The docs-guard roster excused that table on the grounds that it
documents `PretableSurfaceProps.onCellEdit` — a prop that exists
nowhere in the repo. That stale excuse is why the false claim survived.
Replaced with the real limitation: the member reader handles interfaces
only and cannot bind a mapped-type-indexed union.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
blove
enabled auto-merge (squash)
August 17, 2026 03:31
Contributor
Vercel preview readyPreview: https://pretable-n089t4ye6-cacheplane.vercel.app Updated automatically by the |
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 free
to 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.
The defect
valuein rows-modeonRowChangewas typednever.neveris assignable to everything, so this guard — shipped in the editing page's worked example — compiled green while checking nothing:Root cause:
ColumnValueOffell back toneverfor any column that declares noaccessor. No example in the docs corpus uses accessor columns, so this hit every rows-modeonRowChangethe docs teach — not just this one page.Found while investigating a report that the cell-editing page showed "invalid code". The file typechecked and linted clean; the defect was only visible by probing the type.
The fix
ColumnValueOfnow falls back tounknown, which forces the guard instead of accepting it.neveris still load-bearing inside the distribution — it is the union identity, so non-matching members of a mixed column tuple must vanish rather than widen every answer. The fallback is applied outside the distribution via[X] extends [infer TResolved]; a nakedextends inferwould distribute, and distributing overnevershort-circuits the whole conditional tonever, which is precisely the case the fallback exists to catch.Both behaviors are pinned in
column-value-of.test.ts— including the mixed-tuple precision assertions, which were already green before the fix and so will catch any widening.Fallout, all fixed
apps/benchpassedcolumnId as nevertodistinctValues, forcing the id to match no column.BenchColumndeclares an accessor, so plaincolumnIdresolves correctly — the cast was papering over this same hole.grid/editing.mdx'svaluerow claimed "inferred fromcolumnId", true only for accessored columns. Replaced with an accurate note plus a narrowing snippet.<PretableSurface<StockItem>, which bought nothing (TRowis inferred fromrows) and read as broken JSX.PretableSurfaceProps.onCellEdit— a prop that exists nowhere in the repo. That stale excuse is why the false claim survived. Replaced with the real limitation: the member reader handles interfaces only and cannot bind a mapped-type-indexed union. Follow-up filed to teach it that shape.Verification
Rebased onto
origin/main(10 commits ahead, incl. #447 touchingrow-model); all gates re-run against the new base. The.api.mdfiles merged cleanly and were regenerated to confirm the merged text matches what the generator produces.pnpm typecheckclean;pnpm lintclean (1 pre-existing unrelated TanStack Virtual warning)api:checkclean, with noae-forgotten-exportwarning (this repo had zero; the fix was restructured to avoid introducing the first)One number worth a look: the extra conditional costs ~5% more type instantiations (deterministic: 126,366 → 132,161 on
columns-500, against a 151,640 budget). Memory oncolumns-500reads 98.9–101.6 MiB across runs against a 109.9 MiB budget — ~92% of ceiling, up from ~83%. Memory is noisy run-to-run; instantiations are not. I did not re-baseline the budgets, since that would silently loosen a gate.🤖 Generated with Claude Code