Which JavaScript constructs are not allowed in Angular template expressions, and why does {{ Math.round(price) }} fail?
answer
- a JavaScript subset, not JavaScript
- no globals besides undefined and $any
- no new, no bitwise, no declarations
- bindings cannot assign
- handlers are statements
basics
~20 sTemplate expressions are a JavaScript subset: no globals except undefined and $any, no new, no bitwise operators, no destructuring, comma operator or declarations, and no assignments in bindings. Math.round fails because Math is looked up on the component, not on the global scope.
solid answer
~40 sAngular parses templates with its own expression parser. It supports literals, most arithmetic, comparison and logical operators, `??`, the ternary, calls, pipes, and newer additions such as `typeof`, template literals, `in`, `void` and `**`. It rejects `new`, all bitwise operators, destructuring, the comma operator, BigInt literals and declarations (`let`, `const`, `function`, `class`). The only globals are `undefined` and `$any`, so `Math.round(price)` reads `this.Math` on the component - the type checker reports that the property does not exist, and at runtime it would be `undefined`. Property bindings and interpolations also reject assignments and `;` chains; event-binding **statements** allow both but do not allow pipes. The fix is to move the logic into the class, usually a `computed()`, or expose what the template needs as a member.
code
ts · 18 linesimport { Component, computed, input } from '@angular/core';
@Component({
selector: 'app-price-tag',
template: `
<!-- {{ Math.round(price()) }} fails: Math is looked up on the component -->
<span>{{ rounded() }}</span>
<button type="button" (click)="clicks = clicks + 1; log()">Buy</button>
`,
})
export class PriceTag {
readonly price = input.required<number>();
protected readonly rounded = computed(() => Math.round(this.price()));
protected clicks = 0;
protected log() {
console.log('clicked', this.clicks);
}
}go deeper
Know that template expressions are a restricted subset: no globals besides undefined and $any, no new, and no assignments in bindings.
Explain why Math fails (implied this), list the unsupported constructs, and contrast binding expressions with event-handler statements.
Keep logic in the class with computed() and pipes, and review templates for expressions that grew into hidden business logic.
Decide how much the recently added syntax (arrow functions, spread, regex) should be used, weighing template readability against keeping logic testable in classes.
## A JavaScript-like language, not JavaScript An Angular template expression looks like JavaScript but is parsed by **Angular's own expression parser** and compiled into instructions that read from the component. It deliberately supports a **subset**, and the subset has grown release by release. ## What is supported - Literals: strings, numbers, booleans, `null`, object and array literals, regular expressions, template strings and tagged template strings. - Operators: arithmetic (`+ - * / %`, `**`), comparisons, `&&`, `||`, `!`, `??`, the ternary, `typeof`, `void`, `in`, `instanceof`, unary `+`/`-`, property and keyed access. - Calls, including spread arguments, and spread inside array and object literals. - Angular additions: pipes (`value | date`), the safe-navigation `?.`, and the TypeScript non-null assertion `!`. | Feature | Available since | |---|---| | `typeof` | v19.0 | | untagged template literals | v19.2 | | `**`, `in`, `void`, tagged template literals | v20.0 | | `&&=`, `\|\|=`, `??=` in event handlers | v20.1 | | regular expression literals | v21.0 | | spread in array and object literals | v21.1 | | `instanceof`, single-expression arrow functions | v21.2 | ## What is not allowed 1. **Globals.** Only `undefined` and `$any` are available. `Math`, `JSON`, `Number`, `parseInt`, `window`, `document`, `console` and friends are not. 2. **`new`.** `new Date(ts)` is rejected; construct the value in the class. 3. **Bitwise operators** (`&`, `|` as bitwise, `~`, `^`, shifts). A single `|` is the **pipe** operator, so `a | b` means "pipe `a` through a pipe named `b`". 4. **Destructuring** (`const { name } = user`) and the **comma operator**. 5. **Declarations**: `let`/`const`/`var`, `function`, `class`. Since v21.2 an arrow function with a single-expression body is accepted (useful as a callback argument); a block body is rejected with "Multi-line arrow functions are not supported". 6. **BigInt literals** such as `1n`. 7. In **bindings** (interpolation, `[prop]`, `@if` conditions): no assignments ("Bindings cannot contain assignments") and no `;`-chained expressions ("Binding expression cannot contain chained expression"). ## Why Math.round(price) fails Bare identifiers resolve against the component (implied `this`) and template variables. `Math` is neither, so `{{ Math.round(price) }}` means `this.Math.round(this.price)`: - with the template type checker on (strict templates are on by default for new projects), the build reports that property `Math` does not exist on the component; - if it were compiled anyway, `this.Math` is `undefined` and reading `.round` throws a `TypeError` during change detection. Fixes, in order of preference: - **Derive the value in the class**: `rounded = computed(() => Math.round(this.price()))`, then `{{ rounded() }}`. - **Use a pipe** built for formatting numbers when the need is display formatting. - **Expose the global as a member** (`protected readonly Math = Math;`) - it works, but it invites logic into the template. ## Statements in event handlers are different `(click)="..."` holds a **statement**, not an expression. Statements share the expression syntax with two differences: - they **allow assignments** (`count = count + 1`, `open = !open`) and several statements separated by `;`; - they **do not allow pipes** ("Cannot have a pipe in an action expression"). They still have no globals: `(click)="alert('hi')"` looks for `this.alert`. ## Reading a failing template When a template breaks on one of these rules, the message usually names the rule directly: - "Bindings cannot contain assignments" - an `=` (or `+=`, `??=`) inside `{{ }}`, `[prop]` or an `@if` condition; move the write into an event handler or a method. - "Binding expression cannot contain chained expression" - a `;` in a binding; split it or compute the value in the class. - "Cannot have a pipe in an action expression" - a pipe inside `(event)="..."`; transform the value in the handler method instead. - A type-checker error saying a property such as `Math` or `JSON` does not exist on the component - a global was used; derive the value in the class. ## Why the restrictions exist Templates run on every check of their view, are type-checked against the component, and are compiled ahead of time. Keeping them free of side effects, object construction and global state makes change detection predictable (a binding that assigned would change state while Angular reads it) and pushes real logic into the class where it can be tested.
- What does {{ total | 2 }} mean in an Angular template if the author intended a bitwise OR?Angular has no bitwise operators; `|` is the pipe operator. The parser treats the right-hand side as a pipe name, and a number is not a valid pipe name, so the template fails to compile. Compute the bitwise result in the class.
- Why may an event binding assign a field while a property binding may not?A property binding is evaluated during change detection to read a value; assigning there would mutate state in the middle of a check. An event binding runs in response to an event, outside the check, so it is parsed as a statement that allows assignments and `;` chains.
- Can a template pass an arrow function as a callback argument in Angular 22.2?Yes, since v21.2 the parser accepts an arrow function whose body is a single expression, such as `items().filter(i => i.active)`. Block bodies are rejected. It is still worth keeping such logic in a `computed()`, because the expression re-runs on every check of the view.
saying these in an interview costs you the question
- Angular templates accept any valid JavaScript expression
- Globals like Math work once imported in the component file
- new Date() is fine inside an interpolation
- A single | in a template is a bitwise OR
- Event handlers support pipes just like bindings
- Property bindings may assign values, e.g. [value]="x = 1"