TypeScript Decorators: Property Decorators

This post takes an in-depth look at property decorators. It examines their signature, provides sample usage, and exposes a common antipattern. Reading the previous posts in the series is encouraged but not necessary.

Overview

a property decorator can only be used to observe that a property of a specific name has been declared for a class.

Property decorators ignore any return, underscoring their inability to affect the decorated properties. Similar to parameter decorators, property decorators can be used in tandem with other decorators to define extra information about the property. By themselves, their effectiveness is limited. Logging property data seems to be the best use for a property decorator by itself.

A widely used antipattern is to update a property descriptor on target in a property decorator. This wreaks havoc on all sorts of things. Instead, property decorators should set metadata that can be consumed elsewhere. Don't use them to do too much.

target: any

target is the object that owns the decorated property. target in the example is TargetDemo.

propertyKey: string | symbol

propertyKey is the name of the decorated property. It could also be a Symbol, depending on how the property is defined on the object. propertyKey in the example is foo.

Usage

As property decorators do not affect the underlying object, their primary use is to create and attach metadata. Consuming said metadata involves other decorators, so it's skipped here. The example below illustrates an easy way to attach metadata to properties.

// The metadata is not defined on the classconsole.log("Class property metadata:",Reflect.getMetadata(PROPERTY_METADATA_KEY,Demo),);// It's defined on an instanceconstdemo=newDemo();console.log("Instance property metadata:",Reflect.getMetadata(PROPERTY_METADATA_KEY,demo),);

Antipattern

Property decorators cannot modify the owning object as their return is ignored. Ergo any changes made on target are actually made globally. You might have seen this common property decorator example; it defines a property on target using the decorator's scope to maintain state (note that example will not work in strict mode). However, for a single class, the decorator is only called once. This means any instance of the class reuses the same decorator scope, essentially changing an instance property into a static property that can be updated.

Example

This will make more sense with an example. Rather than rehash the copypasta that pops up everywhere, I applied the logic to a (possibly?) common use-case. Many classes include numeric properties; many of those properties should not fall below a minimum value. We can erroneously solve this problem using something like the following.

// Pick a set of valuesfor(constnewValueof[-10,10,20]){// Create a new instanceconstdemo=newHasDecoratedProperty();// Add a basic linebreakif(newValue>-10){console.log("---");}// Log the current valueconsole.log("Current value:",demo.currentValue);// Update the valueconsole.log(`Attempting to set demo.currentValue = ${newValue}`);demo.currentValue=newValue;// Log the current valueconsole.log("Current value:",demo.currentValue);}// Add a basic linebreakconsole.log("---");// Create a new instanceconstdemo1=newHasDecoratedProperty();// Update its valueconsole.log("Setting demo1.currentValue = -10");demo1.currentValue=-10;console.log("demo1.currentValue:",demo1.currentValue);// Create a new instanceconstdemo2=newHasDecoratedProperty();// Update its valueconsole.log("Setting demo2.currentValue = 20");demo2.currentValue=20;// Compare the resultsconsole.log("demo1.currentValue:",demo1.currentValue);console.log("demo2.currentValue:",demo2.currentValue);

Attempting to use the decorator will present several issues. The first, demonstrated in the for loop, is that value recycles state even though we've created a new object. This is because the decorator is only run once, the first time the class is loaded (I think; if I'm wrong I'd love to know). The second, demonstrated with demo1 and demo2, shows that value is actually a singleton. Changing it in one instance changes it everywhere.

Solution

A full solution involves other decorators and is therefore outside the scope of this post (I'll add a link to the full example when I finish it). The gist of the solution is to combine class and property decorators, similar to combining parameter and method decorators. The property decorator sets metadata that the class decorator consumes.

Recap

Property decorators are great at adding extra information about properties at runtime. They can't do a whole more. Property decorators are often used in combination with other decorators to perform new actions at runtime.