Classes themselves are JavaScript (fields, #private, static blocks). TypeScript layers type-time features on top:
public/protected/privateare checked only at compile time.privateis "soft": bracket accessa["secret"]is allowed by the checker, and at runtime the property is an ordinary own property (visible inObject.keysandJSON.stringify).protectedmeans "this class and subclasses". Both still allow cross-instance access (other.secretinside the class).#nameis ECMAScript's hard private field. It is enforced at runtime (a syntax error outside the class), is invisible toObject.keys, and TypeScript emits it as-is for ES2022+, or as aWeakMapfor older targets.readonlyis compile-time only; assignment outside the constructor is TS2540.- Parameter properties (
constructor(public id: string)) declare a field and assign it. This is one of the few class features that emits code. override(TS 4.3) asserts the base class has that member;noImplicitOverridemakes it mandatory.
Two compatibility rules matter: classes with private or # members are compared nominally for those members. Two unrelated classes with an identically named private member are not assignable to each other, and a class with a private member is not assignable to an object type with the same public-looking property.
Choose # when privacy must hold at runtime (libraries, secrets in objects that get serialised or inspected). Choose private when you want test access or have to support targets where WeakMap emit is unwanted. With erasableSyntaxOnly, parameter properties are rejected.