DEV Community

Cover image for Using Property Decorators in Typescript with a real example
Dany Paredes
Dany Paredes

Posted on • Edited on • Originally published at danywalls.com

Using Property Decorators in Typescript with a real example

I was talking about class decorators in typescript in my previous post, today is time for properties decorators, how to define and use it for writing clean and elegant code.

What is Property Decorator

The property decorator is a function, applied to the property declaration in our classes.

It gets the constructor function of the class and the name of the property as parameters and with this information, we can do funny and cool things, like change the default definition or modify our object instance like an add new properties or change data.

class User {
 @MyDecorator
 password: string;
}
Enter fullscreen mode Exit fullscreen mode

How to create my property decorator.

I build the Min property decorator, it checks if the property has a minimum length, if not the object instance will have a new the errors' property with a message.

I will explain step by step, or you can scroll and read the full code, then let's go.

1- Declare the Min function as the decorator.

The decorator is a function, but because we use a factory, the Min function needs the limit number and returns another function that expects the Object and the property key.

function Min(limit: number) {
  return function(target: Object, propertyKey: string) {
Enter fullscreen mode Exit fullscreen mode

2- Define functions for the getter and setter.

We need to define 2 functions for handling when the user needs to read or set the value of the property with the decorator.

The getter returns the value of the property himself.

let value : string;
    //the getter of the property to return the value.
    const getter = function() {
      return value;
    };
Enter fullscreen mode Exit fullscreen mode

The setter gets the value of the property when using it and handles the validation.

 const setter = function(newVal: string) {

      if(newVal.length < limit) { 
//waiting for Object.define implementation.
      }
Enter fullscreen mode Exit fullscreen mode

3- Using the Object.defineProperty

The property error needs to be declared using Object.defineProperty a short story about object.defineProperty is it helps to define properties.

The defineProperty methods take 3 parameters: the instance of the object, the property name, and an object with the configuration like the value or the getter and setter.

 Object.defineProperty(target, 'errors', {
          value: `Your password should be bigger than ${limit}`
        });
Enter fullscreen mode Exit fullscreen mode

4- Redefine the property using Object.defineProperty and our functions.

The next step is the key, redefine the property with the decorator and set the getter and setter hooks to work with our logic.

 Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter
    });
Enter fullscreen mode Exit fullscreen mode

Done!, you have a clear overview of each statement, then feel free to read the full code.

function Min(limit: number) {
  return function(target: Object, propertyKey: string) { 
    let value : string;
    const getter = function() {
      return value;
    };
    const setter = function(newVal: string) {
       if(newVal.length < limit) {
        Object.defineProperty(target, 'errors', {
          value: `Your password should be bigger than ${limit}`
        });
      }
      else {
        value = newVal;
      }      
    }; 
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter
    }); 
  }
}

Enter fullscreen mode Exit fullscreen mode

The decorator is ready, the class User will have a password property then using the Min decorator, I set the password minimum length is 8.

class User {
    username: string;
    @Min(8)
    password: string;
    constructor(username: string, password: string){
        this.username = username;
        this.password = password;
    }    
}
Enter fullscreen mode Exit fullscreen mode

When the property password is set in the constructor, it calls our decorator and internal getter and setter hooks.

    let danyUser = new User("dany", "pass");
    console.log(danyUser);
    console.log(danyUser.errors);
Enter fullscreen mode Exit fullscreen mode

Because the password doesn't fit with the requirements, then the error property will be available and contains the value.

[nodemon] starting `node app.js`
User { username: 'dany' }
Your password should be bigger than 8
Enter fullscreen mode Exit fullscreen mode

That's it!

Hopefully, that will give you a bit of help with how and when using Property decorator in Typescript. If you enjoyed this post, share it!

Photo by Ferenc Almasi on Unsplash

Top comments (13)

Collapse
 
jwp profile image
John Peters

Very cool Dany! I recall many years ago when Java Spring came out and they talked about AOP at the time .NET didn't have anything close to it . Now we see this for .NET

All in all what you've shown here is really powerful. Thanks!

Collapse
 
marcela profile image
Marcel A

This does not work. All instances of the User class do have the same value for Password! Here is an example: TypeScript Playground

Collapse
 
navjot50 profile image
Navjot • Edited

There is a reason where every instance of User class is having the same value for the password. The article author is binding the properties to the target argument in the decorator function. According to the docs the target parameter in the decorator function is the prototype of the class (in this case User.prototype). So when the author did Object.defineProperty(target, propertyKey, { get: getter, set: setter }), then the property is being bound to the User.prototype object and not the current object. Since, the property is defined on the prototype it is available to all the instances of that class.

Collapse
 
ahuigo profile image
ahuigo • Edited

For es2022 or above, useDefineForClassFields is default to be true.
This causes that the property of instance(new User()) is independent from its User.prototype.

To make this example work only, we should set "useDefineForClassFields":false.

My example playground

Collapse
 
jascmiller profile image
Jamie Miller

Hi Marcel,

I came up for a solution with this problem. One should set a value on the object themselves using a unique symbol rather than a value that is accessed on the prototype.

function Min(limit: number) {
return function(target: Object, propertyKey: string) {
const symbol = Symbol();
Object.defineProperty(target, propertyKey, {
get: function() {
return this[symbol];
},
set: function(newVal: string) {
if(newVal.length < limit) {
Object.defineProperty(target, 'errors', {
value: Your password should be bigger than ${limit}
});
}
else {
this[symbol] = newVal;
}

}
});
}
}

Collapse
 
danywalls profile image
Dany Paredes

Thanks marcel for you feedback, the article was wrote one year and half, so I need take time for remmember, nowdays most of people use npmjs.com/package/reflect-metadata. If you found why is not working or someidea leave a message or link to share, When return from my holidays I promise update the article.

Collapse
 
jascmiller profile image
Jamie Miller

Hi Danny,

I came up for a solution with this problem. One should set a value on the object themselves using a unique symbol rather than a value that is accessed on the prototype.

function Min(limit: number) {
return function(target: Object, propertyKey: string) {
const symbol = Symbol();
Object.defineProperty(target, propertyKey, {
get: function() {
return this[symbol];
},
set: function(newVal: string) {
if(newVal.length < limit) {
Object.defineProperty(target, 'errors', {
value: Your password should be bigger than ${limit}
});
}
else {
this[symbol] = newVal;
}

}
});
}
}

Collapse
 
josethz00 profile image
José Thomaz

any solution for this issue?

Thread Thread
 
jascmiller profile image
Jamie Miller • Edited

Hi Jose,

I came up for a solution with this problem. One should set a value on the object themselves using a unique symbol rather than a value that is accessed on the prototype.

function Min(limit: number) {
return function(target: Object, propertyKey: string) {
const symbol = Symbol();
Object.defineProperty(target, propertyKey, {
get: function() {
return this[symbol];
},
set: function(newVal: string) {
if(newVal.length < limit) {
Object.defineProperty(target, 'errors', {
value: Your password should be bigger than ${limit}
});
}
else {
this[symbol] = newVal;
}

}
});
}
}

Collapse
 
snowfrogdev profile image
Philippe Vaillancourt

Could other decorators with a similar implementation be used along with this one? Something like this:

class User {
    username: string;
    @Min(8)
    @Max(12)
    @SpecialChar(2)
    password: string;
    constructor(username: string, password: string){
        this.username = username;
        this.password = password;
    }    
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
danywalls profile image
Dany Paredes

Yes, you can use nested decorators :D

Collapse
 
thekosmix profile image
Siddharth Kumar

with TS5.0, I'm getting below error:

Unable to resolve signature of property decorator when called as an expression. Argument of type 'ClassFieldDecoratorContext<User, string> & { name: "password"; private: false; static: false; }' is not assignable to parameter of type 'string'.ts(1240)

Anyone has been able to resolve it?

Collapse
 
bezael profile image
Bezael Pérez

Thank you so much for sharing!!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.