Skip to content

Refuse returns a generic callback's inferred result would drop members of - #1507

Open
cramforce wants to merge 1 commit into
mainfrom
opt/bc-callback-drops
Open

cramforce wants to merge 1 commit into
mainfrom
opt/bc-callback-drops

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

The callback here is passed to a generic call and has no return annotation, and its returns extend the elements it was given:

const items = run(() => {
  if (plain.length === 0) return plain;            // Plain[]
  return plain.map((x) => ({ ...x, extra: true })); // { name; n; extra }[]
});
const asItems: Item[] = items;                     // Item = Plain & { extra?: boolean }

The checker unifies both returns into the narrower Plain[], so run's T is Plain[]. Node keeps extra on the returned objects, and the later wider view reads it back. Natively, converting the extended records into the inferred type was a width copy that dropped extra silently. On main the last line printed [{"name":"a","n":1},…] where Node prints [{"name":"a","n":1,"extra":true},…].

Such a return is now refused by name. The message names the dropped members and points at the fix: an annotation on the callback's return type or on the call's type argument. That annotation names a type that keeps the members, and those programs compile and match Node.

Only conversions the refusal targets change, and gate covers it. A return is refused only when all of these hold:

  • it sits in an unannotated arrow or function expression;
  • that function is a call argument;
  • the conversion would drop declared members, through array elements and promise payloads.

Callbacks whose results the checker types by their own returns keep every member already, so they are unaffected: map with conditional or spread-extended returns, filter + map chains, and reduce returning a spread-extended accumulator. All of them were probed and match Node.

Evidence

  • tests/diagnostics/inferred-callback-drops.ts (snapshot) pins the refusal.
  • tests/corpus/6480-generic-callback-extended-results.ts covers the annotated callback, the type-argument form, the async form, and the map/reduce shapes. It is byte-identical to Node in both lanes.
  • Zero-cost gate against origin/main 4bbf31b: PASS, 2502 of 2502 identical (bench, self-hosted compiler, corpus).
  • pnpm lint and pnpm format:check are clean, pnpm test:ts7 --baselines-only passes, and tests/harness/diagnostics.test.ts passes.

🤖 Generated with Claude Code

…s of

A callback passed to a generic call without a return annotation can
return values the checker unifies into a narrower result type ('return
items' beside 'return items.map((i) => ({ ...i, extra }))' infers
Item[]). Converting the extended objects into that type copied them
and dropped the added members, which Node keeps and a later wider view
reads back. Such a return is now refused by name, pointing at the
annotation that keeps the members.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 20, 2026 8:24am UTC

Comment on lines +13010 to +13011
while (ts.isParenthesizedExpression(site.parent) || ts.isAwaitExpression(site.parent))
site = site.parent;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conditional and satisfies-wrapped callback returns bypass the new refusal and still silently lose extended members.

Fix on Vercel

This branch was successfully deployed

1 active deployment
Preview — c70d9d7e Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant