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
195: Accessor Decorators
I've seen this exact bug happen a dozen times when developers first move from simple property decorators to accessor decorators. You want to add a bit of side-effect logic—like logging or validation—to a property, so you wrap the setter. It looks correct on paper, but the second you run it, the whole app crashes with a TypeError.
function LogChange(target: any, key: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set;
descriptor.set = function(value: any) {
console.log(`Changing ${key} to ${value}`);
// This looks fine, right?
originalSet(value);
};
}
class UserProfile {
private _username: string = "Guest";
@LogChange
set username(val: string) {
this._username = val;
}
get username() {
return this._username;
}
}
const user = new UserProfile();
user.username = "Alice"; // ❌ TypeError: Cannot read property '_username' of undefined
The disappearing 'this' context
If you try to run that code, you'll see the log message appear, but then it immediately crashes. The problem is a classic JavaScript gotcha: execution context. When we saved descriptor.set into the variable originalSet, we stripped it away from the UserProfile instance. When we call originalSet(value) inside our new function, this is no longer pointing to our user object; it's either undefined (in strict mode) or the global window object.
Since the original setter relies on this._username to actually save the data, it blows up because this is missing.
Restoring context with .call()
To fix this, we need to explicitly tell JavaScript which object this should refer to when the original setter runs. We do this using the .call()` method, passing this (the current instance) as the first argument.
function LogChange(target: any, key: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set;
descriptor.set = function(value: any) {
console.log(`Changing ${key} to ${value}`);
// We explicitly bind 'this' back to the instance
originalSet.call(this, value);
};
}
Now, when user.username = "Alice" is called, our decorator function executes, and originalSet.call(this, value) ensures that the original setter receives the user instance as its context. The value is saved, the log is printed, and the app doesn't crash.
How Accessor Decorators actually hook in
It's important to realize that accessor decorators are specifically designed for getters and setters. They differ from property decorators (which target a field) because they give you direct access to the PropertyDescriptor.
The PropertyDescriptor is essentially the "blueprint" for how that property behaves. It contains:
get: The function that runs when you read the property.set: The function that runs when you assign a value to the property.configurable: Whether the property can be deleted or changed.enumerable: Whether the property shows up duringfor...inloops.
I usually recommend accessor decorators when you need to intercept a value. If you just need to add metadata to a field (like @Required for a form), a property decorator is enough. But if you want to transform a value before it's saved, or prevent a value from being set based on some logic, you need the accessor decorator to wrap the get or set functions.
One quick tip: always check if descriptor.set or descriptor.get actually exists before trying to wrap them. If you accidentally put an accessor decorator on a property that only has a getter, trying to wrap the (non-existent) setter will throw an error before your code even starts running.
📋 Practical Task
Building a Non-Negative Price Validator
You are building an e-commerce system where product prices can never be negative. Instead of writing if (val < 0) inside every single setter in your codebase, you've been asked to create a reusable accessor decorator.
Your Task:
- Create a decorator named
@NonNegative. - This decorator should target a setter.
- If the value being set is less than 0, the decorator should
console.error("Price cannot be negative!")and not call the original setter. - If the value is 0 or greater, it should call the original setter using the correct
thiscontext. - Apply this decorator to the
pricesetter in theProductclass provided below.
class Product {
private _price: number = 0;
// Add your decorator here
set price(value: number) {
this._price = value;
}
get price() {
return this._price;
}
}
const shirt = new Product();
shirt.price = 25; // Should work
shirt.price = -10; // Should log error and NOT update the price
console.log(shirt.price); // Should still be 25
There are no comments for now.