With Angular (and other frameworks as well) developers supply source code in Typescript. In the process of transpiling this code to JavaScript (which is the language the browser speaks), this code is adapted to the target.
With Angular we create a number of classes for components, directives, services, pipes etc. They differ in decorators, but at the end they are classes from the Typescript perspective.
Within classes we usually have public, private and other properties (variables, functions). If we do not specify anything, a member is public. We can emphasize this by using the keyword "public". But we can also declare a member to be private. This is achieved by using the prefixing keyword "private". This is - in Angular - often used for injected services. They shall be "private" to the class. But how private are they? It turns out, that private members of a class - defined as described above - are not really private in JS terms, but they are treated as private in the transpilation from Typescript to JavaScript.
Let us have a look at an example component:
@Component({
selector: 'app-application-toolbar-content',
standalone: true,
imports: [
...
],
templateUrl: './application-toolbar-content.component.html',
styleUrl: './application-toolbar-content.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ApplicationToolbarContentComponent {
/**
* DI
*/
public breakpointService = inject(BreakpointService);
public titleService = inject(Title);
private router = inject(Router);
private location = inject(Location);
private navInfoService = inject(NavInfoService);
// Demo constructor
constructor() {
// log this component
console.log('ApplicationToolbarContentComponent instantiated: this', this);
}
/**
* Signal providing current AppRouteDataHeader settings
* - if null, the default header is shown
*/
public headerData: Signal<AppRouteDataHeader | null> = toSignal(this.router.events.pipe(
filter(event => (event instanceof NavigationEnd)),
map(() => {
/** Get the current route from router state */
let currentRoute = this.router.routerState.root;
while (currentRoute.firstChild) {
currentRoute = currentRoute.firstChild;
}
const appRouteData: AppRouteData = currentRoute.routeConfig?.data as AppRouteData;
return appRouteData?.header ?? null;
})
), { initialValue: null });
... and some more properties
}
The component is simple:
- it injects some services
- it provides a public headerData signal.
- What is uncommon: the constructor logs the component object to the console to inspect the component object. It had been introduced for demo purposes in course of this article.
The component itself comes from one of our applications being currently refactored. Nothing special. It is used in production.
We created a production build with the demo constructor. Result, after deployment in production is: the constructor logs private and public members:
Finding: properties declared as private in Typescript are not private in JavaScript running in the Browser
The class properties
- router
- location
- navInfoService
have been declared as "private" in Typescript, but can be accessed from the outside in running JavaScript code. This is depicted by the screenshot of the browser console above.
The one or other may argue: Change the names of the private properties to ones with a trailing underscore:
- router -> _router
- location -> _location
- navInfoService -> _navInfoService
This is a common convention. Private properties have a trailing underscore, properties without trailing underscore are public.
Nice so far, but this changes nothing: The trailing underscore is just a convention - the property itself remains public.
Background: JavaScript, ECMAScript and Typescript versions
The technical term under which the JavaScript language development continues is ECMAScript®. Driving forces are Technical Committees and Task Groups. One of them is TC39: ECMAScript. TC39-TG1 works on "General ECMAScript® language".
In 2019 the feature Private-Syntax has been proposed to TC39 and made its way into the ECMAScript®-Standard. In essence it suggests to use the hash-character as prefix for real private properties. This properties
- can just be accessed from within the class
- but not from the outside
- and it can not be accessed within the class as
this[#x]
assuming a private property#x
being property of the class.
In fact it is much more complicated in details. For further details please refer to the proposal FAQ.
The feature Private-Syntax has been implemented by all major browsers and Node.JS in 2020 and 2021.
To use it in Angular, it is also necessary . that Typescript supports Private-Syntax. In Typescript it is called support for "ECMAScript Private Fields". It cam with Typescript version 3.8, releases February 20th, 2020.
And for Angular: starting with Angular version 10, at least Typescript version 3.9 was required - already including the feature "ECMAScript Private Fields".
So: since Angular version 10, released in June 2020, you can use "ECMAScript Private Fields" depending on browser capabilities. Today it is a baseline feature in all browsers and NodeJS.. So let us refactor code accordingly.
Refactoring component to make private properties private
Given all that, we refactored component code to:
@Component({
selector: 'app-application-toolbar-content',
standalone: true,
imports: [
...
],
templateUrl: './application-toolbar-content.component.html',
styleUrl: './application-toolbar-content.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ApplicationToolbarContentComponent {
/**
* DI
*/
breakpointService = inject(BreakpointService);
titleService = inject(Title);
#router = inject(Router);
#location = inject(Location);
#navInfoService = inject(NavInfoService);
// Demo constructor
constructor() {
// log this component
console.log('ApplicationToolbarContentComponent instantiated: this', this);
}
/**
* Signal providing current AppRouteDataHeader settings
* - if null, the default header is shown
*/
public headerData: Signal<AppRouteDataHeader | null> = toSignal(this.#router.events.pipe(
filter(event => (event instanceof NavigationEnd)),
map(() => {
/** Get the current route from router state */
let currentRoute = this.#router.routerState.root;
while (currentRoute.firstChild) {
currentRoute = currentRoute.firstChild;
}
const appRouteData: AppRouteData = currentRoute.routeConfig?.data as AppRouteData;
return appRouteData?.header ?? null;
})
), { initialValue: null });
... and some more properties
}
In this case we use JS private properties and when running this in production we see - as expected:
Conclusion
It means the private properties
- router
- location
- navInfoService
are not exposed to the outside any more. This is, what we aimed to achieve.
We think, that "ECMAScript Private Fields" should be used in similar scenarios more frequently. And it may help a lot in particular scenarios, where application details shall be hidden.
Thanks a lot for reading.