Skip to content

feat(compiler): allow template to access private props - #70188

Merged
thePunderWoman merged 1 commit into
angular:mainfrom
JeanMeche:discard-private-props
Aug 18, 2026
Merged

thePunderWoman merged 1 commit into
angular:mainfrom
JeanMeche:discard-private-props

Conversation

@JeanMeche

@JeanMeche JeanMeche commented Aug 13, 2026 •

Copy link
Copy Markdown
Member

To allow this we'll catch the errors during typechecking and discard it.

As context, when setting isolatedDeclarations: true this requires developers to explicitly type every property but the private ones. By allowing private properties to be used in templates we discard the actually for explicit typing for template only properties.

@angular-robot angular-robot Bot added detected: feature PR contains a feature commit area: compiler Issues related to `ngc`, Angular's template compiler labels Aug 13, 2026
@ngbot ngbot Bot added this to the Backlog milestone Aug 13, 2026
@JeanMeche
JeanMeche force-pushed the discard-private-props branch 2 times, most recently from 5b918f8 to 416c285 Compare August 13, 2026 21:19
@angular-robot angular-robot Bot added the area: build & ci Related the build and CI infrastructure of the project label Aug 13, 2026
@JeanMeche
JeanMeche force-pushed the discard-private-props branch 4 times, most recently from 6733a6f to 0509c48 Compare August 13, 2026 21:46
@JeanMeche
JeanMeche marked this pull request as ready for review August 13, 2026 22:42
@JeanMeche
JeanMeche force-pushed the discard-private-props branch 2 times, most recently from 1ab10b8 to b468975 Compare August 13, 2026 23:17
To allow this we'll catch the errors during typechecking and discard it.

As context, when setting `isolatedDeclarations: true` this requires developers to explicitly type every property but the `private` ones. By allowing private properties to be used in templates we discard the actually for explicit typing for template only properties.
@JeanMeche
JeanMeche force-pushed the discard-private-props branch from b468975 to f7a1fab Compare August 13, 2026 23:32
@JeanMeche
JeanMeche requested a review from alxhub August 13, 2026 23:33
@JeanMeche JeanMeche added target: minor This PR is targeted for the next minor release and removed area: build & ci Related the build and CI infrastructure of the project detected: feature PR contains a feature commit labels Aug 13, 2026
@JeanMeche JeanMeche added the action: merge The PR is ready for merge by the caretaker label Aug 17, 2026
@thePunderWoman
thePunderWoman merged commit 48a0fd6 into angular:main Aug 18, 2026
27 checks passed
@thePunderWoman

Copy link
Copy Markdown
Contributor

This PR was merged into the repository. The changes were merged into the following branches:

@JeanMeche
JeanMeche deleted the discard-private-props branch August 25, 2026 18:19
JeanMeche added a commit to JeanMeche/angular that referenced this pull request Aug 25, 2026
This is basically a follow up to angular#70188 to address the same inconvenience introduced by `isolatedDeclarations: true`
@kaplan81

kaplan81 commented Sep 12, 2026 •

Copy link
Copy Markdown

@JeanMeche @alxhub

Hi there.

From my humble corner, if I may suggest something here, after the PR has been merged.

It seems like the access modifiers question has not been yet properly explained to the framework users, both experts and newcomers.

When Angular started this was the common understanding:

  • public: accessible from current component class, its template and other classes that may compose out of the current one.
  • protected: accessible from current component class and its template.
  • private: accessible from current component class only.

This was a clear contract back in the days. Let us also not forget that nowadays AI does benefit from clear contracts. It makes it more deterministic.

In the last couple of years, the use protected started being recommended for members that can be used in the template but that we want to hide from other classes. As a consequence many people started overusing protected everywhere.

The explanation was refined in the style guide: "A component class's public members intrinsically define a public API that's accessible via dependency injection and queries. Prefer protected access for any members that are meant to be read from the component's template". As an example:

@Component({
  ...,
  template: `<p>{{ fullName() }}</p>`,
})
export class UserProfile {
  firstName = input();
  lastName = input();
// `fullName` is not part of the component's public API, but is used in the template.
  protected fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}

And now there is this PR. This merge states that TS component classes and templates are 2 sides of the same coin and they should have access to the same things.

With this new ability, private member would be accessible from the templates but still private to outer classes.

IMHO what this suggests is that we can stop using protected, since now private is the new protected and the latter seems to be useless.

Public members are the ones that would describe the API and private ones simply the rest of them.

But as much as a technical reason for this implementation is to some extend understandable, the semantic concerns to the community are much broader than the small changes introduced here.

Many users (including myself) prefer using JS private elements since they offer class members that are genuinely private at runtime. These members would not be accessible in the template and that has its advantages.

There are several problems when using private with the new template access:

  1. By removing the encapsulation boundary you can no longer safely rename or remove a property without considering the template. If for example we rename a private property in VS code by renaming the symbol, the name of the property in the template won't be re-written. This makes refactors more dangerous.
  2. When properties are injectables we would be exposing a dependency rather than a intent:
private readonly router = inject(Router);
private readonly http = inject(HttpClient);
private readonly store = inject(MyStore);
private readonly analytics = inject(AnalyticsService);
<button (click)="router.navigate(...)">
<button (click)="store.doSomething()">
{{ http... }}
{{ analytics... }}

Now the template isn't expressing the component's UI behavior through a component API. It is reaching directly into the component's implementation dependencies.

It would be much better to do this:

#router = inject(Router);

protected goToDetails() {
  this.#router.navigate(...);
}
<button (click)="goToDetails()">
  1. It encourages templates to become too powerful. For example:
private readonly copilotKit = inject(CopilotKit);
@let service = copilotKit;

{{ service.someInternalThing() }}
  1. It creates an inconsistent notion of private and this can be really confusing for people.

IMHO, we should promote this in the documentation as good practice:

@Component({
  selector: 'app-user-profile',
  template: `
    <h1>{{ fullName() }}</h1>
    <button (click)="editProfile()">Edit profile</button>
  `,
})
export class UserProfile {
  // Private properties come first since we might need them next for public ones.
  #router = inject(Router);
  #analytics = inject(AnalyticsService);

  // Public properties - Component API
  readonly firstName = input.required<string>();
  readonly lastName = input.required<string>();

  // Protected Template API
  protected fullName = computed(
    () => `${this.firstName()} ${this.lastName()}`
  );

  // Not part of the public API, but intentionally exposed to the template.
  protected editProfile() {
    this.#router.navigate(['/profile', 'edit']);
  }
}

The use of protected would be more consistent with the current guidelines and using # in combination with it would make sense of it.

@ippeiukai

Copy link
Copy Markdown

We have a large Angular codebase and I don't feel comfortable upgrading the Angular now without rewriting all the private/protected members in our codebase. Any chance providing an opt-out from this feature while we transition to the new style?

There are several problems when using private with the new template access:

  1. By removing the encapsulation boundary you can no longer safely rename or remove a property without considering the template. If for example we rename a private property in VS code by renaming the symbol, the name of the property in the template won't be re-written. This makes refactors more dangerous.
  2. When properties are injectables we would be exposing a dependency rather than a intent:
private readonly router = inject(Router);
private readonly http = inject(HttpClient);
private readonly store = inject(MyStore);
private readonly analytics = inject(AnalyticsService);

@PowerKiKi

Copy link
Copy Markdown
Contributor

@ippeiukai, you might be interested in following #70514 that talks about the consequences of this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: merge The PR is ready for merge by the caretaker area: compiler Issues related to `ngc`, Angular's template compiler target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants