Skip to content

Provide an option to control template access to TypeScript private members #71066

Description

@ippeiukai

Which @angular/* package(s) are relevant/related to the feature request?

compiler

Description

Follow-up to the discussion in #70514, filed as agreed there.

Since #70188, templates can access members declared private on the component class. The motivation given in #70188 is specific: under isolatedDeclarations: true, template-facing members declared protected require explicit type annotations, and allowing private removes that requirement. The behaviour change, however, applies to every Angular component, whether or not isolatedDeclarations is used.

For existing codebases this lands implicitly. On upgrade, every private member already written becomes reachable from the template at once. Nothing breaks at runtime, but the boundary a declaration expresses and the boundary the compiler enforces no longer match, and they stay mismatched until the codebase has been migrated to whatever the new convention is. There is no diagnostic to surface this, and the style guide still recommends protected for template-facing members.

JS #private elements remain inaccessible from templates, but only because of a runtime limitation. If class and template are meant to be one cohesive unit, that argument covers #private too, so it is not clear this is a boundary teams should build on. A related question raised in #70514 is whether, with the TypeScript 7 compiler, .ngtypecheck.ts files are still generated in the same way, which also bears on how stable the current behaviour is.

As it stands, upgrading to 22.2 and committing to a new convention for member visibility cannot be separated. An option would let them happen in that order, on the team's own schedule.

Proposed solution

An angularCompilerOptions flag that controls whether templates may access private members. Following @PapaNappa's suggestion in #70514:

"angularCompilerOptions": {
  // "auto" (default) | "allow" | "disallow"
  "privateAccessFromTemplates": "auto"
}
  • auto derives the behaviour from isolatedDeclarations: allowed when it is enabled, disallowed when it is not. This keeps the benefit for the codebases the change was aimed at, and leaves the previous semantics in place elsewhere.
  • allow and disallow let a team opt in or out explicitly, independently of isolatedDeclarations.

@PowerKiKi suggested a simpler variant: no new flag, and template access to private members permitted only when isolatedDeclarations is enabled. That would also address the problem, at the cost of not being separately controllable.

A lighter alternative would be an opt-in diagnostic that reports template access to private members, leaving the behaviour unchanged. That is enough to keep a convention enforceable, though it does not restore the previous semantics.

Alternatives considered

Migrate template-facing members to private and hidden members to #private. This is the smallest change, and mostly automatable. It depends on #private remaining inaccessible from templates, which is not documented. It also has known friction: #private members cannot be reached from tests the way private ones can via component['member'], and private members used only in templates are reported as unused (TS6133).

Move anything the template must not reach off the component, into an injected ViewModel, Facade or Store. This keeps the boundary regardless of how member visibility is treated, but it is a design change in every affected component rather than a rename, and it is disproportionate for small components.

Adopt the new behaviour as is. Viable for new code. For an existing codebase it means accepting a mismatch between declared and enforced visibility across the whole codebase for the duration of the migration, with no tooling to track progress.

An external lint rule. Angular ESLint could in principle flag template access to private members. This would live outside the compiler and could drift from its semantics, and it does not help teams that do not use it.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions