In an Angular template, what does {{ }} interpolation evaluate its expression against, and what text does it render for null, undefined or an object?
answer
- this is implied
- template variables shadow members
- result becomes a string
- nullish renders as nothing
- signals must be called
basics
~20 sInterpolation evaluates its expression against the component instance, with this implied, plus any template variables in scope, converts the result to a string and writes it as text. null and undefined render as an empty string; an object renders through String().
solid answer
~40 sAn expression inside `{{ }}` resolves bare names against the **component instance** (`this` is implied) and against **template variables** in scope - a `@let`, a `#ref` or a loop variable with the same name shadows the class member, and `this.name` reaches the member explicitly. Angular re-evaluates the expression each time the view is checked and updates the text node only when a value changed. The result is stringified: `null` and `undefined` become an empty string, strings are used as they are, and anything else goes through `String()`, so a plain object prints `[object Object]`. A signal must be called, `{{ count() }}`; writing `{{ count }}` prints the function (in development builds something like `[Signal: 3]`). Interpolation in an attribute value, `title="Hi {{ name() }}"`, compiles to a property binding of the concatenated string.
code
ts · 17 linesimport { Component, signal } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `
<p>Hello, {{ nickname() }}</p> <!-- '' while nickname is null -->
<p>Hello, {{ nickname() ?? 'guest' }}</p>
@let title = 'Draft';
<h2>{{ title }} / {{ this.title }}</h2> <!-- 'Draft / Welcome' -->
<p title="Signed in as {{ nickname() }}">{{ unread() }} unread</p>
`,
})
export class Greeting {
protected readonly title = 'Welcome';
protected readonly nickname = signal<string | null>(null);
protected readonly unread = signal(3);
}go deeper
Remember that names resolve on the component with this implied, that null and undefined print nothing, and that signals need parentheses.
Explain the stringification table, shadowing by template variables and this., and how attribute interpolation becomes a property binding.
Spot silent empty output from nullish values and costly expressions re-run on every check, and move that logic into computed() values.
Set team conventions for display logic: what may live in a template expression and what must be derived in the class so templates stay cheap and testable.
## What interpolation is **Interpolation** is the `{{ expression }}` syntax in an Angular template. The compiler turns every interpolation into an instruction that evaluates the expression when the view is checked and writes the resulting string into a DOM text node (or, inside an attribute value, into a property). It is the simplest one-way binding Angular has: data flows from the component to the DOM, never back. ## What names resolve to An Angular template expression has no access to the global scope. Its **lexical context** is: - the **component instance** - every bare identifier such as `userName` or `greet(...)` is read as `this.userName` or `this.greet(...)`; `this` is implied; - **template variables** in scope - `@let` declarations, `#ref` references, and the variables a block such as `@for` introduces (`$index` and friends); - two allowed globals, `undefined` and the `$any()` cast. When a template variable and a class member share a name, the **variable wins** (it shadows the member). Writing `this.title` explicitly reaches the class member. This matters most with `@let`, which is often declared under the same name as a signal to narrow its type. ## How the value becomes text The runtime converts every interpolated value with a small helper: | Value | Rendered text | |---|---| | a string | the string itself | | `null` or `undefined` | `''` (nothing) | | a number or boolean | `String(value)`, e.g. `3`, `true` | | a plain object | `String(value)`, i.e. `[object Object]` | | an array | `String(value)`, i.e. comma-joined items | | an object with a custom `toString()` | whatever `toString()` returns | Two consequences come up in interviews: 1. **Nullish values disappear silently.** `Hello, {{ nickname }}` renders `Hello, ` when `nickname` is `null` - no error, no `null` text. That is convenient, but it also hides a missing value, so give an explicit fallback with `??` when the empty output would confuse the user. 2. **Objects are not formatted.** `{{ user }}` prints `[object Object]`; bind a property (`{{ user.name }}`) or transform the value (a pipe or a `computed()` in the class) instead. The value is written as **text**, not parsed as markup. ## When it re-evaluates Angular evaluates each interpolation every time its view is **checked**. Since v22 components default to `OnPush`, so a view is checked when something marks it dirty (a signal it reads changes, an input changes, a template event fires). During a check, Angular compares the new value with the previous one and touches the DOM only if it changed. Because the expression can run on every check, keep it cheap - heavy work belongs in a `computed()` in the class. ## Signals in interpolation A signal is a getter **function**. Reading its value requires calling it: - `{{ count() }}` renders the current value and registers the view as a consumer, so the view is refreshed when the signal changes. - `{{ count }}` interpolates the function itself. In development builds a signal's `toString()` prints `[Signal: 3]`; in production it is the function's source text. The **extended diagnostic** "Signals must be invoked in template interpolations" (`NG8109`) flags this at build time. ## Interpolation inside attributes `<img alt="Avatar of {{ userName() }}">` is not a string attribute any more: the compiler turns it into a **property binding** of the concatenated string (`alt` is set to `'Avatar of ' + value`). Binding a non-string value this way therefore stringifies it first, which is why `[prop]="value"` is the form to use when the target property expects a number, boolean or object. ## Common mistakes - Referring to `window`, `Math` or `JSON` - they are looked up on the component and are not found. - Interpolating a signal without calling it. - Expecting `null` to print as the word "null". - Doing expensive work (sorting, filtering) directly inside `{{ }}`.
- Why can a template show '[object Object]' after a refactor that changed a string input to an object?Interpolation stringifies whatever the expression returns. A string prints as is, but a plain object goes through `String()`, which yields `[object Object]`. Bind the specific field, such as `{{ user().name }}`, or derive a display string in a `computed()`.
- How do you reach a component field named title from a template that also declares @let title?Write `this.title`. Bare names resolve to template variables first, so the `@let` shadows the field; the explicit `this.` prefix skips template variables and reads the component instance.
saying these in an interview costs you the question
- Interpolating null prints the word null
- Template expressions can read window, Math or JSON
- {{ count }} renders a signal's current value without calling it
- Interpolation re-renders the text node on every check even when unchanged
- A class member always wins over a template variable of the same name