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
194: Method Decorators
A few years ago, I was reviewing a PR from a junior dev who was trying to track down a performance bottleneck in our payment processing service. To find the slow spot, they had manually added console.time('methodName') at the start and console.timeEnd('methodName') at the end of about fifteen different methods. It worked, but the business logic was now buried under a mountain of boilerplate timing code. Every time they wanted to toggle the logging, they had to hunt through the whole file and comment out lines. It was a mess, and more importantly, it was fragile.
This is the exact problem method decorators were designed to solve. Instead of polluting your core logic with cross-cutting concerns—like logging, caching, or permission checks—you can "decorate" the method from the outside. It allows you to wrap the original function in a new one, giving you a hook to run code before and after the method executes without ever touching the method's internal body.
Intercepting the Execution Flow
When you write a method decorator, TypeScript gives you access to three things: the class prototype, the name of the method, and the PropertyDescriptor. The descriptor is the real prize here. It contains the value, which is the original function implementation. To modify the behavior, you save that original function to a variable and replace the descriptor's value with a new function.
function LogExecutionTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`--- Starting ${propertyKey} ---`);
const start = performance.now();
// We call the original method here
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`--- Finished ${propertyKey} in ${(end - start).toFixed(2)}ms ---`);
return result;
};
return descriptor;
}
class PaymentService {
@LogExecutionTime
processTransaction(amount: number) {
// Simulate a heavy API call
return new Promise(resolve => setTimeout(() => resolve(`Processed $${amount}`), 500));
}
}
In the example above, processTransaction doesn't even know it's being timed. The decorator intercepts the call, starts the timer, lets the original method do its work, and then logs the result. It's clean, reusable, and—most importantly—easy to remove if you no longer need it.
Preserving the 'this' Context
There is one major "gotcha" here that trips up almost everyone the first time: the this context. If you were to call originalMethod(...args) instead of originalMethod.apply(this, args), your code would likely crash the moment the method tried to access a class property. This is because the function is being detached from its instance.
By using .apply(this, args), you ensure that the original method still has access to the class instance it belongs to. I've seen developers try to use arrow functions for the wrapper, but be careful—arrow functions don't have their own this binding. For method decorators, a standard function declaration for the wrapper is usually your safest bet to keep the context intact.
One last thing: remember that decorators are still an experimental feature in TypeScript. You'll need "experimentalDecorators": true in your tsconfig.json for this to compile. It's a bit of a legacy holdover, but it's how the vast majority of the ecosystem (including frameworks like NestJS or Angular) handles this logic.
📋 Practical Task
Exercise: Building a Method-Level Permission Guard
You are building a banking application. Some methods in your AccountService should only be accessible to users with an 'admin' role. Instead of writing if (user.role !== 'admin') at the start of every single sensitive method, you're going to build a decorator to handle this.
Requirements:
- Create a
@RequiredRole(role: string)decorator factory (a function that returns a decorator). - The decorator should check a property called
currentUserRoleon the class instance. - If the
currentUserRolematches the required role, the method should execute normally. - If the roles do not match, the method should throw an Error:
"Unauthorized: Required role [role] not met." - Implement an
AccountServiceclass with acurrentUserRoleproperty and a methoddeleteAccount(id: string)decorated with@RequiredRole('admin').
Test your implementation: Set the currentUserRole to 'user' and attempt to call deleteAccount() to ensure it throws the error. Then change it to 'admin' and verify it succeeds.
There are no comments for now.