## 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. ![](https://i.imgur.com/aAmSFty.png) ### 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]]