Skip to content

NG8115 fires on the documented Signal Forms field-identity tracking when the field is reached through a property #70207

Description

@pkurcx

Which @angular/* package(s) are the source of the bug?

compiler-cli, forms

Is this a regression?

No

Description

The NG8115 extended diagnostic (uninvokedTrackFunction) fires on the Signal Forms field-identity tracking that the docs recommend, whenever the FieldTree is reached through a property read instead of directly as the @for loop variable.

Field state management → Tracking values for array fields says:

In signal forms, a @for block over a set of fields should be tracked by field identity.

@for (field of form.emails; track field) {
  <input [formField]="field" />
}

The forms system is already tracking the model values within the array and maintaining a stable identity of the fields it creates automatically.

That exact form is fine. But it is common to iterate a small view model that pairs each field with something else (an index for a remove button, a sibling value from the model, drag state), and then the very same field identity is expressed as row.field:

@for (row of rows(); track row.field) { ... }   <!-- NG8115 -->

Both loops track the identical object with the identical semantics; only the shape of the expression differs. The first is silent, the second warns.

Cause

UninvokedTrackFunctionCheck warns when the track expression is a PropertyRead/SafePropertyRead whose type has at least one call signature. FieldTree<T> is () => FieldState<T> plus proxied children, so it is callable — but the call signature is the state accessor, and its identity is precisely what the docs ask you to track. The check cannot distinguish "identity of a callable object" from "forgot to invoke the track function".

The bare-loop-variable case escapes only by accident: SymbolBuilder.getSymbolOfTemplateExpression resolves it via getExpressionTarget to a SymbolKind.Variable, and the check requires SymbolKind.Expression. So the exemption is a symbol-kind artifact, not a carve-out for Signal Forms — which is why the documented shape passes and every other spelling of the same intent does not.

The suggested fix in the message is actively harmful here: invoking with arguments is meaningless for a FieldTree, and the two obvious ways to make the warning go away — tracking by $index, or by a value such as row.field().value() — are exactly the patterns Signal Forms warns against (see #66711, #66796). Tracking by index reuses stateful inputs across a delete and moves focus to the wrong row.

Suggested fix

Skip the check when the track expression's type is a FieldTree (or, more generally, when the type is callable with zero required parameters and is not a template reference to a method), so that field-identity tracking is accepted regardless of how the field is reached.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/stackblitz-starters-hedc1b4t?file=src%2Fmain.ts

The whole repro is the single component in src/main.ts. It produces exactly one NG8115, pointing at loop B; loop A (the documented shape) is silent. NG8115 is a compile-time extended diagnostic, so look for it in the build output or as an editor squiggle — the page itself renders fine.

Please provide the exception or error you saw

▲ [WARNING] NG8115: NG8115: The track function in the @for block should be invoked: row.field(/* arguments */). Find more at https://v21.angular.dev/extended-diagnostics/NG8115 [plugin angular-compiler]

    libs/.../ng8115-repro.ts:14:4:
      14 │     @for (row of rows(); track row.field) {
         ╵     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 21.2.5
Node: 24.19.0
Package Manager: bun
Angular: 21.2.6
... animations, cdk, common, compiler, compiler-cli, core, forms
... material, platform-browser, router

Package                         Version
---------------------------------------------------------
@angular-devkit/build-angular   21.2.5
typescript                      6.0.2

Anything else?

Still present on main at the time of filing:

  • packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_track_function/index.ts is unchanged apart from using getTypeOfSymbol(symbol) in place of symbol.tsType; there is no FieldTree exemption.
  • adev/src/content/guide/forms/signals/field-state-management.md still documents track field.

Workaround, for anyone who lands here: invoke it — track row.field(). The FieldTree is a Proxy over () => fieldNode with no apply trap, so the call just unwraps the proxy to the FieldNode it is permanently bound to. Proxy and node are one-to-one, so this partitions items identically to tracking the tree itself, and FieldState is not callable, so the check is satisfied.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: compilerIssues related to `ngc`, Angular's template compilergemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions