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
198: Decorator Factories
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
Userclass that has aroleproperty. - The
@Role(requiredRole: string)factory should return a method decorator. - The resulting decorator should intercept the method call and check if the
Userinstance'srolematches therequiredRole. - 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.
There are no comments for now.