public / private / protected

Controls class member visibility during type checking only; nothing is hidden at runtime.

Signature
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

ParameterTypeDescription
$publicoptionalmodifier= publicThe default visibility. Accessible from anywhere. Writing it is optional and purely documentary, except on a constructor parameter property.
$protectedoptionalmodifierAccessible inside the declaring class and its subclasses, but not from outside. A subclass may widen a protected member to public when it redeclares it.
$privateoptionalmodifierAccessible only inside the declaring class. Cross-instance access is permitted, so a method may read other.x on another instance of the same class.
$#nameoptionalECMAScript private fieldNot 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · typescript
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
// true

Parameter 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.

Example · typescript
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

  • Be the first to comment on this lesson.