Skip to content

Fix ko.unwrap for find/findLast/at(-1) results in LINQ JS translation (FirstOrDefault/LastOrDefault with predicate) - #2106

Draft
tomasherceg with Copilot wants to merge 2 commits into
mainfrom
copilot/fix-firstordefault-where-runtime-error
Draft

Fix ko.unwrap for find/findLast/at(-1) results in LINQ JS translation (FirstOrDefault/LastOrDefault with predicate)#2106
tomasherceg with Copilot wants to merge 2 commits into
mainfrom
copilot/fix-firstordefault-where-runtime-error

Conversation

Copilot AI commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Regression in v5: FirstOrDefault(pred), LastOrDefault(), and LastOrDefault(pred) JS translations generated a hard observable call expr() on the returned array item. When array items are plain mapped objects (e.g. control property arrays), calling the item as a function throws at runtime.

Root cause

firstOrDefaultPred, lastOrDefault, and lastOrDefaultPred translators in JavascriptTranslatableMethodCollection.cs annotated their result with VMPropertyInfoAnnotation without isObservable: false. This caused JsViewModelPropertyAdjuster to set ResultIsObservableAnnotation, which then caused KnockoutObservableHandlingVisitor to emit a hard expr() unwrap — correct only when items are guaranteed to be ko-observable functions, but not when they're plain objects.

Before:

// FirstOrDefault(f => f.KlicRazeni == "")
array().find((f) => ko.unwrap(f).KlicRazeni() == "")()  // item() — fails if item is plain object

After:

array().find((f) => ko.unwrap(f).KlicRazeni() == "")  // ko.unwrap — safe for both observables and plain objects

Changes

  • firstOrDefaultPred (find): add isObservable: false + ResultMayBeObservableAnnotation → emits ko.unwrap(find(pred)) instead of find(pred)()
  • lastOrDefault (at(-1)): same pattern → ko.unwrap(at(-1)) instead of at(-1)()
  • lastOrDefaultPred (findLast): same pattern → ko.unwrap(findLast(pred)) instead of findLast(pred)()

ko.unwrap is already used for lambda parameters inside predicates (e.g. ko.unwrap(f) inside find), making this consistent. For actual ko-observables ko.unwrap(x) === x(), so the behavior is equivalent; for plain objects it returns the object itself rather than throwing.

Note: Where(pred).FirstOrDefault() (Variant B from the issue) goes through BuildIndexer which is not changed in this PR. That path requires a more targeted fix to avoid affecting Select-mapped arrays where items are already plain values.

…irstOrDefault/LastOrDefault with predicate)

Co-authored-by: tomasherceg <5599524+tomasherceg@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix FirstOrDefault and Where predicates in JS translation Fix ko.unwrap for find/findLast/at(-1) results in LINQ JS translation (FirstOrDefault/LastOrDefault with predicate) Aug 30, 2026
Copilot AI requested a review from tomasherceg August 30, 2026 19:42
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.

FirstOrDefault/Where().FirstOrDefault() with property-comparing predicate broken in JS translation (v5) — regression of #1870/#1871

2 participants