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
199: Decorator Composition Order
I've always found that the most confusing part of decorators isn't how to write the factory function, but understanding exactly when the code inside those decorators actually runs. If you're coming from a language where middleware is explicit, this "magic" can feel a bit opaque. To get a handle on it, we need to think of decorators as layers of an onion.
Creating a simple audit trail
Let's start by building a small system to track when methods are called. I'll create a @Log decorator. My goal here is simple: I want to see a message when a method starts and another when it finishes. In a real app, this would be your audit trail.
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[Log] Entering ${propertyKey}...`);
const result = originalMethod.apply(this, args);
console.log(`[Log] Exiting ${propertyKey}.`);
return result;
};
return descriptor;
}
class UserService {
@Log
saveUser(name: string) {
console.log(`Saving user ${name} to the database...`);
}
}
const service = new UserService();
service.saveUser("Alice");
// Output:
// [Log] Entering saveUser...
// Saving user Alice to the database...
// [Log] Exiting saveUser.
Adding a guard for input validation
Now, we rarely just log things. Usually, we need to make sure the data is actually valid before the method even touches it. I'll add a @Validate decorator that checks if the first argument is a non-empty string. If it's empty, I'll throw an error to stop the execution.
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!args[0] || args[0].trim() === "") {
throw new Error(`[Validate] Invalid argument provided to ${propertyKey}`);
}
return originalMethod.apply(this, args);
};
return descriptor;
}
Flipping the script to fix the execution flow
Here is where things get interesting. I'm going to apply both decorators to my saveUser method. I'll put @Log on top, and @Validate underneath it.
class UserService {
@Log
@Validate
saveUser(name: string) {
console.log(`Saving user ${name} to the database...`);
}
}
const service = new UserService();
try {
service.saveUser(""); // Testing with an empty string
} catch (e) {
console.error(e.message);
}
// Output:
// [Log] Entering saveUser...
// [Validate] Invalid argument provided to saveUser
Wait, look at that output. I've made a mistake in my architectural logic. My logs are recording that we "Entered" the method, but then the validation failed. In a high-traffic production system, my logs would be flooded with "Entering saveUser..." entries for requests that were fundamentally invalid. I don't want to log the start of a process if the input is garbage.
The rule for decorator composition is that they are evaluated from top to bottom, but the wrappers are applied from bottom to top. Essentially, the decorator closest to the method is the innermost wrapper. In the example above, @Log wrapped @Validate, which wrapped the saveUser method. When I called the function, I hit the @Log logic first.
To fix this, I need to move @Validate to the top. This makes @Validate the outermost layer of the onion.
class UserService {
@Validate // Outermost layer
@Log // Innermost layer
saveUser(name: string) {
console.log(`Saving user ${name} to the database...`);
}
}
const service = new UserService();
try {
service.saveUser("");
} catch (e) {
console.error(e.message);
}
// Output:
// [Validate] Invalid argument provided to saveUser
Now it's correct. The @Validate decorator catches the error and throws before the @Log wrapper is ever even invoked. If the validation passes, it then passes control down to the @Log wrapper, and finally to the actual method. Always remember: the top-most decorator is the first one to execute its wrapping logic.
📋 Practical Task
Refactoring the Order of the UserPermissions Guard
You are working on a system where methods are decorated with @CheckPermissions (which verifies if a user is an admin) and @CacheResult (which saves the method output to memory to avoid expensive DB calls).
Currently, the code looks like this:
class DocumentService {
@CheckPermissions
@CacheResult
async getSecureDocument(id: string) {
// Expensive DB call here
}
}
The team has noticed a security flaw: the @CacheResult decorator is returning cached data to users before the @CheckPermissions decorator even runs. This means a non-admin user can see a document if it was previously cached by an admin.
Your Task: Rewrite the DocumentService class to ensure that permissions are verified before the system attempts to retrieve a cached result. Explain why your change fixes the security leak based on the composition order.
There are no comments for now.