Public Private and Protected in TypeScript

TypeScript access modifiers control which parts of your code can see and use class members. Learn the differences between public, private, and protected, and when to use each.

8 min read

TypeScript gives you three keywords to control who can see and use a class member: public, protected, and private. All three are compile-time only. They disappear from the emitted JavaScript entirely.

Think of them as documentation that the compiler enforces. They tell your team (and TypeScript) which parts of a class are safe to use from the outside and which are internal details.

Public: The Default

Every class member is public unless you say otherwise. A public member can be accessed from anywhere: inside the class, from a subclass, and from any external code that holds a reference to the instance.

typescripttypescript
class Greeter {
  greet() {
    console.log("Hello!");
  }
}
 
const g = new Greeter();
g.greet();

This prints Hello! because greet has no access modifier, so it defaults to public and any caller outside the class can invoke it.

texttext
Hello!

You can write public explicitly for style reasons, but it changes nothing. Both versions behave identically at compile time and runtime.

Private: Only the Declaring Class

A private member is only visible inside the class that declares it. Subclasses cannot see it, and external code cannot see it:

typescripttypescript
class Base {
  private secret = "sshh";
 
  revealSecret() {
    return this.secret;
  }
}
 
const b = new Base();
console.log(b.revealSecret());

The revealSecret method works because it is defined inside Base and has access to all private members. Now let us see what happens when external code tries to access the private field directly:

typescripttypescript
console.log(b.secret);

The call to this internal method works because the method is inside the class. Direct external access is blocked by the compiler, as shown below.

texttext
Property 'secret' is private and only accessible within class 'Base'.

A subclass cannot access the private member either. Even though a derived class inherits from Base, private members belong to the declaring class alone.

Cross-Instance Private Access

TypeScript allows two instances of the same class to access each other's private members:

typescripttypescript
class Wallet {
  private balance = 0;
  constructor(initial: number) {
    this.balance = initial;
  }
  sameBalance(other: Wallet): boolean {
    return other.balance === this.balance;
  }
}

The sameBalance method accepts another Wallet and reads its private balance field. TypeScript allows this because both objects are instances of the same class. Now let us create two wallets and compare them:

typescripttypescript
const a = new Wallet(100);
const b = new Wallet(100);
console.log(a.sameBalance(b));

This prints true, since both wallets hold a balance of 100 and the method is allowed to read the private field on the other instance.

texttext
true

Method sameBalance reads other.balance even though balance is private. This works because other is also a Wallet instance. Java, C#, and C++ allow this same pattern.

TypeScript Private vs JavaScript # Fields

TypeScript's private is a type-checking illusion. At runtime, private members are regular JavaScript properties accessible through bracket notation.

If you need real runtime privacy, use JavaScript's # private fields instead. They are enforced by the JavaScript engine and cannot be accessed outside the class, even with bracket notation.

FeatureTypeScript privateJavaScript # private
Checked atCompile time onlyCompile time and runtime
Bracket notation bypassYesNo
Accessible from plain JSYesNo
Subclass visibilityNoNo
Use forDeveloper communicationSecurity / hard encapsulation

Protected: The Class and Its Subclasses

A protected member is visible to the declaring class and all subclasses, but hidden from external code:

typescripttypescript
class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "world";
  }
}
class FriendlyGreeter extends Greeter {
  protected getName() {
    return "friend";
  }
}

FriendlyGreeter overrides the protected getName method. Let us create an instance and see which version of getName actually runs when greet calls it internally:

typescripttypescript
const g = new FriendlyGreeter();
g.greet();
g.getName();

When you run this code, the overridden getName method from FriendlyGreeter is called inside the inherited greet method. The output confirms that the subclass provided its own implementation:

texttext
Hello, friend

The greet method works because it calls getName internally through the class instance. However, external code outside the class hierarchy cannot access the protected method. TypeScript blocks the following call with a clear error:

texttext
Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.

The subclass FriendlyGreeter overrides getName because it is protected. External code cannot call getName at all.

Cross-Hierarchy Protected Access

TypeScript does not let you access a protected member through a reference to a different subclass branch. Even though two classes share a common base, a protected member on one branch cannot be reached from the other branch.

Making Protected Members Public

A derived class can increase the visibility of an inherited protected member by re-declaring it without the protected keyword. The subclass is choosing to expose a member that the base class kept restricted.

Be intentional about this: if you want to keep it protected, write the keyword explicitly on the override.

Quick Reference Table

ModifierClass itselfSubclassesExternal codeAt runtime
public (default)YesYesYesRegular property
protectedYesYesNoRegular property
privateYesNoNoRegular property
# (JS private)YesNoNoTruly private

Access Modifiers on Static Members

Static members can also have access modifiers. A private static field is only visible inside the class, even though it belongs to the class itself rather than an instance:

typescripttypescript
class Config {
  private static apiKey = "sk-abc123";
 
  static getApiKey(): string {
    return this.apiKey;
  }
}
 
console.log(Config.getApiKey());
console.log(Config.apiKey);

The direct access to Config.apiKey produces a compile error. The static getter method can reach the private field because it lives inside the same class.

Common Mistakes

Assuming private means secure. It does not. TypeScript's private is for developer communication and compile-time checks. Anyone can still access the property at runtime through bracket notation or plain JavaScript. Use # fields for runtime encapsulation.

Forgetting to repeat the modifier in a subclass. If you override a protected member and do not write the keyword again, it becomes public because public is the default.

Using private when protected is enough. If you are writing a base class that subclasses will extend, use protected for members that subclasses need. Reserve private for implementation details that should never leak, even to subclasses.

For more on class field immutability, see readonly class properties in TypeScript. For how a class promises to satisfy a contract, see implements keyword in TypeScript.

Rune AI

Rune AI

Key Insights

  • public is the default. Members are accessible from anywhere. You can write it explicitly for clarity.
  • protected members are visible to the declaring class and all subclasses, but not to external code.
  • private members are only visible to the declaring class itself, not even to subclasses.
  • All TypeScript access modifiers are compile-time only. Use JavaScript # fields for runtime privacy.
  • TypeScript allows cross-instance private access. Two instances of the same class can see each other's private members.
RunePowered by Rune AI

Frequently Asked Questions

Are TypeScript private members truly private at runtime?

No. TypeScript's private is compile-time only. At runtime, private fields are regular JavaScript properties and can be accessed with bracket notation or in plain JavaScript code. For true runtime privacy, use JavaScript's # private fields.

What is the difference between TypeScript private and JavaScript # private fields?

TypeScript private is erased at compile time and only checked during type checking. JavaScript # private fields (hard private) are enforced at runtime and cannot be accessed outside the class, even with bracket notation. Use # when you need true encapsulation at runtime.

Can I access a protected member from a different instance of the same derived class?

You can access a protected member of your own class instance or the same subclass instance. You cannot access a protected member through a reference typed as a different subclass, even if both share the same base class.

Conclusion

TypeScript access modifiers let you communicate intent: public means anyone can use it, protected means only subclasses can, and private means only the declaring class can. All three are erased at compile time. If you need runtime enforcement, use JavaScript's # private fields. The modifiers are about helping your team use the class correctly, not about security.