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.
| Modifier | Accessible from | Assignable after construction |
|---|---|---|
public | Anywhere | โ |
private | Same class only | โ (within class) |
protected | Same class + subclasses | โ (within class hierarchy) |
readonly | Same 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
publicis 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
privateisn’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 addedprivateas 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
protectedexists: Subclasses often need to interact with parent state or call parent helpers.privatewould prevent that.protectedis 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
readonlymatters: Immutable state is easier to reason about. Areadonlyproperty 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,readonlyis 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:
| Combination | Read access | Write access |
|---|---|---|
public | Anyone | Anyone |
public readonly | Anyone | Constructor only |
private | Class only | Class only |
private readonly | Class only | Constructor only |
protected | Class + subclasses | Class + subclasses |
protected readonly | Class + subclasses | Constructor 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 anybypasses all checks
JavaScript’s #private is runtime-enforced:
- The
#prefix is a JavaScript feature - The runtime refuses access outside the class
as anydoesn’t bypass it- True encapsulation
Comparison:
| Feature | private | #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:
| Need | Modifier |
|---|---|
| Convention โ “don’t touch this” | private |
| Subclass access | protected |
| Real encapsulation | #private |
| Immutable after construction | readonly |
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
privatebefore#: When TypeScript was designed, JavaScript had no private fields.privatewas 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
protectedis 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,protectedis 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 reassignprotected nameโ subclasses can read/writeprivate static countโ internal to the classpublic readonly radiusโ callers can read, no one can reassignprotected logโ subclasses can callabstract areaโ subclasses must implement
Why this shape: It uses every modifier meaningfully.
idis protected so subclasses can use it.countis private static โ internal tracking.radiusis public readonly โ part of the API but immutable.logis 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
| Modifier | Access | Assignable |
|---|---|---|
public | Anywhere (default) | โ |
private | Class only | โ |
protected | Class + subclasses | โ |
readonly | Same as modifier | Constructor only |
#private | Class only (runtime) | โ |
Access Table
| From | public | private | protected | #private |
|---|---|---|---|---|
| Same class | โ | โ | โ | โ |
| Subclass | โ | โ | โ | โ |
| Outside | โ | โ | โ | โ |
Composition
| Syntax | Meaning |
|---|---|
private readonly x | Private, immutable after constructor |
protected readonly x | Protected, immutable after constructor |
public readonly x | Public read, immutable after constructor |
static readonly x | Class-level immutable |
private static x | Class-level private |
readonly Deepness
| Syntax | Effect |
|---|---|
readonly x: T | Can’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
| Feature | private | #private |
|---|---|---|
| Compile-time enforced | โ | โ |
| Runtime enforced | โ | โ |
Bypassable via as any | โ | โ |
| Erased at runtime | โ | โ |
Composes with readonly | โ | โ |
Composes with static | โ | โ |
Constructor Modifiers
| Syntax | Effect |
|---|---|
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
| Need | Modifier |
|---|---|
| Default member | public |
| Internal state | private |
| Subclass access | protected |
| Immutable after construction | readonly |
| Real encapsulation | #private |
| Singleton | private constructor |
| Abstract-like base | protected constructor |
Common Combinations
| Pattern | Syntax |
|---|---|
| Singleton | private static instance + private constructor |
| DI | constructor(private http: HttpClient) |
| Immutable ID | public readonly id |
| Shared state | protected value |
| Internal cache | private cache = new Map() |
| Constant | static readonly MAX = 100 |
Rules
| Rule | Detail |
|---|---|
| Default | public |
| Order | access readonly (convention) |
| Compose | โ all combinations allowed |
| Static | โ with any modifier |
| Runtime | Only # 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
| Pitfall | Problem | Solution |
|---|---|---|
private thought as security | Bypassable | Use #private for real privacy |
readonly on mutable array | Still mutable | Use readonly T[] |
private blocks subclass access | Subclass error | Use protected |
Forgetting readonly on IDs | Accidental reassignment | Add readonly |
Mixing # and private | Inconsistent style | Pick one convention |
| Accessing protected from outside | Compile error | Use a method or public getter |
| Uninitialized private field | Strict error | Initialize or use ! |
| Static private accessed on instance | Compile error | Use Class.x, not instance.x |
| Public state directly mutated | Breaks invariants | Make 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
| Modifier | Access | Runtime |
|---|---|---|
public | Anywhere | N/A (default) |
private | Class only | Compile-time |
protected | Class + subclasses | Compile-time |
readonly | Same as access | Compile-time |
#private | Class only | Runtime-enforced |
Key takeaways:
publicis the default โ accessible anywhereprivaterestricts to the class body โ not subclasses, not outsideprotectedallows class + subclasses โ the inheritance-friendly modifierreadonlypermits 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
privateandprotectedare compile-time only โ erased at runtime, bypassable withas any #privateis runtime-enforced โ real privacy, no bypassreadonlyis shallow โ usereadonly T[]for immutable arraysprivateis convention, not security โ use#privatewhen you need real privacyprivate constructorfor singletons โ callers must use a factoryprotected constructorfor abstract-like bases โ only subclasses can construct- Use
readonlyon 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!