Skip to Content
Course content

197: Parameter Decorators

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 the REQUIRED_PARAMS Map 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 PaymentProcessor with a method processPayment(orderId: string, amount: number, currency: string).
  • Ensure that only the orderId and amount are logged, while the currency remains private/unlogged.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.