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
43: Decorators
I see this all the time when developers move from Java or C# to TypeScript: they treat decorators as if they are "annotations." They assume that by slapping @Logged or @Authorized on a method, they are just tagging it with metadata that some other part of the framework will magically find and act upon later.
The "Metadata Tag" Misconception
If you think a decorator is just a label, you'll be confused when you try to actually implement one. You might write something like this, expecting the "tag" to somehow trigger a logging system automatically:
// This doesn't "do" anything on its own
function Log(target: any, key: string) {
console.log(`Method ${key} is now tagged for logging!`);
}
class UserService {
@Log
saveUser(user: any) {
console.log("Saving user to DB...");
}
}
In the example above, the Log function runs once—when the class is defined—not every time saveUser is called. If you were expecting a log entry every time you saved a user, you'll be disappointed. The decorator didn't "tag" the method for later; it just executed a function during the class setup and then vanished.
Decorators are actually Higher-Order Functions
To actually change the behavior of a method, you have to understand that a decorator is a function that receives the "descriptor" of the property it's decorating. To make something happen every time a method runs, you have to replace the original method with a new one that wraps the old one.
I like to think of it as an interceptor. You're not labeling the method; you're hijacking it. Here is how we actually implement a logger that works at runtime:
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// We save a reference to the original method
const originalMethod = descriptor.value;
// We replace the method with a new function
descriptor.value = function (...args: any[]) {
console.log(`---> Calling ${propertyKey} with arguments:`, args);
// We call the original method using .apply to keep the 'this' context
const result = originalMethod.apply(this, args);
console.log(`<--- ${propertyKey} returned:`, result);
return result;
};
}
class UserService {
@Log
saveUser(name: string, email: string) {
return `User ${name} (${email}) saved successfully.`;
}
}
const service = new UserService();
service.saveUser("Alice", "alice@example.com");
// Now it actually logs every time the method is invoked!
The "This" Context Trap
Notice that in my example, I used originalMethod.apply(this, args). This is a crucial detail that trips up almost everyone. If you used an arrow function for the replacement method, or if you called originalMethod(...args) without .apply(this), you would lose the instance of the class. Your this would become undefined, and as soon as your method tried to access a class property (like this.dbConnection), your app would crash.
A quick word of caution: decorators are still technically an "experimental" feature in TypeScript (though they've been used by Angular and NestJS for years). To make the code above work, you must have "experimentalDecorators": true enabled in your tsconfig.json. If you don't, the compiler will just stare at your @ symbols and throw a syntax error.
📋 Practical Task
Build a @ReadOnly Property Guard
In this exercise, you'll create a Class Property Decorator. Unlike the method decorator we built in the lesson, this one will target a property to make it immutable after the class is instantiated.
Your Task:
- Create a decorator called
@ReadOnly. - The decorator should use
Object.definePropertyto set the property'swritableattribute tofalse. - Apply this decorator to a
userIdproperty in aUserSessionclass. - Verify that attempting to change the
userIdafter the object is created throws an error (in strict mode) or is silently ignored.
Starter Code:
class UserSession {
@ReadOnly
userId: string = "user_12345";
userName: string = "DevUser";
}
const session = new UserSession();
session.userName = "NewName"; // Should work
session.userId = "user_99999"; // Should fail/be ignored
There are no comments for now.