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.
Which @angular/* package(s) are the source of the bug?
compiler-cli, forms
Is this a regression?
No
Description
The
NG8115extended diagnostic (uninvokedTrackFunction) fires on the Signal Forms field-identity tracking that the docs recommend, whenever theFieldTreeis reached through a property read instead of directly as the@forloop variable.Field state management → Tracking values for array fields says:
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
UninvokedTrackFunctionCheckwarns when the track expression is aPropertyRead/SafePropertyReadwhose 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.getSymbolOfTemplateExpressionresolves it viagetExpressionTargetto aSymbolKind.Variable, and the check requiresSymbolKind.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 asrow.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 oneNG8115, pointing at loop B; loop A (the documented shape) is silent.NG8115is 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
Please provide the environment you discovered this bug in (run
ng version)Anything else?
Still present on
mainat the time of filing:packages/compiler-cli/src/ngtsc/typecheck/extended/checks/uninvoked_track_function/index.tsis unchanged apart from usinggetTypeOfSymbol(symbol)in place ofsymbol.tsType; there is noFieldTreeexemption.adev/src/content/guide/forms/signals/field-state-management.mdstill documentstrack field.Workaround, for anyone who lands here: invoke it —
track row.field(). TheFieldTreeis aProxyover() => fieldNodewith noapplytrap, so the call just unwraps the proxy to theFieldNodeit is permanently bound to. Proxy and node are one-to-one, so this partitions items identically to tracking the tree itself, andFieldStateis not callable, so the check is satisfied.