TypeScript has three access modifiers for class members: public (the default), protected and private. They control where a member can be used, and the compiler reports any access from the wrong place.
The last line is a compile error, but look at what it printed when run anyway: 123-45-6789. That is the most important fact on this page, and the private section explains it.
What Each Modifier Allows
| Modifier | Inside the class | In a subclass | From outside | Enforced at runtime |
|---|---|---|---|---|
public (default) | yes | yes | yes | nothing to enforce |
protected | yes | yes | no | no |
private | yes | no | no | no |
#name (JavaScript) | yes | no | no | yes |
readonly | read, and write in the constructor | read | read | no |
The modifiers work on fields, methods, getters and setters, constructors and parameter properties (constructor(private id: string)). Writing public is optional, and many codebases leave it out.
private Is a Compile-Time Check
TypeScript erases private along with the rest of the types. The compiled class has an ordinary property, so anything that does not go through the type checker sees it: plain JavaScript callers, JSON.stringify, Object.keys, and even TypeScript's own bracket notation, which is allowed as a deliberate escape hatch.
That is fine for what private is for: telling other developers (and your editor) that a member is an implementation detail. It is not a security boundary, and a private field will end up in logs and JSON responses unless you strip it yourself.
#private Fields: Enforced at Runtime
JavaScript has its own private fields, written with #. The engine enforces them: outside the class, obj.#field is not even valid syntax, and the field does not show up in Object.keys, JSON.stringify or console.log.
Writing s.#token outside the class is error TS18013 (Property '#token' is not accessible outside class 'Session' because it has a private identifier), and unlike the private case, there is no way around it at runtime either. See private fields in JavaScript for the runtime rules.
private vs #private: Which to Use
private x | #x | |
|---|---|---|
| Checked by | the compiler | the JavaScript engine |
Visible to Object.keys / JSON.stringify | yes | no |
Bracket access obj["x"] | allowed | not possible |
Subclass can declare its own x | no, it conflicts | yes, each class has its own #x |
Copied by a spread { ...obj } | yes | no |
| Syntax on methods | private helper() | #helper() |
Use #private when the data must stay private at runtime (tokens, internal state a library's users must not touch) or when you want JSON.stringify to leave it out. Use private when the goal is only a clean public API, when a framework needs to read the field reflectively, or to match a codebase's existing style. Do not combine them: private #x is error TS18010 (An accessibility modifier cannot be used with a private identifier).
protected and Subclasses
A protected member is available inside the class and in any class that extends it, but not on instances from outside.
One rule surprises people. Inside Polygon, you can read sides on this or on another Polygon, but not on a plain Shape passed in as a parameter: other.sides where other: Shape is error TS2446 (Property 'sides' is protected and only accessible through an instance of class 'Polygon'. This is an instance of class 'Shape'.). A subclass may only reach protected members of objects that belong to its own branch of the hierarchy.
A subclass can make a protected member public by redeclaring it, but it cannot make a public member protected or private. Redeclare it with an initializer (public override sides = 6) or as a type only (declare public sides: number). A bare public sides: number; is rejected with TS2564 and TS2612, because under modern class fields it would reset the inherited value to undefined after super() returns.
readonly
readonly blocks reassignment after construction. The field can be set in its declaration or in the constructor; any later assignment is error TS2540.
Two limits show up here. readonly is shallow: the array cannot be replaced, but its contents can change (type it readonly string[] to block push). And like private, it disappears at runtime, so the assignment the compiler rejected still ran. For a value that must not change at runtime, use Object.freeze or a getter with no setter. The readonly page covers Readonly<T> and readonly arrays.
readonly combines with the access modifiers: private readonly cache = new Map<string, number>() is a common pattern for a field that is internal and never reassigned.
Common Mistakes
- Treating
privateas security. It is gone at runtime. Use#fieldfor data that must stay hidden, and never send an object with secrets straight toJSON.stringify. - Writing
publiceverywhere. It is the default; adding it changes nothing. - Using
protectedfor everything "internal". If no subclass needs it,privatekeeps the surface smaller. - Expecting
readonlyto freeze nested data. It only stops reassignment of the property itself.
Frequently Asked Questions
What are the access modifiers in TypeScript?
public (the default: accessible everywhere), protected (inside the class and its subclasses) and private (inside the class only). readonly is a separate modifier that blocks reassignment after the constructor and combines with any of the three.
What is the difference between private and #private in TypeScript?
private is checked by the compiler only; the emitted JavaScript has an ordinary property, so obj["secret"], JSON.stringify and Object.keys still see it. #secret is a JavaScript private field: the runtime enforces it, and code outside the class cannot read it at all.
Is private in TypeScript really private?
Only at compile time. After compilation the field is a normal property that any JavaScript code can read. TypeScript even allows bracket access (obj["field"]) to private members as an escape hatch. Use #field when privacy has to hold at runtime.
What is the difference between protected and private in TypeScript?
A private member is visible only inside the class that declares it. A protected member is also visible inside subclasses. Neither can be accessed on an instance from outside the class hierarchy.
Can readonly properties be changed in TypeScript?
A readonly property can be assigned in its declaration or in the constructor, and nowhere else (TS2540 otherwise). It is shallow: a readonly tags: string[] cannot be replaced, but tags.push() still works. Use readonly string[] to block that too.