Which @angular/* package(s) are relevant/related to the feature request?
compiler
Description
@let correctly follows normal lexical/block scoping: inside its own block it shadows any outer identifier of the same name (e.g. a class member), and outside that block the outer identifier applies again — this is consistent, just like JavaScript's let.
The problem is that this shadowing is easy to miss when reading a template, especially when the @let is declared deep inside a nested @if/@for block while the same-named class member is used elsewhere in the same template.
Nothing in the compiler flags the collision, so it's easy to accidentally rely on the wrong one depending on which part of the template you're looking at, without any warning.
Proposed solution
A compiler/language-service diagnostic (opt-in or warning by default) when a @let name shadows an existing
component class member (property, getter/setter, or plain field), similar to a no-shadow lint rule. This would
make the (correct, but easy-to-miss) scoping explicit instead of silent.
Alternatives considered
- Renaming the
@let to avoid any collision — works, but relies on developers noticing the collision themselves.
- A custom ESLint rule for Angular templates — could work, but a compiler-level diagnostic would catch it for
everyone by default without extra tooling setup.
Anything else?
Example illustrating the shadowing that prompted this request, with multiple usage contexts of the same name:
@if (thing$ | async; as thing) {
@let flag = flag$ | async;
<!-- (A) Inside the block where @let is declared: "flag" resolves to the -->
<!-- @let value (the unwrapped Observable value). -->
@if (flag) {
<inner-block />
}
}
<!-- (B) Outside/after the block where @let was declared: "flag" is no -->
<!-- longer in scope here, so this resolves to the class member (the -->
<!-- getter below) instead — a different value/source than (A). -->
@if (flag) {
<toggle-button />
}
export class ExampleComponent {
readonly flag$ = new BehaviorSubject<boolean>(false);
// Class member sharing the same name as the @let above — setter-only,
// no getter.
set flag(flag: boolean) {
this.flag$.next(flag);
}
}
- At (A),
flag refers to the @let binding — this is correct and intentional, standard block scoping.
- At (B),
flag refers to the class member — but since it's a setter without a getter, there is no readable value
behind it at all. Reading flag here doesn't just resolve to "the wrong value"; it silently resolves to a
member that was never meant to be read from a template expression in the first place.
Both resolutions are individually "correct" per normal scoping rules, but nothing in the template highlights that
these two flag references — just a few lines apart, with the exact same name — are bound to two completely
different things, one of which isn't even readable. This is exactly the kind of easy-to-miss shadowing this
request is about: a warning at the point the @let is declared (flagging that it shadows an existing class
member) would make the split obvious immediately, instead of only being discovered by debugging why (B) doesn't
behave like (A).
Which @angular/* package(s) are relevant/related to the feature request?
compiler
Description
@letcorrectly follows normal lexical/block scoping: inside its own block it shadows any outer identifier of the same name (e.g. a class member), and outside that block the outer identifier applies again — this is consistent, just like JavaScript'slet.The problem is that this shadowing is easy to miss when reading a template, especially when the
@letis declared deep inside a nested@if/@forblock while the same-named class member is used elsewhere in the same template.Nothing in the compiler flags the collision, so it's easy to accidentally rely on the wrong one depending on which part of the template you're looking at, without any warning.
Proposed solution
A compiler/language-service diagnostic (opt-in or warning by default) when a
@letname shadows an existingcomponent class member (property, getter/setter, or plain field), similar to a
no-shadowlint rule. This wouldmake the (correct, but easy-to-miss) scoping explicit instead of silent.
Alternatives considered
@letto avoid any collision — works, but relies on developers noticing the collision themselves.everyone by default without extra tooling setup.
Anything else?
Example illustrating the shadowing that prompted this request, with multiple usage contexts of the same name:
@if (thing$ | async; as thing) { @let flag = flag$ | async; <!-- (A) Inside the block where @let is declared: "flag" resolves to the --> <!-- @let value (the unwrapped Observable value). --> @if (flag) { <inner-block /> } } <!-- (B) Outside/after the block where @let was declared: "flag" is no --> <!-- longer in scope here, so this resolves to the class member (the --> <!-- getter below) instead — a different value/source than (A). --> @if (flag) { <toggle-button /> }flagrefers to the@letbinding — this is correct and intentional, standard block scoping.flagrefers to the class member — but since it's a setter without a getter, there is no readable valuebehind it at all. Reading
flaghere doesn't just resolve to "the wrong value"; it silently resolves to amember that was never meant to be read from a template expression in the first place.
Both resolutions are individually "correct" per normal scoping rules, but nothing in the template highlights that
these two
flagreferences — just a few lines apart, with the exact same name — are bound to two completelydifferent things, one of which isn't even readable. This is exactly the kind of easy-to-miss shadowing this
request is about: a warning at the point the
@letis declared (flagging that it shadows an existing classmember) would make the split obvious immediately, instead of only being discovered by debugging why (B) doesn't
behave like (A).