We use cookies to enhance your experience on the site
CodeWorlds

Access Modifiers (public, private, protected)

"Security in Jurassic Park is a matter of the highest importance," states John Hammond, president of InGen, firmly. "Some data and procedures must be accessible to all employees, others only to those with appropriate clearances, and still others are so critical that only top management has access to them."

Just like in Jurassic Park, in object-oriented programming, controlling access to class properties and methods is a fundamental aspect of good design. TypeScript offers three access modifiers that allow you to precisely define who can use specific class elements and to what extent: public, private, and protected.

Introduction to Access Modifiers

Access modifiers determine from which levels of code you can access a class's properties and methods:

  1. public - accessible from anywhere in the code (default)
  2. private - accessible only within the class itself
  3. protected - accessible within the class and inheriting classes

Let's see how these modifiers work in practice by implementing a dinosaur management system in Jurassic Park.

The public Modifier

Elements marked as public are accessible from anywhere in the code - both inside the class and on its instances. This is the default access modifier in TypeScript - if you don't specify any modifier, the element will automatically be marked as public.

1class Dinosaur {
2  // Public properties
3  public nazwa: string;
4  public gatunek: string;
5  public wiek: number;
6
7  constructor(nazwa: string, gatunek: string, wiek: number) {
8    this.nazwa = nazwa;
9    this.gatunek = gatunek;
10    this.wiek = wiek;
11  }
12
13  // Public method
14  public displayInfo(): void {
15    console.log(`${this.nazwa}: ${this.gatunek}, ${this.wiek} years old`);
16  }
17}
18
19// Creating an instance
20const rex = new ParkDinosaur("Rex", "Tyrannosaurus", 7);
21
22// Accessing public properties and methods from outside the class
23console.log(rex.nazwa);  // "Rex" - access to public property
24rex.wiek = 8;  // Modifying a public property
25rex.displayInfo();  // Calling a public method

Public properties and methods are like publicly available information in the park - for example, the park map, opening hours, or restroom locations. Everyone can use them without restrictions.

The private Modifier

Elements marked as private are accessible only within the class where they were defined. This encapsulation allows you to hide internal implementation details and prevents accidental state modifications from outside the class.

1class HealthDiagnosis {
2  // Private properties
3  private id: string;
4  private testResults: Record<string, number>;
5  private notes: string;
6  public healthStatus: "healthy" | "sick" | "critical";
7
8  constructor(id: string) {
9    this.id = id;
10    this.testResults = {};
11    this.notes = "";
12    this.healthStatus = "healthy";
13  }
14
15  // Private method - used only inside the class
16  private analyzeResults(): "healthy" | "sick" | "critical" {
17    // Complex logic for analyzing test results
18    const bodyTemperature = this.testResults["bodyTemperature"] || 37;
19    const leukocyteLevel = this.testResults["leukocyteLevel"] || 7000;
20
21    if (bodyTemperature > 40 || leukocyteLevel > 15000) {
22      return "critical";
23    } else if (bodyTemperature > 38.5 || leukocyteLevel > 11000) {
24      return "sick";
25    } else {
26      return "healthy";
27    }
28  }
29
30  // Public methods using private properties
31  public addTestResult(name: string, value: number): void {
32    this.testResults[name] = value;
33    this.healthStatus = this.analyzeResults();  // Calling a private method
34  }
35
36  public addNote(note: string): void {
37    this.notes += note + "\n";
38  }
39
40  public getReport(): string {
41    return `
42      ID: ${this.id}
43      Health status: ${this.healthStatus}
44      Notes: ${this.notes || "None"}
45    `;
46  }
47}
48
49// Creating an instance
50const raptorDiagnosis = new HealthDiagnosis("VL-001");
51
52// Using the public API
53raptorDiagnosis.addTestResult("bodyTemperature", 39.2);
54raptorDiagnosis.addTestResult("leukocyteLevel", 12500);
55raptorDiagnosis.addNote("Symptoms of viral infection");
56
57console.log(raptorDiagnosis.healthStatus);  // "sick" - access to public property
58console.log(raptorDiagnosis.getReport());  // Calling a public method
59
60// Attempting to access private elements
61// console.log(raptorDiagnosis.id);  // Error: Property 'id' is private
62// console.log(raptorDiagnosis.testResults);  // Error: Property 'testResults' is private
63// raptorDiagnosis.analyzeResults();  // Error: Property 'analyzeResults' is private

Private properties and methods are like restricted procedures in the genetics laboratory - only authorized laboratory personnel (class methods) have access to them, while for the rest of the staff they are hidden for security reasons.

The protected Modifier

Elements marked as protected are accessible within the class where they were defined and in inheriting classes. This allows creating class hierarchies where derived classes have access to protected elements of the base class, but these elements remain inaccessible from outside the class hierarchy.

1// Base class
2class Animal {
3  // Protected properties
4  protected id: string;
5  protected species: string;
6  protected weight: number;
7  protected health: number;  // 0-100
8
9  constructor(id: string, species: string, weight: number) {
10    this.id = id;
11    this.species = species;
12    this.weight = weight;
13    this.health = 100;
14  }
15
16  // Protected method
17  protected modifyHealth(change: number): void {
18    this.health = Math.max(0, Math.min(100, this.health + change));
19  }
20
21  // Public method
22  public displayStatus(): string {
23    return `${this.species} (${this.id}): Weight: ${this.weight}kg, Health: ${this.health}%`;
24  }
25}
26
27// Inheriting class
28class Dinosaur extends Animal {
29  public name: string;
30  private aggressionLevel: number;  // 0-10
31
32  constructor(id: string, species: string, name: string, weight: number) {
33    super(id, species, weight);
34    this.name = name;
35    this.aggressionLevel = 5;
36  }
37
38  // Method using protected elements from the base class
39  public feed(amount: number): void {
40    this.modifyHealth(amount * 5);  // Access to protected method from base class
41    this.aggressionLevel = Math.max(0, this.aggressionLevel - 1);
42    console.log(`${this.name} has been fed. New health level: ${this.health}%`);
43  }
44
45  public takeDamage(amount: number): void {
46    this.modifyHealth(-amount);  // Access to protected method from base class
47    this.aggressionLevel = Math.min(10, this.aggressionLevel + 2);
48    console.log(`${this.name} took damage. New health level: ${this.health}%`);
49  }
50
51  // Overriding base method using protected properties
52  public displayStatus(): string {
53    return `${this.name} (${this.species}): Weight: ${this.weight}kg, Health: ${this.health}%, Aggression: ${this.aggressionLevel}/10`;
54  }
55}
56
57// Inheriting class with modification of protected property
58class MarineDinosaur extends Animal {
59  private divingDepth: number;
60
61  constructor(id: string, species: string, weight: number, divingDepth: number) {
62    super(id, species, weight);
63    this.divingDepth = divingDepth;
64
65    // Modifying a protected property in the constructor
66    this.health = 90;  // Marine dinosaurs start with lower health
67  }
68
69  public swim(time: number): void {
70    // Using a protected method
71    this.modifyHealth(time > 10 ? -10 : 5);
72
73    console.log(`Marine dinosaur ${this.id} swam for ${time} minutes at a depth of ${this.divingDepth}m`);
74    console.log(`New health level: ${this.health}%`);
75  }
76}
77
78// Creating instances
79const velociraptor = new Dinosaur("VL-002", "Velociraptor", "Blue", 150);
80const mosasaur = new MarineDinosaur("MS-001", "Mosasaurus", 15000, 50);
81
82// Using public methods
83velociraptor.feed(2);
84velociraptor.takeDamage(15);
85console.log(velociraptor.displayStatus());
86
87mosasaur.swim(15);
88console.log(mosasaur.displayStatus());
89
90// Attempting to access protected elements
91// console.log(velociraptor.id);  // Error: Property 'id' is protected
92// console.log(mosasaur.health);  // Error: Property 'health' is protected
93// velociraptor.modifyHealth(50);  // Error: Property 'modifyHealth' is protected

Protected properties and methods are like the park's security procedures - only authorized employees and their subordinates (base class and inheriting classes) have access to them, but not the park's guests (external code).

Constructor and Access Modifiers

TypeScript offers a special syntax for constructors that allows you to declare properties directly with access modifiers in constructor parameters. This is a shorthand notation that avoids code repetition.

1// Traditional notation
2class ParkEmployee {
3  private id: string;
4  public firstName: string;
5  public lastName: string;
6  protected accessLevel: number;
7
8  constructor(id: string, firstName: string, lastName: string, accessLevel: number) {
9    this.id = id;
10    this.firstName = firstName;
11    this.lastName = lastName;
12    this.accessLevel = accessLevel;
13  }
14}
15
16// Shorthand notation with modifiers in constructor parameters
17class ParkEmployeeShorthand {
18  constructor(
19    private id: string,
20    public firstName: string,
21    public lastName: string,
22    protected accessLevel: number
23  ) {}  // Empty constructor body - everything has already been initialized
24}
25
26// Both notations work identically
27const employee1 = new ParkEmployee("EMP-001", "John", "Hammond", 10);
28const employee2 = new ParkEmployeeShorthand("EMP-002", "Ellie", "Sattler", 8);
29
30console.log(employee1.firstName, employee1.lastName);  // "John Hammond"
31console.log(employee2.firstName, employee2.lastName);  // "Ellie Sattler"
32
33// console.log(employee1.id);  // Error: Property 'id' is private
34// console.log(employee2.id);  // Error: Property 'id' is private

This shorthand syntax is especially useful in simple classes with many properties, as it reduces the amount of repetitive code.

Encapsulation and the Accessor Pattern (Getters and Setters)

One of the main reasons for using access modifiers is encapsulation - hiding implementation details and providing only a controlled interface for interacting with the class. Part of this approach is using getters and setters - special methods that control access to properties.

1class EnclosureSecurity {
2  // Private properties
3  private _fenceStatus: "active" | "inactive" | "under_repair";
4  private _batteryChargeLevel: number;  // 0-100
5  private _lastInspection: Date;
6  private _eventLog: string[];
7
8  constructor() {
9    this._fenceStatus = "active";
10    this._batteryChargeLevel = 100;
11    this._lastInspection = new Date();
12    this._eventLog = ["System started: " + new Date().toISOString()];
13  }
14
15  // Getters - controlled read access to private properties
16  get fenceStatus(): "active" | "inactive" | "under_repair" {
17    this.addLog("Fence status read");
18    return this._fenceStatus;
19  }
20
21  get batteryChargeLevel(): number {
22    this.addLog("Battery charge level read");
23    return this._batteryChargeLevel;
24  }
25
26  get lastInspection(): Date {
27    return new Date(this._lastInspection.getTime());  // Defensive copy
28  }
29
30  get securityReport(): string {
31    this.addLog("Security report generated");
32    return `
33      Fence status: ${this._fenceStatus}
34      Battery charge level: ${this._batteryChargeLevel}%
35      Last inspection: ${this._lastInspection.toLocaleDateString()}
36      Number of events in log: ${this._eventLog.length}
37    `;
38  }
39
40  // Setters - controlled write access to private properties
41  set fenceStatus(newStatus: "active" | "inactive" | "under_repair") {
42    this.addLog(`Changed fence status from ${this._fenceStatus} to ${newStatus}`);
43    this._fenceStatus = newStatus;
44
45    if (newStatus === "inactive") {
46      this.startSecurityProcedures();
47    }
48  }
49
50  // This setter contains validation logic
51  set batteryChargeLevel(newLevel: number) {
52    if (newLevel < 0 || newLevel > 100) {
53      throw new Error("Battery charge level must be between 0 and 100");
54    }
55
56    const oldValue = this._batteryChargeLevel;
57    this._batteryChargeLevel = newLevel;
58    this.addLog(`Changed battery charge level from ${oldValue}% to ${newLevel}%`);
59
60    if (newLevel < 20) {
61      this.addLog("WARNING: Low battery level!");
62    }
63  }
64
65  // Private helper method
66  private addLog(message: string): void {
67    this._eventLog.push(`${new Date().toISOString()}: ${message}`);
68  }
69
70  // Private business method
71  private startSecurityProcedures(): void {
72    this.addLog("ALERT: Emergency procedures activated!");
73    // Emergency handling logic
74  }
75
76  // Public method
77  public performInspection(): void {
78    this._lastInspection = new Date();
79    this.addLog("Fence inspection performed");
80    this._batteryChargeLevel = 100;
81    this._fenceStatus = "active";
82  }
83
84  // Public method
85  public getEventLog(): string[] {
86    // We return a copy to prevent modification of the original log
87    return [...this._eventLog];
88  }
89}
90
91// Using the class
92const security = new EnclosureSecurity();
93
94// Using getters
95console.log("Fence status:", security.fenceStatus);
96console.log("Battery level:", security.batteryChargeLevel);
97
98// Using setters
99security.fenceStatus = "under_repair";
100security.batteryChargeLevel = 85;
101
102// Using public methods
103security.performInspection();
104
105// Attempting to set an invalid value
106try {
107  security.batteryChargeLevel = 110;  // Will throw an error
108} catch (e) {
109  console.error(e.message);
110}
111
112// Reading the report and logs
113console.log(security.securityReport);
114console.log("Event history:");
115security.getEventLog().forEach((log, i) => {
116  console.log(`${i + 1}. ${log}`);
117});

Using getters and setters provides greater control over data access:

  • We can add validation logic before changing a value
  • We can perform additional operations when accessing data
  • We can return defensive copies of objects to prevent accidental modifications
  • We can log data access

Practical Example: Dinosaur Management System

Below is a comprehensive example of a dinosaur management system that uses all the discussed access modifiers to create a well-organized object-oriented architecture:

1// Types and interfaces
2type DinosaurHealthStatus = "healthy" | "sick" | "in treatment" | "sedated" | "deceased";
3type DietType = "carnivore" | "herbivore" | "omnivore";
4
5interface IMonitoring {
6  startMonitoring(): void;
7  stopMonitoring(): void;
8  getMonitoringStatus(): string;
9}
10
11// Abstract base class
12abstract class ParkAnimal {
13  // Properties with different access modifiers
14  private _id: string;
15  protected _species: string;
16  protected _age: number;
17  protected _weight: number;
18  protected _status: DinosaurHealthStatus;
19  private _creationDate: Date;
20  private _lastUpdate: Date;
21
22  constructor(id: string, species: string, age: number, weight: number) {
23    this._id = id;
24    this._species = species;
25    this._age = age;
26    this._weight = weight;
27    this._status = "healthy";
28    this._creationDate = new Date();
29    this._lastUpdate = new Date();
30  }
31
32  // Getters
33  get id(): string {
34    return this._id;
35  }
36
37  get species(): string {
38    return this._species;
39  }
40
41  get age(): number {
42    return this._age;
43  }
44
45  get weight(): number {
46    return this._weight;
47  }
48
49  get status(): DinosaurHealthStatus {
50    return this._status;
51  }
52
53  // Setter with value checking and date update
54  set status(newStatus: DinosaurHealthStatus) {
55    if (this._status !== newStatus) {
56      this._status = newStatus;
57      this._lastUpdate = new Date();
58      this.logStatusChange(newStatus);
59    }
60  }
61
62  // Protected method accessible to inheriting classes
63  protected updateWeight(newWeight: number): void {
64    if (newWeight > 0) {
65      this._weight = newWeight;
66      this._lastUpdate = new Date();
67    } else {
68      throw new Error("Weight must be greater than zero.");
69    }
70  }
71
72  // Private method accessible only within this class
73  private logStatusChange(newStatus: DinosaurHealthStatus): void {
74    console.log(`[${new Date().toISOString()}] Status change ${this._id}: ${nowyStatus}`);
75  }
76
77  // Abstract method - every inheriting class must implement it
78  abstract getClinicalDescription(): string;
79
80  // Public method
81  public getBasicData(): string {
82    return `ID: ${this._id} | Species: ${this._gatunek} | Age: ${this._wiek} years | Weight: ${this._weight} kg | Status: ${this._status}`;
83  }
84}
85
86// Inheriting class
87class ParkDinosaur extends ParkAnimal implements IMonitoring {
88  // Private properties specific to dinosaurs
89  private _diet: DietType;
90  private _aggressionLevel: number; // 1-10
91  private _isMonitored: boolean;
92  private _lastMeal: Date | null;
93  private _assignedEnclosure: string | null;
94
95  constructor(
96    id: string,
97    gatunek: string,
98    wiek: number,
99    weight: number,
100    diet: DietType,
101    aggressionLevel: number
102  ) {
103    super(id, gatunek, wiek, weight);
104    this._diet = diet;
105    this._aggressionLevel = this.validateAggressionLevel(poziomAgresji);
106    this._isMonitored = false;
107    this._lastMeal = null;
108    this._assignedEnclosure = null;
109  }
110
111  // Implementation of abstract method
112  getClinicalDescription(): string {
113    let description = `Dinosaur ${this.gatunek} (ID: ${this.id})\n`;
114    description += `Status zdrowotny: ${this.status}\n`;
115    description += `Age: ${this.wiek} lat, Weight: ${this.weight} kg\n`;
116    description += `Diet: ${this._dieta}\n`;
117    description += `Aggression level: ${this._aggressionLevel}/10\n`;
118    description += `Last meal: ${this._lastMeal ? this._lastMeal.toLocaleString() : "No data"}\n`;
119
120    return description;
121  }
122
123  // Implementation of IMonitorowania interface
124  startMonitoring(): void {
125    this._isMonitored = true;
126    console.log(`Monitoring started for dinosaura ${this.id}`);
127  }
128
129  stopMonitoring(): void {
130    this._isMonitored = false;
131    console.log(`Monitoring stopped for dinosaura ${this.id}`);
132  }
133
134  getMonitoringStatus(): string {
135    return this._isMonitored ? "Active" : "Inactive";
136  }
137
138  // Public methods specific to dinosaurs
139  public feed(amount: number): void {
140    if (amount <= 0) {
141      throw new Error("Food amount must be greater than zero.");
142    }
143
144    const weightGain = this._dieta === "carnivore" ? amount * 0.3 : amount * 0.2;
145    this.updateWeight(this.weight + weightGain);
146
147    this._lastMeal = new Date();
148    this._aggressionLevel = this.validateAggressionLevel(this._aggressionLevel - 1);
149
150    console.log(`Dinosaur ${this.id} was fed. New weight: ${this.weight} kg`);
151  }
152
153  public assignToEnclosure(enclosureCode: string): void {
154    this._assignedEnclosure = enclosureCode;
155    console.log(`Dinosaur ${this.id} was assigned to enclosure ${enclosureCode}`);
156  }
157
158  public sedate(): void {
159    this.status = "sedated";
160    this._aggressionLevel = 1;
161    console.log(`Dinosaur ${this.id} was sedated`);
162  }
163
164  public wake(): void {
165    if (this.status === "sedated") {
166      this.status = "zdrowy";
167      this._aggressionLevel = this.validateAggressionLevel(this._aggressionLevel + 2);
168      console.log(`Dinosaur ${this.id} was awakened`);
169    }
170  }
171
172  // Getters and setters
173  get dieta(): TypDiety {
174    return this._dieta;
175  }
176
177  get poziomAgresji(): number {
178    return this._aggressionLevel;
179  }
180
181  get lastMeal(): Date | null {
182    return this._lastMeal ? new Date(this._lastMeal.getTime()) : null;
183  }
184
185  get przypisanyWybieg(): string | null {
186    return this._assignedEnclosure;
187  }
188
189  set poziomAgresji(poziom: number) {
190    this._aggressionLevel = this.validateAggressionLevel(poziom);
191  }
192
193  // Private helper method
194  private validateAggressionLevel(poziom: number): number {
195    return Math.max(1, Math.min(10, poziom));
196  }
197}
198
199// Using the system
200function testDinosaurManagementSystem() {
201  // Creating dinosaurs
202  const trex = new Dinosaur("T-REX-01", "Tyrannosaurus Rex", 8, 7500, "carnivore", 9);
203  const stegosaurus = new Dinosaur("STEG-01", "Stegosaurus", 12, 5000, "herbivorous", 4);
204
205  // Displaying basic information
206  console.log("=== DANE PODSTAWOWE ===");
207  console.log(trex.getBasicData());
208  console.log(stegosaurus.getBasicData());
209
210  // Operations on dinosaurs
211  trex.feed(50);
212  trex.assignToEnclosure("CARNIVORE-A");
213  trex.startMonitoring();
214
215  stegosaurus.feed(80);
216  stegosaurus.assignToEnclosure("HERBIVORE-B");
217
218  // Status change
219  trex.status = "w leczeniu";
220  stegosaurus.sedate();
221
222  // Getting clinical reports
223  console.log("\n=== RAPORTY KLINICZNE ===");
224  console.log(trex.pobierzOpisKliniczny());
225  console.log(stegosaurus.pobierzOpisKliniczny());
226
227  // Monitoring status
228  console.log("\n=== STATUS MONITOROWANIA ===");
229  console.log(`T-Rex monitoring: ${trex.pobierzStatusMonitorowania()}`);
230  console.log(`Stegozaur monitoring: ${stegosaurus.pobierzStatusMonitorowania()}`);
231
232  // Attempting to access private/protected elements
233  // The following lines would cause a compilation error:
234  // console.log(trex._id);  // Error: Property '_id' is private
235  // console.log(trex._gatunek);  // Error: Property '_gatunek' is protected
236  // trex.logZmianyStatusu("zdrowy");  // Error: Property 'logZmianyStatusu' is private
237  // trex.validateAggressionLevel(5);  // Error: Property 'validateAggressionLevel' is private
238}
239
240// Running the test
241testDinosaurManagementSystem();

In the example above:

  1. Abstract base class (ParkAnimal) defines common properties and behaviors for all animals in the park
  2. Private properties (_id, _dataUtworzenia, _lastUpdate) are accessible only within the class
  3. Protected properties (_gatunek, _wiek, _weight, _status) are accessible in the base class and inheriting classes
  4. Public getters and setters provide controlled access to properties
  5. Private methods (logZmianyStatusu, validateAggressionLevel) are used as helper implementations accessible only within the class
  6. Protected methods (updateWeight) are accessible to inheriting classes
  7. Abstract method (pobierzOpisKliniczny) enforces implementation in inheriting classes
  8. Interface (IMonitorowania) defines a contract that the Dinosaur class must implement

When and How to Use Access Modifiers

Choosing the appropriate access modifier depends on the specific use case, but there are a few general rules:

  1. Use private for:

    • Internal implementation details
    • Data that should never be accessible outside the class
    • Helper methods that are not part of the public API
    • Properties that should only be modified through controlled setters
  2. Use protected for:

    • Properties and methods that need to be accessible in inheriting classes
    • Implementations that can be extended or overridden in derived classes
    • Data that is common to the entire class hierarchy but should not be accessible from outside
  3. Use public for:

    • Elements that form the public API interface of the class
    • Methods that need to be accessible to all users of the class
    • Properties that can be safely read or modified from outside

Summary

Access modifiers in TypeScript are a key tool for implementing good object-oriented design principles such as encapsulation, inheritance, and polymorphism. They allow precise control over access to class properties and methods, which promotes creating secure, modular, and maintainable code.

Just like in Jurassic Park, where the security system requires different access levels, a well-designed object-oriented system uses access modifiers to ensure that each element is accessible only to those who should actually have access to it.

John Hammond would summarize it briefly: "In Jurassic Park and in programming - security is paramount. Access modifiers are like security zones in the park - public for everyone, protected for specialists, and private only for top-level management. That's the key to maintaining order and preventing... surprises."

Go to CodeWorlds