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.
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.
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.
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:
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:
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.
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:
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:
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.
trueMethod 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.
| Feature | TypeScript private | JavaScript # private |
|---|---|---|
| Checked at | Compile time only | Compile time and runtime |
| Bracket notation bypass | Yes | No |
| Accessible from plain JS | Yes | No |
| Subclass visibility | No | No |
| Use for | Developer communication | Security / 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:
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:
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:
Hello, friendThe 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:
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
| Modifier | Class itself | Subclasses | External code | At runtime |
|---|---|---|---|---|
| public (default) | Yes | Yes | Yes | Regular property |
| protected | Yes | Yes | No | Regular property |
| private | Yes | No | No | Regular property |
| # (JS private) | Yes | No | No | Truly 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:
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
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.
Frequently Asked Questions
Are TypeScript private members truly private at runtime?
What is the difference between TypeScript private and JavaScript # private fields?
Can I access a protected member from a different instance of the same derived 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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.