ProgrammingFrontend Developer

How do access modifiers (public, private, protected) work in TypeScript? What are the nuances when using them with inheritance and when transpiling code to JavaScript?

Pass interviews with Hintsage AI assistant

Answer

In TypeScript, access modifiers are used to restrict the visibility of properties and methods within classes:

  • public — the property or method is accessible everywhere (default).
  • private — accessible only within the class where it is declared.
  • protected — accessible within the class and its subclasses.
class Animal { public name: string; private age: number; protected kind: string; constructor(name: string, age: number, kind: string) { this.name = name; this.age = age; this.kind = kind; } } class Dog extends Animal { bark() { console.log(this.kind); // OK: protected // console.log(this.age); // Error: private not visible in subclass } } const dog = new Dog('Sharik', 5, 'mammal'); console.log(dog.name); // OK // console.log(dog.kind); // Error: protected // console.log(dog.age); // Error: private

Nuances:

  • In the compiled JavaScript, private and protected modifiers are only enforced at compile time; there is no physical isolation at runtime! The JS code will contain all properties (they can, for example, be accessed through object traversal).
  • In new versions (from TypeScript 3.8), a syntax for real private fields has been introduced: #field, but this is already in the JS specification and compiles differently.

Trick Question

Question: Can you access a private property of a TypeScript class after compilation to JavaScript?

Answer: Yes, because private (and protected) are checks from TypeScript at compile time, after compilation to ES5 or ES6, privacy is not maintained, properties remain in the object and can be accessed by property name (for example, via object['privateProp']).

// JS code after compilation function Animal(name, age, kind) { this.name = name; this.age = age; this.kind = kind; } var dog = new Animal('Sharik', 5, 'mammal'); console.log(dog['age']); // 5 — no access only at TS level!

Examples of real errors due to ignorance of the nuances of this topic.


Story

In a large project, a developer relied on the inaccessibility of private fields at runtime. As a result, when passing an object via serialization (JSON.stringify) to logs, confidential data accidentally leaked, as TS typing did not protect against actual access to fields.


Story

A mechanism for "extending" class instances through dynamically adding properties was implemented in the project. Dynamically added properties overwrote private names and were accidentally modified by external code. The error was discovered only in production, and privacy was not ensured.


Story

When migrating from JavaScript to TypeScript, the team started using protected, believing it protected against the usage of fields outside of child classes. However, a programmer mistakenly overwrote a protected field at runtime via Object.assign, leading to a hard-to-trace bug. This became possible because run-time encapsulation was absent.