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
197: Parameter Decorators
I've spent a lot of time with method and class decorators, but parameter decorators always felt like a bit of a mystery when I first encountered them. They look like they should work the same way—intercepting something and changing it—but they behave very differently. Let's dive in and see why.
Wait, where did the value go?
My first instinct when seeing a parameter decorator was to think, "Great, I can use this to validate or transform an input before the method even runs." I tried to write a simple @Required decorator that would throw an error if a parameter was null. Here is what I tried first:
function Required(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter ${parameterIndex} of ${propertyKey} is marked as required.`);
// I thought I could do something here to "intercept" the value...
}
class UserService {
saveUser(@Required name: string) {
console.log(`Saving user: ${name}`);
}
}
const service = new UserService();
service.saveUser(null as any);
When I ran this, I saw the log from the decorator, but the saveUser method still executed with null. I didn't stop the execution, and I couldn't change the value. I felt like I was missing something obvious. I checked the types: the decorator receives the class prototype, the method name, and the index of the parameter. But it doesn't receive the actual value being passed in at runtime.
It's actually a labeling system
After digging through the spec, I realized my mistake. Parameter decorators are not "interceptors." They are metadata markers. They don't run when the method is called; they run when the class is defined. Their only real job is to say, "Hey, the third parameter of this specific method has this specific trait."
If you want to actually do something with that information, you have to pair the parameter decorator with a method decorator. The parameter decorator leaves a "breadcrumb," and the method decorator follows it.
Connecting the dots with Method Decorators
To make this actually useful, I need a way to store that metadata. In a real-world framework like NestJS or Angular, they use a library called reflect-metadata for this. Since we're keeping it lean, I'll just use a simple Map to track which parameters are marked as required.
Let's try this again. I'll create a @Required marker and a @Validate method decorator that checks those markers before letting the method run.
const REQUIRED_PARAMS = new Map();
function Required(target: any, propertyKey: string, parameterIndex: number) {
if (!REQUIRED_PARAMS.has(propertyKey)) {
REQUIRED_PARAMS.set(propertyKey, []);
}
REQUIRED_PARAMS.get(propertyKey).push(parameterIndex);
}
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredIndices = REQUIRED_PARAMS.get(propertyKey) || [];
for (const index of requiredIndices) {
if (args[index] === undefined || args[index] === null) {
throw new Error(`Parameter at index ${index} is required for method ${propertyKey}`);
}
}
return originalMethod.apply(this, args);
};
}
class UserService {
@Validate
saveUser(@Required name: string, @Required email: string) {
console.log(`Saving user ${name} with email ${email}`);
}
}
const service = new UserService();
try {
service.saveUser("Alice", null as any); // This should now fail!
} catch (e) {
console.error(e.message);
}
Now it clicks. The @Required decorator isn't the one doing the heavy lifting—it's just the "tag." The @Validate decorator is the "engine" that looks for those tags and enforces the logic. It's a powerful pattern for building declarative APIs where you describe what should happen via decorators, and a central piece of logic handles the how.
📋 Practical Task
Build a Parameter-Based Logger
Your goal is to create a system that selectively logs only specific parameters of a method call. You shouldn't log every argument—only the ones explicitly marked for logging.
- Create a parameter decorator called
@LogParam. It should record the index of the parameter in a way that can be accessed later (similar to theREQUIRED_PARAMSMap used in the lesson). - Create a method decorator called
@LogExecution. This decorator should intercept the method call, check which parameters were marked with@LogParam, and print their names and values to the console before executing the original method. - Apply these to a class
PaymentProcessorwith a methodprocessPayment(orderId: string, amount: number, currency: string). - Ensure that only the
orderIdandamountare logged, while thecurrencyremains private/unlogged.
There are no comments for now.