skip to content

An Angular template shows [object HTMLTextAreaElement] where a component's note text should be; how can template variable shadowing cause this, and how do you fix it?

level: seniorimportance: should knowfreq 30%

answer

  1. names resolve template first
  2. the ref hides the member
  3. this. reaches the class
  4. rename local names
  5. @let and let-x shadow too

basics

~20 s

Angular resolves template names before component members, so a #note on a textarea shadows the component's note property and {{ note }} prints the element. Rename the reference, or write this.note to read the member explicitly.

solid answer

~40 s

In an Angular template, an unqualified name is looked up first among template variables of the current and enclosing views, `#ref` references, `@let` declarations, `let-x` variables and `@for` items, and only then on the component, where `this` is implied. So `<textarea #note>` next to a component field `note` makes every `{{ note }}` in scope mean the element, and interpolation prints `[object HTMLTextAreaElement]`. Strict template checks do not flag it, because stringifying an element is type-correct. Fix it by renaming the local name (`#noteBox`), or by writing `this.note` where you mean the member. The same shadowing happens with `@let`: `@let total = total();` fails because it reads itself before definition, while `@let total = this.total();` works.

code

html · 8 lines
html
<!-- before: #note shadows the component's note field -->
<p>Saved note: {{ note }}</p>
<textarea #note></textarea>

<!-- after: distinct local name, member read unambiguously -->
<p>Saved note: {{ note }}</p>
<textarea #noteBox></textarea>
<button (click)="save(noteBox.value)">Save</button>

go deeper

for a junior

Know that a #ref or @let with the same name as a component field hides it in the template, and that renaming fixes it.

for a middle

Explain the lookup order, current view then enclosing views then the component, and why this. reads the member.

for a senior

Diagnose type-correct shadowing from odd output, fix the @let self-reference with this., and add a naming convention to review.

for a principal

Weigh naming conventions and lint rules against relying on the type checker, which cannot see intent.

## How Angular resolves a bare name Inside a template expression, `this` is implied: `{{ title }}` normally means the component's `title`. But the template can also declare names of its own: - **template reference variables**: `#note`; - **`@let` declarations**: `@let note = ...;`; - **template input variables**: `let-note` on an `<ng-template>`, the `@for` item and its aliases; - **`@if (x; as note)`** aliases. When the compiler meets an unqualified name, it resolves it through **scopes**: first the view the expression is in, then each enclosing view, and only if nothing matches, the **component instance**. A template name therefore **shadows** a component member with the same name for every expression in its scope. Angular's docs state this directly and give the escape hatch: write `this.` to reference the class member unambiguously. ## The bug in the question ```ts import {Component, signal} from '@angular/core'; @Component({ selector: 'app-order-note', template: ` <p>Saved note: {{ note }}</p> <textarea #note></textarea> <button (click)="save(note.value)">Save</button> `, }) export class OrderNote { note = 'Leave at the door'; save(text: string): void { this.note = text; } } ``` Step by step: 1. `#note` declares a reference in the top-level view. 2. References are resolved for the whole view, so even `{{ note }}` **above** the textarea resolves to the reference, not to the field. 3. The reference holds the `HTMLTextAreaElement`; interpolation stringifies it as `[object HTMLTextAreaElement]`. 4. The strict template type checker is satisfied: interpolating an element is legal, and `note.value` is a real property, so nothing is reported. ## Fixes | Fix | Example | When to prefer it | |---|---|---| | rename the template name | `<textarea #noteBox>` and `save(noteBox.value)` | almost always: removes the ambiguity for every reader | | qualify the member with `this.` | `{{ this.note }}` | when the local name must stay, for example a `@let` that deliberately mirrors a member | | remove the reference | bind the textarea to a signal instead | when the element was only used to read its value | A naming convention helps: suffix references with what they are (`noteInput`, `rowTpl`) so they cannot collide with state. ## Shadowing with @let `@let` shadows in the same way, with one extra trap. Suppose the component has `total = computed(...)`: - `@let total = total();` is an **error**: inside its own initializer, `total` already resolves to the `@let`, which is read before it has been defined. - `@let total = this.total();` works: `this.` bypasses the template scope and reads the member, and from then on `total` in that scope means the `@let` value. Deliberately mirroring a member like this is legitimate; the `this.` form is what makes it compile. ## What does not shadow silently Some collisions are reported instead of shadowing: - a `@let` that reuses the name of another template symbol **in the same scope**, such as a `#ref`, is a conflicting-declaration error; - declaring the same reference twice on one element is an error; - an alias in a `@for` `let` clause cannot reuse the item's name. Across **different** scopes, an inner template name simply wins over outer ones and over the component. ## How to catch it 1. Read the rendered output: `[object HTMLInputElement]`, `[object Object]` or a `TemplateRef` where text was expected points to a shadowed name. 2. Use the language service's go-to-definition on the name in the template; it lands on the `#ref` or `@let`, not on the class field. 3. Adopt the naming convention above in review, since type checks will not catch type-correct shadowing.

  • Why does Angular's strict template type checking not catch a #ref that shadows a component field?
    Because the shadowed expression is still type-correct: interpolating an `HTMLTextAreaElement` is legal, and reading `.value` on it is a real property. The checker verifies types, not intent. It only complains when the shadowed use is invalid for the new type, such as calling a signal-only method on an element.
  • In an Angular template, does a name declared in an inner @if view shadow the same name in the parent template?
    Yes, within that inner view. Lookup starts in the current view and walks outward, so an inner `@let` or reference with the same name wins inside the block and its descendants, while the parent keeps its own meaning. Only two symbols with the same name in the same scope are reported as a conflict.

saying these in an interview costs you the question

  • Component members always take priority over template variables
  • Strict templates will report any name that shadows a class field
  • Writing this. inside an Angular template is a syntax error
  • @let total = total(); reads the component's total signal
  • Shadowing only happens for names declared above the expression