public / private / protected
Controls class member visibility during type checking only; nothing is hidden at runtime.
class C {
public field: Type; // default; visible everywhere
protected field2: Type; // visible in C and its subclasses
private field3: Type; // visible in C only (soft)
#hard: Type; // ECMAScript private field (hard)
private constructor() {} // class cannot be instantiated or extended
protected constructor() {} // cannot be instantiated, can be extended
constructor(private readonly dep: Dep) {} // parameter property
}- Added in TypeScript 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$publicoptional | modifier= public | The default visibility. Accessible from anywhere. Writing it is optional and purely documentary, except on a constructor parameter property. |
$protectedoptional | modifier | Accessible inside the declaring class and its subclasses, but not from outside. A subclass may widen a protected member to public when it redeclares it. |
$privateoptional | modifier | Accessible only inside the declaring class. Cross-instance access is permitted, so a method may read other.x on another instance of the same class. |
$#nameoptional | ECMAScript private field | Not a TypeScript modifier but the JavaScript alternative. Enforced by the language at runtime, with no bracket-notation escape hatch. Downlevelled to WeakMaps when targeting ES2021 or lower. |
Errors
Errors at compile time with 'Property is private and only accessible within class X' or the protected equivalent. Never throws at runtime, since the modifiers are erased — but accessing an undeclared #private field is a runtime TypeError.
Changelog
- PHP 2.0Constructors may be marked private or protected. A private constructor prevents both instantiation and extension; a protected one prevents instantiation but still allows subclassing.
- PHP 3.8ECMAScript #private fields were supported, giving a hard-private alternative that survives compilation.
- PHP 5.8erasableSyntaxOnly rejects parameter properties such as `constructor(private x: number)`, because they emit an assignment rather than being pure type syntax.
These modifiers are type-checker instructions. They are erased on emit, so a private field is an ordinary property in the compiled JavaScript — visible in Object.keys, readable from any untyped code, and serialised by JSON.stringify.
Soft private versus hard private
The handbook calls TypeScript's private soft private. Beyond runtime access, it leaves a deliberate escape hatch even during type checking: s.secretKey is an error but s["secretKey"] is allowed, which exists so unit tests can reach internals. JavaScript's #field is hard private — enforced by the language, absent from Object.keys, and with no bracket-notation bypass.
Which to use
Use private for ordinary encapsulation inside your own codebase; it is readable, works with mocking and debugging, and the softness rarely matters. Use # when the secrecy is a real requirement — credentials, tokens, anything crossing a trust boundary or shipped to consumers who might poke at it. Use protected for members that are implementation details of a class hierarchy.
What trips people up
TypeScript allows cross-instance private access, so other.x compiles inside the class that declares x — a surprise if you expect Ruby-style rules. And unlike everything else here, parameter properties are not erasable, so they are rejected under erasableSyntaxOnly.
Example
When to use it
- Hide a cache or connection handle so callers cannot reach into a service's internals.
- Expose a hook to subclasses with protected while keeping it off the public API.
- Enforce a singleton or a factory function with a private constructor.
- Store a credential in a # field so it never appears in logs or JSON output.
Frequently asked questions
Is private in TypeScript actually private at runtime?
No — private is erased at compile time, so the member is a normal property in the emitted JavaScript that any untyped code can read and that JSON.stringify will serialise. Even during type checking there is a documented escape hatch, since bracket notation such as obj["secret"] is permitted.
What is the difference between private and # in TypeScript?
`private` is a TypeScript modifier checked only by the compiler and removed on emit, while `#field` is an ECMAScript private field enforced by the JavaScript runtime itself. The # form has no bracket-notation bypass, does not appear in Object.keys or JSON output, and is downlevelled to WeakMaps when targeting ES2021 or earlier.
What is the difference between private and protected?
`private` restricts access to the declaring class only, whereas `protected` also permits access from subclasses. Both allow cross-instance access within the permitted scope, and both are erased at compile time with no runtime enforcement.
More examples
protected members and cross-instance private access
greet() is public and calls the protected getName(), which the subclass overrides, so both lines print "special" — protected members participate in polymorphism normally. The final class shows cross-instance private access: reading other.x compiles because the code sits inside the class that declares x, matching Java and C# rather than Ruby.
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "base";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
console.log("Howdy, " + this.getName()); // ok inside a subclass
}
protected getName() {
return "special";
}
}
const g = new SpecialGreeter();
g.greet();
g.howdy();
// g.getName(); // Property 'getName' is protected and only accessible
// // within class 'Greeter' and its subclasses.
class A {
private x = 10;
sameAs(other: A) { return other.x === this.x; } // no error: cross-instance
}
console.log(new A().sameAs(new A()));
// Hello, special
// Howdy, special
// trueParameter properties and a private constructor
The private constructor makes new Config(...) a compile error outside the class, which is how the singleton is enforced, and it also blocks subclassing — use protected instead if extension should stay possible. The parameter property on the constructor declares this.values and assigns it in one line; the second log confirms both calls return the same cached instance.
class Config {
private static instance: Config | null = null;
// Parameter property: declares and assigns the field in one step.
// Rejected under --erasableSyntaxOnly, since it emits an assignment.
private constructor(private readonly values: Record<string, string>) {}
static get(): Config {
if (!Config.instance) {
Config.instance = new Config({ env: "prod" });
}
return Config.instance;
}
read(key: string): string | undefined {
return this.values[key];
}
}
// new Config({}); // Constructor of class 'Config' is private.
console.log(Config.get().read("env"));
console.log(Config.get() === Config.get());
// prod
// true
Discussion