Skip to content

finding(plugin-detail): InlineEditSaveBar's buildConflict takes err: any, so isConcurrentUpdateError's narrowing reaches no typed consumer #6477

Description

@claude

Observation-class finding, surfaced by objectui#6421 / PR #6474 (which made isConcurrentUpdateError's narrowed type honest). Recorded, not edited — the fix there was scoped to the predicate's type, and this is about the consumer on the other side of it.

What

packages/plugin-detail/src/InlineEditSaveBar.tsx:124-125 declares the only thing that consumes a narrowed isConcurrentUpdateError result, and types the error as any:

constbuildConflict=React.useCallback((draft: Record<string,any>,err: any): ConcurrentUpdateConflict=>{constkeys=Object.keys(draft);constcurrent=(err?.currentRecord??null)asRecord<string,unknown>|null;

It reads exactly two things off err, both through the any:

  • err?.currentRecord — line 127, optional-chained and then as-cast to Record<string, unknown> | null. The cast is the only type currentRecord ever gets here; nothing checks it.
  • err?.currentVersion — lines 135 and 147, optional-chained, flowing straight into the ConcurrentUpdateConflict payload.

The call site one hop up, InlineEditSaveBar.tsx:183, does narrow correctly:

if(isConcurrentUpdateError(err)&&canAtomic){setConflict(buildConflict(draft,err));}

...and then hands the narrowed value to a parameter typed any, which discards it. So the predicate's return type is inert at its only use.

Why it is worth a card

PR #6474 changed the narrowed type from code: 'CONCURRENT_UPDATE' (required, and untrue for the name-limb) to code?: 'CONCURRENT_UPDATE'. The predicate now tells the truth, and nothing is listening. That pin's real-world bite today is smaller than its shape suggests, and the reason is one hop downstream, not in the predicate.

This is the downstream-any species: a type boundary defeated one line after it is drawn. Two siblings already on the backlog, different subjects, same shape — objectui#6459 (ObjectGrid's const dataTableSchema: any means the imported DataTableSchema is never applied) and objectui#5155 (BaseSchema's [key: string]: any).

The decision this needs: should buildConflict take the predicate's narrowed type — which is the point of having a narrowed type at all — or is the any load-bearing for a reason that should be written down? Note that if it does take the narrowed type, the as cast on currentRecord becomes redundant, since the narrowed type already declares currentRecord?: Record<string, unknown> | null.

The "no other consumer" claim, as a measurement

The one-call-site claim is a measurement, not a grep that happened to return one hit. Target, on origin/main:

$ git grep -n "isConcurrentUpdateError" origin/main -- 'packages/plugin-detail' 'apps' 'examples' | grep -v CHANGELOG
packages/plugin-detail/src/ConcurrentUpdateDialog.tsx:223:export function isConcurrentUpdateError(err: unknown): err is {
packages/plugin-detail/src/InlineEditSaveBar.tsx:37: isConcurrentUpdateError,
packages/plugin-detail/src/index.tsx:75: isConcurrentUpdateError,
packages/plugin-detail/src/InlineEditSaveBar.tsx:183: if (isConcurrentUpdateError(err) && canAtomic) {

Four lines: one definition, one re-export, one import, one call site.

Positive control 1 — the identical grep shape over the identical paths, for a symbol known to have many consumers, returns 54 lines:

$ git grep -n "useDetailTranslation" origin/main -- 'packages/plugin-detail' 'apps' 'examples' | grep -v CHANGELOG | wc -l
54

Positive control 2 — the grep is not blind outside those paths either; it finds the two deliberate sibling copies, which is what a path-scoped zero would otherwise be indistinguishable from:

packages/data-objectstack/src/index.ts (narrows to the class)
packages/plugin-form/src/occSave.tsx (returns bare boolean)

Scope note: this measures in-repo consumers. @object-ui/plugin-detail publishes isConcurrentUpdateError, so an external consumer could be reading the narrowed type; the claim above is about this repository only.

Where

  • packages/plugin-detail/src/InlineEditSaveBar.tsx:124-125 — the (draft: Record<string, any>, err: any) signature
  • packages/plugin-detail/src/InlineEditSaveBar.tsx:127 — the as Record<string, unknown> | null cast on err?.currentRecord
  • packages/plugin-detail/src/InlineEditSaveBar.tsx:135, :147err?.currentVersion
  • packages/plugin-detail/src/InlineEditSaveBar.tsx:183 — the narrowing that is then discarded
  • packages/plugin-detail/src/ConcurrentUpdateDialog.tsx:223 — the predicate whose narrowed type PR fix(plugin-detail): declare code optional in isConcurrentUpdateError's narrowed type #6474 corrected

Filed flat, ungraded — no user-visible symptom and no test moves either way.


Generated by Claude Code


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