## Decorators and Dynamic Behavior (Typescript)
🔹 *parent* [[⧋ Typescript|Typescript]]
▫️ *prev* [[⏶ Basic types|Basic types]], [[⏶ Union types|Union types]] *next* [[Module basics|Module basics]]
### Decorators Overview
>[!warning]
>The specification for this is still in development and may be added to the official JS language soon, to double-check exact usage.
Decorators can be thought of as metadata that can be added to classes, methods, and getter/setter properties.
For instance, you can log when a class is accessed and add authorization to a method for certain users.
```ts
@log // creates log
class ContactRespository {
private contacts: Contact[] = []
@authorize("ContactViewer") // limits access to ContactViewer
getContactById(id:number): Contact | null {
const contact = this.contacts.find(x => x.id === id);
return contact
}
@authorize("ContactEditor") // different access
save(contact: Contact): void {
//something
}
}
```
### Configuring decorator support
>[!info]
> In order for this to work, **tsconfig.json** must have compiler option set.
>
In **tsconfig.json**
```json
{
"compilerOptions": {
"target": "esnext",
"noEmit": true,
"experimentalDecorators": true, // This sets decorator options ON
"emitDecoratorMetadata": true // not required but helps
},
"include": ["src/**/*"]
}
```
And install `reflect-metadata` library - implements polyfills for experimental features that will help decorators work.
```zsh
npm i reflect-metadata --save
```
### Method decorators
Best way to implement a method decorator is to create a function like`authorize` to setup a method decorators. It takes three arguments:
- Target: The object the decorator is being applied to (i.e., the instance that the object that the method belongs to)
- Property: The name of the property that the decorator is being applied to
- Descriptor: current metadata about the property.
```ts
function authorize(target:any, property:string, descriptor:PropertyDescriptor){
}
```
The descriptor can be changed a couple of different ways.
Editing in place:
```ts
//editing in place
function authorize(target:any, property:string, descriptor:PropertyDescriptor){
// by editing property in place
descriptor.value = function() {
}
// by returning as PropertyDescriptor
return {
// ... make changes here
} as PropertyDescriptor
}
```
This can be used to check authentication for a user.
```ts
//editing in place
function authorize(target:any, property:string, descriptor:PropertyDescriptor){
// make a copy of the original property
const wrapped = descriptor.value
descriptor.value = function() {
if (!currentUser.isAuthenticated()) {
throw Error("User is not authenticated")
}
// call that copy using apply() - this logs every success
return wrapped.apply(this, arguments)
// Alternately, it can also be wrapped in a try/catch block
try {
return wrapped.apply(this,arguments);
} catch (ex) {
// TODO: some fancy logging logic here
throw ex;
}
}
}
```
Once that is set, the decorator can be used simply by calling `@authorize`
```ts
class ContactRespository {
private contacts: Contact[] = [];
@authorize
getContactById(id:number): Contact | null {
if (!currentUser.isInRole("ContactViewer")) {
throw Error("User not authorized to use this action");
}
const contact = this.contacts.find(x => x.id === id);
return contact;
}
}
```
In order to create a function with custom parameters, the original function must be modified to be a *decorator factory* by wrapping it in another function:
```ts
function authorize(role: string) {
return function authorizeDecorator(target:any, property:string, descriptor:PropertyDescriptor){
const wrapped = descriptor.value;
descriptor.value = function() {
if (!currentUser.isAuthenticated()) {
throw Error("User is not authenticated")
}
// pass role argument in parent function
if (!currentUser.isInRole(role)) {
throw Error(`User not in role ${role}`);
}
return wrapped.apply(this, arguments);
}
}
}
```
### Class decorators
Freeze a class to prevent any changes.
```ts
// using the argument 'constructor' makes it obvious how it's used
function freeze(constructor: Function) {
// freeze needs to be added to both constructor and prototype
Object.freeze(constructor)
Object.freeze(constructor.prototype)
}
// instead of using any
// function singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
function singleton(constructor: any) {
return class Singleton extends constructor {
static _instance = null;
constructor(...args) {
super(...args);
if(Singleton.instance)
{
throw Error("Duplicate instance")
}
Singleton._instance = this
}
}
}
// singleton prevents duplication
@singleton
class ContactRespository {
// add to constructor function
@freeze
constructor() {
}
}
```
Adding `@singleton` decorator with generic type prevents compilation errors.

### Property Decorators
You add this to the property of a class in the same way you would for classes and methods.
```ts
@freeze
@singleton
class ContactRespository {
// logs to console anytime the value of this property is changed
@auditable
private contacts: Contact[] = [];
}
```
Property decorators accept to arguments: the *target* and the *property name*
```ts
function auditable(target: object, key: string | symbol) {
// get the initial value, before the decorator is applied
let val = target[key];
// then overwrite the property with a custom getter and setter
Object.defineProperty(target, key, {
get: () => val,
set: (newVal) => {
// log a message anytime a new property is set
console.log(`${key.toString()} changed: `, newVal);
val = newVal;
},
enumerable: true,
configurable: true
})
}
```
It's important to understand how the `Object.defineProperty()` method works.
The static method `Object.defineProperty()` defines a new property directly on an object, or modifies an existing property on an object, and returns the object.
```js
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
object1.property1 = 77;
// throws an error in strict mode
console.log(object1.property1);
// expected output: 42
```
### References
*see* [[⧋ Typescript#References|Typescript References]]