| |

TypeScript 20 ๐Ÿ”ท Access Modifiers โ€” public, private, protected, readonly

Access modifiers control who can read and write a class member. TypeScript adds four: public, private, protected, and readonly. They’re compile-time checks โ€” the compiler refuses to compile code that violates the rules. But they’re not runtime enforcement: private and protected can be bypassed with as any, and readonly is only enforced at the type level. For real runtime privacy, you need JavaScript’s #private fields. Knowing the difference between compile-time and runtime enforcement is the key to using modifiers correctly.

Key point: Access modifiers are about intent and compile-time safety, not security. public is the default โ€” accessible anywhere. private restricts to the class. protected restricts to the class and its subclasses. readonly prevents reassignment after construction. #private is the runtime-enforced version of private. Pick the right modifier for the intent โ€” and don’t rely on them for security.


The four modifiers

Each modifier controls a different kind of access.

ModifierAccessible fromAssignable after construction
publicAnywhereโœ…
privateSame class onlyโœ… (within class)
protectedSame class + subclassesโœ… (within class hierarchy)
readonlySame as the other modifierโŒ

public is the default. If you write no modifier, the member is public.

class User {
  name: string = '';               // public by default
  public id: number = 0;           // explicit public โ€” same thing
}

private restricts access to the class body.

class User {
  private secret = 'x';

  reveal(): string {
    return this.secret;            // โœ… inside the class
  }
}

new User().secret;                 // โŒ private

protected restricts to the class and any subclass.

class Animal {
  protected name = '';

  getName(): string {
    return this.name;
  }
}

class Dog extends Animal {
  bark(): string {
    return `${this.name} barks`;   // โœ… subclass access
  }
}

new Animal().name;                 // โŒ protected

readonly makes the member assignable only in the constructor.

class User {
  readonly id: number;

  constructor(id: number) {
    this.id = id;                  // โœ… assignable here
  }

  changeId(): void {
    // this.id = 2;                // โŒ after construction
  }
}

readonly composes with access modifiers:

class Config {
  private readonly secret = 'x';
  public readonly name = 'config';
  protected readonly version = 1;
}

The decision: public unless there’s a reason to hide. Private for internal state. Protected for subclass access. Readonly for anything that shouldn’t change after construction.

Why four modifiers and not one: They express different intents. A property that’s private and readonly is different from one that’s public and mutable. The modifiers document the class’s design โ€” who can do what, and what can change. That documentation is checked by the compiler, so it can’t drift from reality.


public โ€” the default

public members are accessible from anywhere.

class User {
  public name = '';
  id = 0;                          // public by default
}

const u = new User();
u.name = 'Alice';                  // โœ…
u.id = 1;                          // โœ…

When to use public explicitly:

  • To make the intent clear when mixed with other modifiers
  • To document that a member is deliberately part of the API

When to omit:

  • When it’s the only modifier on the member
  • When the class is small and the API is obvious

public doesn’t affect runtime behavior. It’s purely a type-level declaration โ€” no runtime overhead, no behavior change.

class User {
  name = 'Alice';
}

Compiles to roughly:

class User {
  constructor() {
    this.name = 'Alice';
  }
}

No public in the output โ€” it was erased.

Why public is the default: Most members are public โ€” that’s the common case. Making it explicit everywhere adds noise. The default is public, and modifiers are used to restrict access where needed.


private โ€” class-only access

private members are accessible only within the class body.

class Counter {
  private count = 0;

  increment(): void {
    this.count++;                  // โœ…
  }

  value(): number {
    return this.count;             // โœ…
  }
}

const c = new Counter();
c.count;                           // โŒ private
c.increment();                     // โœ…
c.value();                         // โœ…

Not accessible from subclasses:

class Base {
  private secret = 'x';
}

class Derived extends Base {
  reveal(): string {
    // return this.secret;         // โŒ not accessible
  }
}

Subclasses don’t get access to private members. Use protected for that.

Private methods:

class User {
  save(): void {
    if (this.validate()) {
      // ...
    }
  }

  private validate(): boolean {
    return true;
  }
}

new User().validate();             // โŒ private

Private static members:

class Singleton {
  private static instance: Singleton | null = null;

  private constructor() {}

  static get(): Singleton {
    return this.instance ??= new Singleton();
  }
}

private on a constructor prevents new outside the class โ€” used for singletons.

Why private matters: It hides internal state and implementation details. Callers see only the public API. Refactoring internals doesn’t break external code. It’s how you keep the public surface small and the internals flexible.

TypeScript’s private is not security: The keyword is erased at runtime. (obj as any).secret bypasses it.

class User {
  private secret = 'x';
}

const u = new User();
(u as any).secret;                 // โœ… compiles โ€” no runtime check

If you need runtime-enforced privacy, use #private.

Why private isn’t runtime-enforced: It’s a type-level convention, not a runtime mechanism. JavaScript didn’t have runtime private fields when TypeScript was designed. TypeScript added private as a compile-time check โ€” the runtime sees a normal property. It works for preventing accidental access in TypeScript code; it doesn’t protect against code that bypasses the types.


protected โ€” class and subclasses

protected members are accessible from the class and any subclass, but not from outside.

class Animal {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

  describe(): string {
    return `Animal: ${this.name}`;
  }
}

class Dog extends Animal {
  bark(): string {
    return `${this.name} barks`;   // โœ… subclass access
  }
}

const d = new Dog('Rex');
d.bark();                          // โœ…
d.name;                            // โŒ protected

When to use protected:

  • Fields shared with subclasses
  • Methods subclasses should call or override
  • Template methods โ€” public method that calls protected methods
abstract class Shape {
  abstract area(): number;

  protected log(msg: string): void {
    console.log(`[Shape] ${msg}`);
  }

  describe(): string {
    this.log('describing');
    return `Area: ${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(private r: number) { super(); }

  override area(): number {
    this.log('computing');         // โœ… protected access
    return Math.PI * this.r ** 2;
  }
}

describe is public โ€” callers use it. log is protected โ€” subclasses can call it. area is abstract โ€” subclasses must implement it.

Protected constructor:

class AbstractBase {
  protected constructor() {}
}

class Concrete extends AbstractBase {}

new AbstractBase();                // โŒ protected constructor
new Concrete();                    // โœ…

A protected constructor prevents direct instantiation but allows subclassing.

Protected is still compile-time only: Like private, it’s erased at runtime and can be bypassed with as any.

Why protected exists: Subclasses often need to interact with parent state or call parent helpers. private would prevent that. protected is the middle ground โ€” accessible to the class hierarchy, hidden from the outside. It’s the standard mechanism for template-method and inheritance patterns.


readonly โ€” assignment only in the constructor

readonly prevents reassignment after the constructor.

class User {
  readonly id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id;                  // โœ…
    this.name = name;
  }

  rename(newName: string): void {
    this.name = newName;           // โœ…
    // this.id = 999;              // โŒ
  }
}

readonly can be combined with access modifiers:

class Config {
  public readonly name: string;
  private readonly secret: string;
  protected readonly version: number;

  constructor(name: string, secret: string) {
    this.name = name;
    this.secret = secret;
    this.version = 1;
  }
}

readonly on constructor parameters:

class User {
  constructor(
    public readonly id: number,
    public name: string
  ) {}
}

id can’t be reassigned. name can.

readonly is shallow:

class Store {
  readonly items: string[] = [];
}

const s = new Store();
s.items = ['x'];                   // โŒ can't reassign
s.items.push('y');                 // โœ… can mutate contents

readonly prevents reassigning the property. It doesn’t freeze the object.

For deep readonly: Use readonly T[], Readonly<T>, or a recursive DeepReadonly<T> type.

class Store {
  readonly items: readonly string[] = [];
}

const s = new Store();
s.items.push('y');                 // โŒ can't mutate

readonly string[] makes the array itself immutable.

Readonly arrays in parameters:

function sum(nums: readonly number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}

The function promises not to mutate the array โ€” the caller’s array is safe.

Why readonly matters: Immutable state is easier to reason about. A readonly property can’t be accidentally reassigned, which prevents bugs where two parts of the code fight over a shared value. For IDs, creation timestamps, and configuration, readonly is the right default.


Modifiers compose

Access modifiers combine with readonly โ€” order doesn’t matter.

class Config {
  private readonly secret: string = 'x';
  protected readonly version: number = 1;
  public readonly name: string = 'config';

  // Readonly is enforced within the modifier's scope:
  // private readonly  โ†’ only the class can read; no one can reassign
  // protected readonly โ†’ class + subclasses can read; no one can reassign
  // public readonly    โ†’ anyone can read; no one can reassign
}

Access modifier + readonly table:

CombinationRead accessWrite access
publicAnyoneAnyone
public readonlyAnyoneConstructor only
privateClass onlyClass only
private readonlyClass onlyConstructor only
protectedClass + subclassesClass + subclasses
protected readonlyClass + subclassesConstructor only

Order of keywords: Both orders work, but the convention is access readonly:

private readonly secret = 'x';     // โœ… conventional
readonly private secret = 'x';     // โœ… works, unconventional

Combining with static:

class User {
  static readonly MAX = 100;
  private static count = 0;
  protected static baseUrl = '/api';
}

The same modifiers apply to static members.

Combining with #private:

class Secret {
  readonly #value: string;

  constructor(v: string) {
    this.#value = v;
  }

  get() { return this.#value; }
}

readonly composes with #private โ€” the property is truly private at runtime and immutable after construction.

Why composition matters: Real classes need combinations. A private immutable config, a protected mutable state, a public readonly ID. Each combination expresses a specific design intent. TypeScript lets you express all of them.


Compile-time vs runtime enforcement

This is the most important distinction in the chapter.

TypeScript’s modifiers are compile-time only:

  • public, private, protected, readonly โ€” all erased
  • The compiler checks access rules
  • The runtime sees normal properties
  • as any bypasses all checks

JavaScript’s #private is runtime-enforced:

  • The # prefix is a JavaScript feature
  • The runtime refuses access outside the class
  • as any doesn’t bypass it
  • True encapsulation

Comparison:

Featureprivate#private
Enforced at compile timeโœ…โœ…
Enforced at runtimeโŒโœ…
Accessible via as anyโœ…โŒ
Works with readonlyโœ…โœ…
Erased in outputโœ…โŒ

Example โ€” private bypassed:

class User {
  private secret = 'x';
}

const u = new User();
console.log((u as any).secret);    // 'x' โ€” no runtime error

Example โ€” #private not bypassed:

class User {
  #secret = 'x';
}

const u = new User();
console.log((u as any).#secret);   // โŒ SyntaxError โ€” can't even write it

When to use which:

NeedModifier
Convention โ€” “don’t touch this”private
Subclass accessprotected
Real encapsulation#private
Immutable after constructionreadonly

Why the distinction matters: A library author who wants real privacy uses #private. A team using TypeScript as a discipline uses private. Both are valid โ€” they just enforce at different levels. The runtime bypass is a real difference when you’re building libraries that others import.

Why TypeScript added private before #: When TypeScript was designed, JavaScript had no private fields. private was a compile-time convention that gave developers the syntax and checking they wanted. When JavaScript added # fields in ES2022, TypeScript supported them natively. Both exist because they serve different needs โ€” convention versus real privacy.


private in practice

Where private helps most.

Hiding implementation details:

class UserRepository {
  private cache = new Map<number, User>();

  async get(id: number): Promise<User> {
    if (this.cache.has(id)) return this.cache.get(id)!;
    const user = await this.fetch(id);
    this.cache.set(id, user);
    return user;
  }

  private async fetch(id: number): Promise<User> {
    // implementation detail
    return fetch(`/users/${id}`).then(r => r.json());
  }
}

Callers see get. The cache and fetch are hidden. Refactoring internals doesn’t break the API.

Enforcing invariants:

class BankAccount {
  private balance = 0;

  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Invalid amount');
    this.balance += amount;
  }

  withdraw(amount: number): void {
    if (amount > this.balance) throw new Error('Insufficient funds');
    this.balance -= amount;
  }

  getBalance(): number {
    return this.balance;
  }
}

balance can only be changed through methods that enforce rules. Direct mutation would break the invariant.

Private methods as helpers:

class Formatter {
  format(value: string): string {
    return this.trim(this.capitalize(value));
  }

  private trim(s: string): string { return s.trim(); }
  private capitalize(s: string): string {
    return s.charAt(0).toUpperCase() + s.slice(1);
  }
}

Helpers stay private; the public method orchestrates them.

Private constructor for singletons:

class Config {
  private static instance: Config | null = null;

  private constructor(public readonly env: string) {}

  static get(): Config {
    return this.instance ??= new Config(process.env.NODE_ENV ?? 'dev');
  }
}

Private static for singleton state:

class Counter {
  private static count = 0;

  static increment(): void { this.count++; }
  static get(): number { return this.count; }
}

Why these patterns: They keep the public API minimal, enforce invariants, and let internals evolve. The public surface is what other code depends on; the private surface is what you can change freely. A small public API is easier to maintain.


protected in practice

Where protected helps most.

Template method pattern:

abstract class DataProcessor {
  process(data: string[]): string[] {
    const filtered = this.filter(data);
    const transformed = this.transform(filtered);
    return this.sort(transformed);
  }

  protected filter(data: string[]): string[] { return data; }
  protected transform(data: string[]): string[] { return data; }
  protected sort(data: string[]): string[] { return [...data].sort(); }
}

class UpperCaseProcessor extends DataProcessor {
  protected override transform(data: string[]): string[] {
    return data.map(s => s.toUpperCase());
  }
}

process is the public algorithm. The three protected hooks are overridable. Subclasses change behavior without redefining the flow.

Shared state with subclasses:

abstract class Entity {
  protected id: string;
  protected createdAt: Date;

  constructor() {
    this.id = crypto.randomUUID();
    this.createdAt = new Date();
  }
}

class User extends Entity {
  constructor(public name: string) {
    super();
  }

  describe(): string {
    return `${this.name} (${this.id})`;  // โœ… protected id
  }
}

Subclasses get access to id and createdAt; outside code doesn’t.

Protected helper methods:

class Logger {
  protected prefix = '[LOG]';

  log(msg: string): void {
    this.write(`${this.prefix} ${msg}`);
  }

  protected write(msg: string): void {
    console.log(msg);
  }
}

class FileLogger extends Logger {
  protected override write(msg: string): void {
    // write to file instead
  }
}

write is protected โ€” subclasses can override, outside code can’t call it directly.

Protected constructor for abstract-like base:

class Animal {
  protected constructor(public name: string) {}
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }
}

new Animal('x');                   // โŒ protected
new Dog('Rex');                    // โœ…

Subclasses can construct, but the base class can’t be instantiated directly.

When protected beats private: When subclasses need to extend or override behavior. If a member is truly internal, use private. If subclasses need it, use protected.

Why protected is the inheritance-friendly modifier: It’s the sweet spot between public (too open) and private (too restrictive for subclasses). Template methods, hooks, and shared state use protected. Modern TypeScript sometimes prefers composition over inheritance, but when you do inherit, protected is the right tool for internals shared across the hierarchy.


A full example

A small class hierarchy with all four modifiers.

// ============================================
// BASE CLASS
// ============================================

abstract class Shape {
  protected readonly id: string;
  protected name: string;
  private static count = 0;

  constructor(name: string) {
    this.id = crypto.randomUUID();
    this.name = name;
    Shape.count++;
  }

  static created(): number {
    return Shape.count;
  }

  abstract area(): number;

  describe(): string {
    return `${this.name} (${this.id.slice(0, 8)}): area ${this.area().toFixed(2)}`;
  }

  protected log(msg: string): void {
    console.log(`[Shape ${this.id.slice(0, 4)}] ${msg}`);
  }
}

// ============================================
// SUBCLASS
// ============================================

class Circle extends Shape {
  constructor(public readonly radius: number) {
    super('Circle');
  }

  override area(): number {
    this.log('computing area');
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Shape {
  constructor(
    public readonly width: number,
    public readonly height: number
  ) {
    super('Rectangle');
  }

  override area(): number {
    return this.width * this.height;
  }
}

// ============================================
// USAGE
// ============================================

const c = new Circle(5);
const r = new Rectangle(4, 6);

console.log(c.describe());
console.log(r.describe());
console.log(`Shapes created: ${Shape.created()}`);

// c.id;           // โŒ protected
// c.name;         // โŒ protected
// c.log('x');     // โŒ protected
// c.radius = 10;  // โŒ readonly

What each modifier does here:

  • protected readonly id โ€” subclasses can read, no one can reassign
  • protected name โ€” subclasses can read/write
  • private static count โ€” internal to the class
  • public readonly radius โ€” callers can read, no one can reassign
  • protected log โ€” subclasses can call
  • abstract area โ€” subclasses must implement

Why this shape: It uses every modifier meaningfully. id is protected so subclasses can use it. count is private static โ€” internal tracking. radius is public readonly โ€” part of the API but immutable. log is protected โ€” subclass helper. Each modifier serves a specific purpose. That’s the design goal.


Complete Example Session

# ============================================
# PART 1: PUBLIC
# ============================================

cat > public.ts << 'EOF'
class User {
  name = 'Alice';
  public id = 1;
}

const u = new User();
console.log(u.name, u.id);
u.name = 'Bob';
u.id = 2;
console.log(u.name, u.id);
EOF

npx tsc --noEmit public.ts
# (no errors)

# ============================================
# PART 2: PRIVATE
# ============================================

cat > private.ts << 'EOF'
class Counter {
  private count = 0;

  increment(): void { this.count++; }
  value(): number { return this.count; }
}

const c = new Counter();
c.increment();
c.increment();
console.log(c.value());  // 2

// c.count;  // โŒ private
EOF

npx tsc --noEmit private.ts
# (no errors)

# ============================================
# PART 3: PROTECTED
# ============================================

cat > protected.ts << 'EOF'
class Animal {
  protected name: string;
  constructor(name: string) { this.name = name; }
}

class Dog extends Animal {
  bark(): string { return `${this.name} barks`; }
}

const d = new Dog('Rex');
console.log(d.bark());

// d.name;  // โŒ protected
EOF

npx tsc --noEmit protected.ts
# (no errors)

# ============================================
# PART 4: READONLY
# ============================================

cat > readonly.ts << 'EOF'
class User {
  readonly id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  rename(n: string): void { this.name = n; }
  // changeId(): void { this.id = 0; }  // โŒ
}

const u = new User(1, 'Alice');
u.rename('Alicia');
console.log(u.id, u.name);
EOF

npx tsc --noEmit readonly.ts
# (no errors)

# ============================================
# PART 5: TRIGGER ERRORS
# ============================================

cat > errors.ts << 'EOF'
class User {
  private secret = 'x';
  protected name = '';
  readonly id = 1;
}

const u = new User();
u.secret;                     // โŒ private
u.name;                       // โŒ protected
u.id = 2;                     // โŒ readonly
EOF

npx tsc --noEmit errors.ts
# [ errors.ts:7:3  - Property 'secret' is private ... ]
# [ errors.ts:8:3  - Property 'name' is protected ... ]
# [ errors.ts:9:1  - Cannot assign to 'id' because it is a read-only property. ]

rm errors.ts

# ============================================
# PART 6: PRIVATE BYPASS WITH as any
# ============================================

cat > bypass.ts << 'EOF'
class User {
  private secret = 'x';
}

const u = new User();
console.log((u as any).secret);  // 'x' โ€” bypasses compile-time check
EOF

npx tsc --noEmit bypass.ts
# (no errors)

node -e "
class User { constructor() { this.secret = 'x'; } }
const u = new User();
console.log(u.secret);
"
# [ x ]
#   โ†‘ private is erased at runtime

# ============================================
# PART 7: #private RUNTIME
# ============================================

cat > hash.ts << 'EOF'
class User {
  #secret = 'x';

  get(): string { return this.#secret; }
}

const u = new User();
console.log(u.get());

// u.#secret;  // โŒ SyntaxError at parse time
EOF

npx tsc --noEmit hash.ts
# (no errors)

# ============================================
# PART 8: COMPILE AND RUN
# ============================================

npx tsc public.ts private.ts protected.ts readonly.ts hash.ts
node public.js
# [ Alice 1 ]
# [ Bob 2 ]

node private.js
# [ 2 ]

node protected.js
# [ Rex barks ]

node readonly.js
# [ 1 Alicia ]

node hash.js
# [ x ]

Quick Reference

The Four Modifiers

ModifierAccessAssignable
publicAnywhere (default)โœ…
privateClass onlyโœ…
protectedClass + subclassesโœ…
readonlySame as modifierConstructor only
#privateClass only (runtime)โœ…

Access Table

Frompublicprivateprotected#private
Same classโœ…โœ…โœ…โœ…
Subclassโœ…โŒโœ…โŒ
Outsideโœ…โŒโŒโŒ

Composition

SyntaxMeaning
private readonly xPrivate, immutable after constructor
protected readonly xProtected, immutable after constructor
public readonly xPublic read, immutable after constructor
static readonly xClass-level immutable
private static xClass-level private

readonly Deepness

SyntaxEffect
readonly x: TCan’t reassign x
readonly x: T[]Can’t reassign, can mutate
readonly x: readonly T[]Can’t reassign, can’t mutate
readonly x: Readonly<T>Shallow immutable
readonly x: DeepReadonly<T>Deep immutable

TypeScript vs JavaScript

Featureprivate#private
Compile-time enforcedโœ…โœ…
Runtime enforcedโŒโœ…
Bypassable via as anyโœ…โŒ
Erased at runtimeโœ…โŒ
Composes with readonlyโœ…โœ…
Composes with staticโœ…โœ…

Constructor Modifiers

SyntaxEffect
constructor(public x: T)Public property + assign
constructor(private x: T)Private property + assign
constructor(protected x: T)Protected property + assign
constructor(readonly x: T)Readonly property + assign
constructor(public readonly x: T)Public readonly + assign
constructor(private readonly x: T)Private readonly + assign
private constructor()Can’t new outside class
protected constructor()Can’t new outside hierarchy

When to Use

NeedModifier
Default memberpublic
Internal stateprivate
Subclass accessprotected
Immutable after constructionreadonly
Real encapsulation#private
Singletonprivate constructor
Abstract-like baseprotected constructor

Common Combinations

PatternSyntax
Singletonprivate static instance + private constructor
DIconstructor(private http: HttpClient)
Immutable IDpublic readonly id
Shared stateprotected value
Internal cacheprivate cache = new Map()
Constantstatic readonly MAX = 100

Rules

RuleDetail
Defaultpublic
Orderaccess readonly (convention)
Composeโœ… all combinations allowed
Staticโœ… with any modifier
RuntimeOnly # is enforced

Best Practices

โœ… Do This:

// Use public by default
class User {
  name = '';
  id = 0;
}                                                        // โœ…

// Mark internals private
class User {
  private secret = 'x';
}                                                        // โœ…

// Use protected for subclass access
class Animal {
  protected name = '';
}                                                        // โœ…

// Mark immutable fields readonly
class User {
  readonly id: number;
  constructor(id: number) { this.id = id; }
}                                                        // โœ…

// Compose modifiers
class Config {
  private readonly secret = 'x';
}                                                        // โœ…

// Use #private for real privacy
class Secret {
  #value = 'x';
}                                                        // โœ…

// Use private constructor for singletons
class Singleton {
  private static instance: Singleton | null = null;
  private constructor() {}
  static get(): Singleton { return this.instance ??= new Singleton(); }
}                                                        // โœ…

// Use parameter properties for DI
class Service {
  constructor(private http: HttpClient) {}
}                                                        // โœ…

// Document why a member is public/private
// via the modifier and its use                          // โœ…

โŒ Don’t Do This:

// Don't use `private` for security
class Secret {
  private password = 'x';  // โš ๏ธ  bypassable via as any        // โš ๏ธ
}

// Don't expect `readonly` to be deep
class Store {
  readonly items = [1, 2, 3];
  // store.items.push(4);  // โš ๏ธ  still works                  // โš ๏ธ
}

// Don't use `private` when subclasses need access
class Base {
  private x = 1;  // subclasses can't use it                // โš ๏ธ
}

// Don't expose internal state publicly
class Cache {
  data: Record<string, unknown> = {};  // โš ๏ธ  use private      // โš ๏ธ
}

// Don't mix # and private carelessly
class Mixed {
  private a = 1;
  #b = 2;  // โš ๏ธ  pick one style                             // โš ๏ธ
}

// Don't forget readonly on immutable fields
class User {
  id: number;  // โš ๏ธ  assignable after construction          // โš ๏ธ
}

// Don't bypass modifiers with as any in production code
(u as any).secret;                                         // โŒ

// Don't use protected where public is enough
class Simple {
  protected name = '';  // โš ๏ธ  no subclasses โ†’ make it public // โš ๏ธ
}

Common Pitfalls

PitfallProblemSolution
private thought as securityBypassableUse #private for real privacy
readonly on mutable arrayStill mutableUse readonly T[]
private blocks subclass accessSubclass errorUse protected
Forgetting readonly on IDsAccidental reassignmentAdd readonly
Mixing # and privateInconsistent stylePick one convention
Accessing protected from outsideCompile errorUse a method or public getter
Uninitialized private fieldStrict errorInitialize or use !
Static private accessed on instanceCompile errorUse Class.x, not instance.x
Public state directly mutatedBreaks invariantsMake private, use methods

Real-World Examples

1. Public property

class User { name = ''; }

2. Private property

class User { private secret = 'x'; }

3. Protected property

class Base { protected value = 1; }

4. Readonly property

class User { readonly id = 1; }

5. Readonly assignable in constructor

class User {
  readonly id: number;
  constructor(id: number) { this.id = id; }
}

6. Private readonly

class Config { private readonly secret = 'x'; }

7. Protected readonly

class Entity { protected readonly createdAt = new Date(); }

8. #private field

class Secret { #value = 'x'; }

9. Parameter property

class Service { constructor(private http: HttpClient) {} }

10. Private method

class Parser {
  parse(s: string): object {
    return this.validate(s);
  }
  private validate(s: string): object { return {}; }
}

11. Protected method

class Base {
  protected log(msg: string): void { console.log(msg); }
}

12. Static private

class Counter {
  private static count = 0;
  static inc() { this.count++; }
}

13. Static readonly

class Config {
  static readonly MAX = 100;
}

14. Private constructor

class Singleton {
  private constructor() {}
  static get() { return new Singleton(); }
}

15. Protected constructor

class AbstractBase {
  protected constructor() {}
}
class Concrete extends AbstractBase {}

16. Readonly array

class Store { readonly items: readonly string[] = []; }

17. Public getter for private

class Counter {
  private count = 0;
  get value(): number { return this.count; }
}

18. Subclass access protected

class Animal {
  protected name = '';
}
class Dog extends Animal {
  bark() { return this.name; }
}

19. Access via interface

interface Named { name: string; }
class User { name = 'Alice'; }
const n: Named = new User();

20. Compose modifiers

class Config {
  constructor(
    public readonly name: string,
    private readonly secret: string
  ) {}
}

Visual: Access Ranges

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  public                                      โ”‚
โ”‚  โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—    โ”‚
โ”‚  โ•‘  Outside  Subclass  Same class       โ•‘    โ”‚
โ”‚  โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  protected                                   โ”‚
โ”‚                โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—  โ”‚
โ”‚                โ•‘  Subclass  Same class    โ•‘  โ”‚
โ”‚                โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  private                                     โ”‚
โ”‚                           โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—    โ”‚
โ”‚                           โ•‘  Same class โ•‘    โ”‚
โ”‚                           โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  #private                                    โ”‚
โ”‚                           โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—    โ”‚
โ”‚                           โ•‘  Same class โ•‘    โ”‚
โ”‚                           โ•‘  (runtime)  โ•‘    โ”‚
โ”‚                           โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Compile-time vs Runtime

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Source (TypeScript)                         โ”‚
โ”‚                                              โ”‚
โ”‚  class User {                                โ”‚
โ”‚    private secret = 'x';                     โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  compile
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Output (JavaScript)                         โ”‚
โ”‚                                              โ”‚
โ”‚  class User {                                โ”‚
โ”‚    constructor() {                           โ”‚
โ”‚      this.secret = 'x';  // โ† public!        โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  (private was erased)                        โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Source (TypeScript)                         โ”‚
โ”‚                                              โ”‚
โ”‚  class User {                                โ”‚
โ”‚    #secret = 'x';                            โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  compile
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Output (JavaScript)                         โ”‚
โ”‚                                              โ”‚
โ”‚  class User {                                โ”‚
โ”‚    #secret = 'x';  // still private          โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  (runtime enforced)                          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: readonly Composition

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  public readonly x                           โ”‚
โ”‚                                              โ”‚
โ”‚  Read:  โœ… anywhere                          โ”‚
โ”‚  Write: constructor only                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  private readonly x                          โ”‚
โ”‚                                              โ”‚
โ”‚  Read:  โœ… class only                        โ”‚
โ”‚  Write: constructor only                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  protected readonly x                        โ”‚
โ”‚                                              โ”‚
โ”‚  Read:  โœ… class + subclasses                โ”‚
โ”‚  Write: constructor only                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: readonly is Shallow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Store {                               โ”‚
โ”‚    readonly items: string[] = [];            โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  s.items = ['x'];   โŒ reassign              โ”‚
โ”‚  s.items.push('y'); โœ… mutate                โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class Store {                               โ”‚
โ”‚    readonly items: readonly string[] = [];   โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  s.items = ['x'];   โŒ reassign              โ”‚
โ”‚  s.items.push('y'); โŒ mutate                โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Constructor Parameter Modifiers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  constructor(public x: number) { }           โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ this.x is a public property               โ”‚
โ”‚  โ†’ assigned from parameter                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  constructor(private x: number) { }          โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ this.x is private                         โ”‚
โ”‚  โ†’ assigned from parameter                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  constructor(readonly x: number) { }         โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ public readonly                          โ”‚
โ”‚  โ†’ assigned from parameter                   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  constructor(x: number) { }                  โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ parameter only                           โ”‚
โ”‚  โ†’ no property created                      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: When to Use Which

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Anyone should access?                       โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Yes โ”€โ”€โ–บ public                     โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ No                                  โ”‚
โ”‚            โ”‚                                 โ”‚
โ”‚            โ”œโ”€โ”€ Subclasses need it?           โ”‚
โ”‚            โ”‚      โ”‚                          โ”‚
โ”‚            โ”‚      โ”œโ”€โ”€ Yes โ”€โ”€โ–บ protected      โ”‚
โ”‚            โ”‚      โ”‚                          โ”‚
โ”‚            โ”‚      โ””โ”€โ”€ No  โ”€โ”€โ–บ private        โ”‚
โ”‚            โ”‚                                 โ”‚
โ”‚            โ””โ”€โ”€ Need runtime privacy?         โ”‚
โ”‚                   โ”‚                          โ”‚
โ”‚                   โ”œโ”€โ”€ Yes โ”€โ”€โ–บ #private       โ”‚
โ”‚                   โ”‚                          โ”‚
โ”‚                   โ””โ”€โ”€ No  โ”€โ”€โ–บ private        โ”‚
โ”‚                                              โ”‚
โ”‚  Add `readonly` if it shouldn't change.      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Common Combinations

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Class member patterns                       โ”‚
โ”‚                                              โ”‚
โ”‚  public readonly id          โ†’ identity      โ”‚
โ”‚  private cache               โ†’ internal      โ”‚
โ”‚  private readonly secret     โ†’ config        โ”‚
โ”‚  protected value             โ†’ shared state  โ”‚
โ”‚  protected readonly createdAtโ†’ base field    โ”‚
โ”‚  static readonly MAX         โ†’ constant      โ”‚
โ”‚  #private token              โ†’ real secret   โ”‚
โ”‚  private constructor         โ†’ singleton     โ”‚
โ”‚  protected constructor       โ†’ base class    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Composition Rules

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Access + readonly                           โ”‚
โ”‚                                              โ”‚
โ”‚  public   readonly โœ…                        โ”‚
โ”‚  private  readonly โœ…                        โ”‚
โ”‚  protected readonly โœ…                       โ”‚
โ”‚                                              โ”‚
โ”‚  Access + static                             โ”‚
โ”‚                                              โ”‚
โ”‚  public   static โœ…                          โ”‚
โ”‚  private  static โœ…                          โ”‚
โ”‚  protected static โœ…                         โ”‚
โ”‚                                              โ”‚
โ”‚  readonly + static                           โ”‚
โ”‚                                              โ”‚
โ”‚  static readonly โœ…                          โ”‚
โ”‚  private static readonly โœ…                  โ”‚
โ”‚                                              โ”‚
โ”‚  All three together                          โ”‚
โ”‚                                              โ”‚
โ”‚  private static readonly โœ…                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Private Bypass

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class User {                                โ”‚
โ”‚    private secret = 'x';                     โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  const u = new User();                       โ”‚
โ”‚  u.secret;          โŒ compile error         โ”‚
โ”‚  (u as any).secret; โœ… compiles              โ”‚
โ”‚                                              โ”‚
โ”‚  Runtime: property is public                 โ”‚
โ”‚  โ†’ Console: 'x'                              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  class User {                                โ”‚
โ”‚    #secret = 'x';                            โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  const u = new User();                       โ”‚
โ”‚  u.#secret;           โŒ syntax error        โ”‚
โ”‚  (u as any).#secret;  โŒ syntax error        โ”‚
โ”‚                                              โ”‚
โ”‚  Runtime: truly private                      โ”‚
โ”‚  โ†’ No bypass                                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Compile-Time Checks

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  User code                                   โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  TypeScript compiler                         โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ public   โ†’ allowed anywhere        โ”‚
โ”‚       โ”œโ”€โ”€ private  โ†’ class only              โ”‚
โ”‚       โ”œโ”€โ”€ protectedโ†’ class + subclasses      โ”‚
โ”‚       โ”œโ”€โ”€ readonly โ†’ constructor only        โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  Emitted JavaScript                          โ”‚
โ”‚                                              โ”‚
โ”‚  Modifiers erased โ€” plain properties         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ModifierAccessRuntime
publicAnywhereN/A (default)
privateClass onlyCompile-time
protectedClass + subclassesCompile-time
readonlySame as accessCompile-time
#privateClass onlyRuntime-enforced

Key takeaways:

  • public is the default โ€” accessible anywhere
  • private restricts to the class body โ€” not subclasses, not outside
  • protected allows class + subclasses โ€” the inheritance-friendly modifier
  • readonly permits assignment only in the constructor
  • Modifiers compose โ€” private readonly, protected static, etc.
  • Constructor parameter modifiers โ€” constructor(public x: T) โ€” declare and assign in one line
  • TypeScript’s private and protected are compile-time only โ€” erased at runtime, bypassable with as any
  • #private is runtime-enforced โ€” real privacy, no bypass
  • readonly is shallow โ€” use readonly T[] for immutable arrays
  • private is convention, not security โ€” use #private when you need real privacy
  • private constructor for singletons โ€” callers must use a factory
  • protected constructor for abstract-like bases โ€” only subclasses can construct
  • Use readonly on IDs, timestamps, and configuration โ€” anything that shouldn’t change

Remember: Access modifiers express design intent and let the compiler enforce it. Public for the API, private for internals, protected for subclass access, readonly for immutability. But remember the enforcement boundary: TypeScript’s modifiers are compile-time checks, not runtime guarantees. When you need true privacy, reach for #private. For everything else, the modifiers keep your class’s surface honest โ€” and the compiler keeps you honest about using it.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!