Imagine you're in a library, surrounded by aisles full of books. While many books are available for everyone to read, some are restricted to specific members, such as academic researchers, due to security reasons. Similarly, in object-oriented programming, not all parts of an object should be openly accessible to every part of the program. For such situations, many programming languages, including TypeScript, introduce the concept of access modifiers (like private, public, protected, and readonly). You can think of this as controlling who can see, use, or modify certain parts of your class at specific levels, like how some sections in a library are open to all, while others require special access.
Why access modifiers?
Access modifiers in TypeScript are mainly important for reasons related to Object-Oriented Programming (OOP):
- Encapsulation: By restricting access, it makes sure that internal details of a class will be hidden and that only essential parts are shared. This is a key principle of object-oriented programming.
- Maintenance: It becomes easier to maintain and update the code when you're sure about which parts of a class can be accessed from outside.
- Safety: It ensures that the class is used correctly and keeps its details secure when protection is required. Using the appropriate access keyword in such situations can prevent unwanted changes or access.
When we want to assign a specific access level to certain members of a class using one of the access modifier keywords (private, public, protected, and readonly), we place them directly before the property or method name.
Let's examine these keywords and understand when to use each:
'public' keyword
By default, all members (properties and methods) of a class in TypeScript are public. This means they can be accessed from anywhere, regardless of whether they're inside the class, in a child class, or outside the class.
class Book {
public title: string;
constructor(title: string) {
this.title = title;
}
public displayTitle() {
console.log(this.title);
}
}
const myBook = new Book("TypeScript Essentials");
myBook.displayTitle(); // Outputs: TypeScript Essentials
In the above code snippet, we have a Book class with a public property title and a public method displayTitle(). Because they are set as public, they can be accessed from anywhere in the code and can be used to create an instance from it. This is similar to a book in a library that anyone can read.
'private' keyword
When you mark a class member as private, it means you don't want it to be accessed from outside the class in which it's defined. This protects certain properties or methods from unwanted changes and hides implementation details.
For example, in the code below, the Journal class contains a private property entries and a private method displayEntries(). When trying to call the displayEntries() method outside the class, TypeScript throws an error, expressing that we're trying to access a private member.
class Journal {
private entries: string[] = [];
addEntry(entry: string) {
this.entries.push(entry);
}
private displayEntries() {
console.log(this.entries);
}
}
const myJournal = new Journal();
myJournal.addEntry("Learned about TypeScript access modifiers today.");
myJournal.displayEntries(); // Error: displayEntries is private and only accessible within class 'Journal'.
If the constructor is private, you can't create an object of that class using the new keyword elsewhere in the program other than the containing class:
class MyClass {
private constructor() {
console.log('MyClass instance created.');
}
}
const obj = new MyClass(); // Error: Constructor of class 'MyClass' is private and only accessible within the class declaration.
Here, the constructor of MyClass is marked as private. Therefore, trying to instantiate the class using the new keyword directly outside of the class will result in an error.
Furthermore, there may be situations where you use the private keyword to limit access to a property while allowing external classes to interact with it, but in a controlled way. For instance, you might want others to read the value of a private property without changing it or to check something before this value is updated. In the context of object-oriented programming, getters and setters are often used for this purpose:
- Getters — denoted by the
getkeyword — are methods that allow you to look at the value of a private property without changing it. - Setters — denoted by the
setkeyword — are methods that allow you modify a property's value. Additionally, with setters, you can add rules about how it's changed, like validating the input, ensuring it meets certain criteria, and so on.
So, with getters and setters, you can keep the control you get from private, but also decide how and when to share or change the data.
class Person {
private _name: string;
constructor(name: string) {
this._name = name;
}
// Getter for the name property
get name(): string {
return this._name;
}
// Setter for the name property
set name(value: string) {
if (value) {
this._name = value;
}
}
}
To illustrate, in the code provided above, the Person class has a class member called _name. Direct interactions with this property are restricted outside the class boundaries by marking it with the private keyword. To grant controlled access, a getter (name) and a setter (name) are introduced within the class. The name getter method allows the value of _name to be read and the name setter method allows the value of _name to be updated with some basic validation (checking it to prevent empty values).
_name shows that a variable or property is meant for internal use or is private. It's a hint to developers to treat it as private or protected, even if the language doesn't strictly enforce this treatment. 'protected' keyword
The protected modifier is similar to private, but with an exception: members marked as protected can also be accessed within deriving (child) classes. This also means that when a class constructor is marked as protected, instances of the class cannot be created from outside of it using the new keyword, similar to a private constructor. However, the class can still be extended (or inherited) by other classes.
class Novel {
protected characters: string[] = [];
addCharacter(name: string) {
this.characters.push(name);
}
}
class FantasyNovel extends Novel {
displayCharacters() {
console.log(this.characters);
}
}
const myFantasy = new FantasyNovel();
myFantasy.addCharacter("Frodo");
myFantasy.displayCharacters(); // Outputs: ['Frodo']
Here, the Novel class has a protected property characters. Although it's not accessible directly outside of this class, the derived class FantasyNovel can access it and display its contents. Overall, the protected keyword allows a class to share certain elements with its subclasses without making them directly accessible to the rest of the code and supports class hierarchies with controlled instantiation (object creation).
'readonly' keyword
The readonly modifier ensures that once a property (not a method) is set within a class, it cannot be changed thereafter (making the property immutable). In simpler terms, it is all about keeping a property's value constant after its first assignment, while other modifiers are about controlling access or visibility.
class Diary {
readonly createdAt: Date;
constructor() {
this.createdAt = new Date();
}
}
const todayDiary = new Diary();
todayDiary.createdAt = new Date(); // Error: Cannot assign to 'createdAt' because it is a read-only property.
In the above code, the Diary class has a readonly property createdAt. After we create an instance of this class, trying to reassign a value to the createdAt property results in an error, emphasizing its immutability. We can visualize it as the first page in a diary that marks its start date; once written, you can't just go back and change that date.
Conclusion
TypeScript's access modifiers (public, private, protected, and readonly), offer varying levels of accessibility to class items. They play a significant role in protecting the inner workings of your classes and ensuring your programs run smoothly. With these access modifiers, developers can create more secure and well-organized TypeScript code.