TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
39: Classes and Access Modifiers
A few years ago, I was reviewing a PR for a junior dev who was building a payment processing module. They had a PaymentSession class with a property called transactionAmount. Because they didn't specify an access modifier, it defaulted to public. Later in the codebase, a UI component—which should have only been displaying the amount—had a bug that accidentally reassigned that value to zero during a state update. The code compiled perfectly, but the actual payment processed for $0.00. It was a classic case of "too much visibility." If that property had been private and only modifiable via a specific method, the compiler would have caught the bug instantly.
Guarding Your Data with Public, Private, and Protected
In TypeScript, classes aren't just blueprints for objects; they are tools for encapsulation. By default, every member of a class is public, meaning any part of your app can reach in and touch it. As you saw in my payment example, this is often a recipe for disaster. That's where access modifiers come in.
When you mark a property as private, it's only accessible within the class it was defined in. If you try to access a private member from an instance outside the class, TypeScript will throw a compile-time error. Now, you might wonder, "What if I want a child class to be able to see the property, but still keep it hidden from the rest of the world?" That's exactly why we have protected. It's a middle ground: it keeps the member private to the outside world but allows subclasses to inherit and use it.
class BankAccount {
public accountHolder: string;
private balance: number;
protected accountType: string;
constructor(holder: string, initialBalance: number, type: string) {
this.accountHolder = holder;
this.balance = initialBalance;
this.accountType = type;
}
public getBalance() {
return `Your balance is $${this.balance}`;
}
}
class SavingsAccount extends BankAccount {
public getAccountInfo() {
// This works because accountType is protected
return `This is a ${this.accountType} account for ${this.accountHolder}`;
}
}
const myAcc = new BankAccount("Alice", 1000, "Checking");
console.log(myAcc.accountHolder); // OK
// console.log(myAcc.balance); // Error: Property 'balance' is private
Cutting the Boilerplate with Parameter Properties
If you've written a lot of classes, you've probably noticed a tedious pattern: you declare a property, then you pass it into the constructor, and then you assign it using this.property = property. It's a lot of typing for very little value. TypeScript gives us a shorthand called "Parameter Properties" that I use in almost every project.
By adding an access modifier (like private or public) directly to the constructor argument, TypeScript automatically declares the property and assigns the value for you. It's a massive quality-of-life improvement.
// The long way
class User {
private username: string;
constructor(username: string) {
this.username = username;
}
}
// The TypeScript way (does exactly the same thing)
class UserShort {
constructor(private username: string) {}
}
Preventing Accidental Overwrites with Readonly
Sometimes a property needs to be public—you want the rest of the app to be able to read it—but you don't want anyone to change it after the object has been created. This is where the readonly modifier shines. It's not an access modifier in the sense of visibility, but it's a powerful constraint on mutability.
I typically use readonly for IDs, configuration settings, or any "source of truth" that should be set once at instantiation and never touched again. If you try to reassign a readonly property, TypeScript will stop you dead in your tracks.
class Employee {
constructor(public readonly employeeId: number, public name: string) {}
}
const emp = new Employee(12345, "Bob");
console.log(emp.employeeId); // OK
// emp.employeeId = 67890; // Error: Cannot assign to 'employeeId' because it is a read-only property.
📋 Practical Task
Exercise: Secure Vault Management System
You need to build a Vault class that mimics a secure storage system. The requirements are as follows:
- The vault must have a
readonlyproperty calledvaultId(a string) that is set during initialization. - The vault must have a
privateproperty calledsecretCode(a string) that cannot be accessed from outside the class. - The vault should have a
protectedproperty calledsecurityLevel(a number) that can be accessed by subclasses. - Implement a public method
unlock(inputCode: string): boolean. This method should returntrueif theinputCodematches thesecretCode, andfalseotherwise. - Create a subclass called
HighSecurityVaultthat extendsVault. This subclass should have a method calledgetSecurityReport()that returns a string mentioning thesecurityLevel.
Test your implementation: Create an instance of HighSecurityVault, attempt to change the vaultId (it should fail), attempt to access the secretCode directly (it should fail), and verify that unlock() and getSecurityReport() work as expected.
There are no comments for now.