Skip to Content
Course content

198: Decorator Factories

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

Up until now, we've looked at decorators as simple functions that modify a class or a method. They work great for binary switches—either you want the behavior or you don't. But in the real world, you usually need a bit more control. You don't just want to "log" a method; you want to log it at a specific level, or perhaps you want to validate a property against a specific maximum value.

This is where Decorator Factories come in. Essentially, a factory is just a function that returns a decorator. It adds an extra layer of wrapping, which allows us to pass arguments into the decorator itself.

Designing a configurable logger

Let's build something practical. I want a @Log decorator that I can use on methods, but I want to be able to specify whether the log is 'INFO', 'WARN', or 'ERROR'. If I just used a standard decorator, I'd have to write three different decorators: @LogInfo, @LogWarn, and @LogError. That's a maintenance nightmare.

Instead, I'll write a factory. Here is how I'm thinking about the structure: the outer function takes the configuration, and the inner function is the actual decorator that TypeScript expects.

type LogLevel = 'INFO' | 'WARN' | 'ERROR';

function Log(level: LogLevel) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      console.log(`[${level}] Calling ${propertyKey} with args:`, args);
      return originalMethod.apply(this, args);
    };
  };
}

Where I tripped up

When I first implemented this in a previous project, I made a classic mistake. I tried to use the factory like a regular decorator. I wrote @Log instead of @Log('INFO'). TypeScript gave me a cryptic error about the decorator not being a function, or rather, that the return type didn't match what the compiler expected for a method decorator.

I had to remind myself: Log isn't the decorator. Log is the factory. It returns the decorator. If you omit the parentheses, you're passing the factory function itself to the class, and TypeScript is left wondering why you're trying to decorate a method with a function that requires a LogLevel argument.

Always remember: if your decorator has parentheses () when you apply it, you're using a factory.

Applying the factory to a Service

Now that the factory is solid, let's see it in action. I'll create a simple UserService. I want the create method to be an INFO log, but the delete method should probably be a WARN log because it's a destructive action.

class UserService {
  @Log('INFO')
  create(name: string) {
    console.log(`User ${name} created in database.`);
  }

  @Log('WARN')
  delete(id: number) {
    console.log(`User ${id} deleted from database.`);
  }
}

const service = new UserService();
service.create('Alice'); 
// Output: [INFO] Calling create with args: ['Alice']
// Output: User Alice created in database.

service.delete(42);
// Output: [WARN] Calling delete with args: [42]
// Output: User 42 deleted from database.

By wrapping the decorator in a factory, we've turned a rigid piece of metadata into a flexible tool. We can now pass any configuration we want into our decorators without duplicating the core logic of the decorator itself.




📋 Practical Task

Build a Role-Based Access Control (RBAC) Decorator Factory

Your task is to create a decorator factory called @Role. This factory should take a string argument representing the required role (e.g., 'admin', 'editor', 'guest').

Implement the following requirements:

  • Create a User class that has a role property.
  • The @Role(requiredRole: string) factory should return a method decorator.
  • The resulting decorator should intercept the method call and check if the User instance's role matches the requiredRole.
  • If the roles match, execute the method.
  • If the roles do not match, log an error message: "Access Denied: Required role [role] but user is [userRole]" and do not execute the method.

Test your implementation with a method called deleteDatabase() that requires the 'admin' role, and try calling it with a user who has the 'guest' role.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.