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
196: Property Decorators
I see this all the time: developers coming into TypeScript think a property decorator is like a piece of middleware for a variable. They assume that if they put @Log above a property, the decorator function will run every single time that property is read or written to. It feels intuitive, right? But it's completely wrong.
The 'Middleware' Myth: Thinking Decorators Intercept Every Access
Let me show you why that assumption fails. Look at this snippet. I'm trying to create a decorator that logs whenever a property is accessed:
function Log(target: any, propertyKey: string) {
console.log(`Decorator initialized for ${propertyKey}`);
}
class User {
@Log
name: string = "Alice";
}
const user = new User();
console.log(user.name); // Expected: "Decorator initialized..." and "Alice"
If you run this, you'll notice something annoying: "Decorator initialized for name" prints exactly once—when the class is defined—not when you access user.name. The decorator function isn't a wrapper around the value; it's a setup function that runs during the class declaration phase. Once the class is built, the decorator is gone, and user.name is just a plain old string. I've spent way too many hours debugging this exact misunderstanding in the past.
Using Property Descriptors to Actually Control Data
If you actually want to intercept a property, you can't just "be" a decorator; you have to use the decorator to redefine how the property works on the prototype. We do this using Object.defineProperty. This allows us to swap a simple value for a getter and a setter.
Let's build something useful: a @Required decorator that throws an error if you try to set a property to null or undefined. To make this work, we have to store the actual value in a "private" variable (usually by prefixing the property name) and use the decorator to manage the access.
function Required(target: any, propertyKey: string) {
const privateKey = `_${propertyKey}`;
Object.defineProperty(target, propertyKey, {
get() {
return this[privateKey];
},
set(newValue: any) {
if (newValue === null || newValue === undefined) {
throw new Error(`Property ${propertyKey} cannot be null or undefined.`);
}
this[privateKey] = newValue;
},
enumerable: true,
configurable: true
});
}
class Profile {
@Required
username: string;
constructor(name: string) {
this.username = name;
}
}
const myProfile = new Profile("dev_master");
myProfile.username = null; // This will now throw the error we defined!
Notice what happened here. The Required function ran once at the start. But instead of just logging something and leaving, it reached into the class prototype and said, "Whenever someone asks for username, run this getter. Whenever they try to change it, run this setter."
A quick word of caution: because we are creating a new property (like _username) on the fly, TypeScript's type system doesn't actually "know" about that hidden property. It's a bit of a hack, but it's the only way to achieve true property interception using this specific decorator pattern. It's powerful, but use it sparingly—overusing decorators can make your code feel like magic, and "magic" is usually a nightmare to debug six months later.
📋 Practical Task
Implementing a Numeric Ceiling Constraint with Property Decorators
Your task is to create a property decorator called @MaxLimit. This decorator should take a numeric argument representing the maximum allowed value for a property. If a user attempts to set the property to a value higher than this limit, the setter should automatically cap the value at the limit rather than throwing an error.
Requirements:
- Create a decorator factory
MaxLimit(limit: number). - Use
Object.definePropertyto implement a getter and setter. - The setter must check if the new value exceeds the
limit; if it does, it should assign thelimitvalue instead. - Test it using a
Productclass with apriceproperty capped at 1000.
// Your code here
There are no comments for now.