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

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.defineProperty to set the property's writable attribute to false.
  • Apply this decorator to a userId property in a UserSession class.
  • Verify that attempting to change the userId after 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
Rating
0 0

There are no comments for now.

to be the first to leave a comment.