Skip to Content
Course content

196: Property Decorators

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

I see this all the time: developers coming into TypeScript think a property decorator is like a piece of middleware for a variable. They assume that if they put @Log above a property, the decorator function will run every single time that property is read or written to. It feels intuitive, right? But it's completely wrong.

The 'Middleware' Myth: Thinking Decorators Intercept Every Access

Let me show you why that assumption fails. Look at this snippet. I'm trying to create a decorator that logs whenever a property is accessed:

function Log(target: any, propertyKey: string) {
    console.log(`Decorator initialized for ${propertyKey}`);
}

class User {
    @Log
    name: string = "Alice";
}

const user = new User();
console.log(user.name); // Expected: "Decorator initialized..." and "Alice"

If you run this, you'll notice something annoying: "Decorator initialized for name" prints exactly once—when the class is defined—not when you access user.name. The decorator function isn't a wrapper around the value; it's a setup function that runs during the class declaration phase. Once the class is built, the decorator is gone, and user.name is just a plain old string. I've spent way too many hours debugging this exact misunderstanding in the past.

Using Property Descriptors to Actually Control Data

If you actually want to intercept a property, you can't just "be" a decorator; you have to use the decorator to redefine how the property works on the prototype. We do this using Object.defineProperty. This allows us to swap a simple value for a getter and a setter.

Let's build something useful: a @Required decorator that throws an error if you try to set a property to null or undefined. To make this work, we have to store the actual value in a "private" variable (usually by prefixing the property name) and use the decorator to manage the access.

function Required(target: any, propertyKey: string) {
    const privateKey = `_${propertyKey}`;

    Object.defineProperty(target, propertyKey, {
        get() {
            return this[privateKey];
        },
        set(newValue: any) {
            if (newValue === null || newValue === undefined) {
                throw new Error(`Property ${propertyKey} cannot be null or undefined.`);
            }
            this[privateKey] = newValue;
        },
        enumerable: true,
        configurable: true
    });
}

class Profile {
    @Required
    username: string;

    constructor(name: string) {
        this.username = name;
    }
}

const myProfile = new Profile("dev_master");
myProfile.username = null; // This will now throw the error we defined!

Notice what happened here. The Required function ran once at the start. But instead of just logging something and leaving, it reached into the class prototype and said, "Whenever someone asks for username, run this getter. Whenever they try to change it, run this setter."

A quick word of caution: because we are creating a new property (like _username) on the fly, TypeScript's type system doesn't actually "know" about that hidden property. It's a bit of a hack, but it's the only way to achieve true property interception using this specific decorator pattern. It's powerful, but use it sparingly—overusing decorators can make your code feel like magic, and "magic" is usually a nightmare to debug six months later.




📋 Practical Task

Implementing a Numeric Ceiling Constraint with Property Decorators

Your task is to create a property decorator called @MaxLimit. This decorator should take a numeric argument representing the maximum allowed value for a property. If a user attempts to set the property to a value higher than this limit, the setter should automatically cap the value at the limit rather than throwing an error.

Requirements:

  • Create a decorator factory MaxLimit(limit: number).
  • Use Object.defineProperty to implement a getter and setter.
  • The setter must check if the new value exceeds the limit; if it does, it should assign the limit value instead.
  • Test it using a Product class with a price property capped at 1000.
// Your code here
Rating
0 0

There are no comments for now.

to be the first to leave a comment.