DEV Community

Poorshad Shaddel
Poorshad Shaddel

Posted on • Originally published at levelup.gitconnected.com on

Start Implementing Your Own Typescript Property Decorators


Property Decorator

What is a Decorator?

It is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors (reference).

What is a Property Decorator?

No specific definition for property decorators in Typescript Docs, This is what you can find there:

A _Property Decorator_ is declared just before a property declaration. A property decorator cannot be used in a declaration file, or in any other ambient context
Enter fullscreen mode Exit fullscreen mode

In one of the notes in the document you can find this which gives us more information:

A property decorator can only be used to observe that a property of a specific name has been declared for a class.
Enter fullscreen mode Exit fullscreen mode

A pattern that we are going to use is to set some metadata for the current object and use them in other functionalities.

What is going on behind the scene?

Your property decorator is actually a simple function that is called as a function at runtime and it gets two arguments:

  • Constructor of the class if the property is static, and class prototype if property is an instance member
  • name of the member

Did you see that we do not have access to property ? and also what we return in this function will be ignored! That’s why we said before that this decorator is just for observing.

Setup

In order to run the Typescript code, we need to compile them using the Typescript compiler.

We need a tsconfig.json file :

https://medium.com/media/61b31c67596e779a9eb1ec9fa70a056d/href

We have to enable the experimentalDecorators. Also, the target should not be less than ES5. Also use reflect-metadata types, otherwise you get some type errors.

If you do not want to use a tsconfig file you can pass these options directly:

tsc --experimentalDecorators // If you installed tsc globaly
npx tsc --experimentalDecorators // If you installed tsc in your current directory
Enter fullscreen mode Exit fullscreen mode

Now by running tsc in the current directory, the typescript files will compile to Javascript files and we can run them using Node. (reference)

Define Our Property Decorator

1- Example: Just Log the Arguments

Let’s first log the arguments in a simple example:

https://medium.com/media/65de09f420e753edcbe36a0c18c26013/href

We have a user with three members, one of the members is static: maxDailyUsage . Let’s see the logs after running this piece of code:

{ firstArgument: {}, propertyName: 'email' } // line 10
{
  firstArgument: [class User] { maxDailyUsage: 12 }, // line 13
  propertyName: 'maxDailyUsage'
}
Enter fullscreen mode Exit fullscreen mode

In the static case, we can see the User Class Constructor and also the initialization value. If we initialize the value of this static member somewhere else we will see undefined in our log.

For instance member, we can see the name of the property and class prototype which in the console it shows an empty object but class members are available in this object.

The next example is real usage.

2- Example: Censor Sensitive Data From User

In this example, we have a user, and it has some sensitive fields like password and card number. We want to censor some of the fields when someone is calling toString() function on the user object.

For this example we are using Reflection API to set metadata and read that metadata.

It is called Reflection because it reflects information about the objects .It is still not implemented in native Javascript, There is a proposal of adding decorators and reflect API for ES7. Since it is not supported, Typescript team built a polyfill for reflection API and now it is usable in Typescript by enabling experimental features.

Let’s see the implementation:

First thing we need to build is our property descriptor:

https://medium.com/media/382ced42e87c7cd839cb0ae2d5371d13/href

All we did in this descriptor was set metadata.

For setting metadata on objects, we have used Reflect.defineMetadata and we can pass 3 arguments to this function:

  • key : we used a pattern, and later we used the same pattern for getting metadata sensitive:${propertyName}
  • value : we set it to true to know that this property is sensitive or not.
  • target : This is the target object, and we are passing the first argument of the property decorator, which is the instance of the class.

Now we can easily use it with @sensitive before our fields.

Let’s see the whole implementation together:

https://medium.com/media/10ed7c147b3d56db1cf6dd497e4c6eab/href

Two fields are sensitive. We do not want to expose them in toString function, so we should check them. We are using Reflect.getMetadata in line 31 with the same pattern to check if it is a sensitive field or not.

const isSensetiveField = Reflect.getMetadata(`sensitive:${iterator}` , this)
Enter fullscreen mode Exit fullscreen mode

If the field is not sensitive, we are not adding it to userJSON object, and as a result, these fields are not exposed. Let’s see the log result:


Log result

As we expected fields with @sensitive decorator are not exposed.

*** It is important to understand that we did our logic in another method, we used the property decorator to set metadata, and later we have used that metadata. We did not have access to property value in the property decorator .***

Is there a way to get rid of this pattern we used sensitive:${iterator} ?

Yes, both functions we have used have another overload to define metadata for specific property and not for the whole object. Let’s see the implementation :

https://medium.com/media/09ada9be13d84d2ddde362654415d024/href

Function defineMetadata now has 4 arguments, and the last one is the property name. Also, the key does not need to be unique across the whole object, it just needs to be unique for this property.

Function getMetadata now has 3 arguments, and the last one is key which is the property name.

3- Explanation of Typescript document example

https://medium.com/media/d7d5e362ce4653dd1d954ba488053663/href

Why it is hard to understand the property decorator from this example?

You need to know some concepts before jumping to this example.

1- What is Symbol? Symbol is a built-in object whose constructor returns a symbol primitive — also called a Symbol value or just a Symbol — that’s guaranteed to be unique (reference)

2- Why the decorator function argument is only one and it is a string? This is because this function is not a decorator, it just creates a decorator_._ Reflect.metadata(…) itself returns a function that could be used as our decorator. These functions are called decorator factory , since they can be used to create decorators. In this case, it returns a function that has these arguments: (target: Object, propertyKey: string | symbol) .

3- How is the same key used for setting metadata? Is it working on different fields? Yes it is working, the reason is that it used Reflect.metadata and it automatically defines that metadata only for that specific property. The function we were using also has another overload for defining metadata only for that property. When we want to get metadata in this case we are going to use another overload of function getMetadata and we can pass property key that has this metadata.

I think with these answers the problem is like the same we solved in part 2.

Conclusion

Property descriptor only gives us the ability to know that this decorator is used on that specific field, we do not have access to field value, and we cannot mutate it(there are some cases that we can mutate field value but it can result in buggy code, check out this article and the comments).

One way to use it is to set metadata and consume that metadata somewhere else.

Another example of usage is to define some functions based on the properties. Check this article for that.

If you want to know more about decorators, you can check out my other articles:

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Top jobs for software engineers


Top comments (0)