TypeScript Decorators: A complete guide

TypeScript Decorators: A complete guide

Decorators are awesome features, Many libraries are made out of decorators like React and Angular. These are an awesome concept

In this article, we are going to learn about typescript decorators.

What are TypeScript Decorators?

Decorators are basically, just functions at their core.

With decorators, you can apply re-useable behaviours to classes, methods, and properties. Decorators are inspired by decorators in other languages like Java and Python

Dead Simple Chat offers Javascript Chat API and SDK to add in-app chat to your React applications in minutes. Dead Simple Chat is a highly customizable chat solution and can be used for any chat use case.

Types of Decorators

These Decorators can apply to

  1. Class

  2. Method

  3. Class Property

  4. Accessor and

    1. Method Parameter

      Class decorator

      Class decorator is applied to the constructor of a class and can be used to observe, modify and change a class definition

      example of a class decorator

       type ClassDecorator = <TFunction extends Function>
         (target: TFunction) => TFunction | void;
      
      1. Params

        target The constructor of the class

      2. Returns

        If the decorator returns a value. It replaces the class definition and it is a way to extend class definition with new properties and methods

        Let us look at this with the help of an example

 function TechClass(constructor: Function) {
    console.log(`Class Name: ${constructor.name}`);
  }

  @TechClass
  class CoolClass {
    constructor() {
      console.log('New Class instance has beed created');
    }
  }

2. Method Decorators

Method decorators are used for methods, they can change and replace or observe the definition of a function

Let us learn more about method decorators with an example:

function Techfunc(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const realMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`function Name: ${propertyName}`);
    return realMethod.apply(this, args);
  };
}

class CoolClass {
  @Techfunc
  testFunc() {
    console.log('from testFunc');
  }
}

const instance = new CoolClass();
instance.testFunc();

Here the techfunc decorator logs the functions name to the console. the TestFunc method of the CoolClass triggers the TechFunc decorator which in turn logs the function name to the console

let us look at another example

function CoolLogs(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = function (...args) {
    console.log('params: ', ...args);
    const result = original.call(this, ...args);
    console.log('const result: ', result);
    return result;
  }
}

class C {
  @CoolLogs
  add(a: number, b:number ) {
    return a + b;
  }
}

const c = new C();
c.add(1, 2);
// -> params: 1, 2
// -> result: 3

Here we have a CoolLogs function that serves as a decorator for the class named c

Our decorator takes 3 parameters

  • target

  • propertyKey

  • descriptor

Inside the const original the original value of the function is stored.

then the original function is replaced by another function that takes a number of arguments

then it calls the new function with the same arguments

Property Decorators

Property descriptors can be used to change or observe a properties property definition

Dead Simple Chat offers Javascript Chat API and SDK to add in-app chat to your React applications in minutes. Dead Simple Chat is a highly customizable chat solution and can be used for any chat use case.

for example

function TestProperty(target: any, propertyName: string) {
  console.log(`Name of the Property: ${propertyName}`);
}

class MyClass {
  @TestProperty
  CoolProperty: string;

  constructor() {
    this.CoolProperty = 'Aweseome, we are so happy';
  }
}

Here the TestProperty defines the TestProperty decorator that observes a ClassProperty with the type string

whenever a new class instance is created the Test property logs the name of the property to the console

Parameter Decorator

Parameter decorators are used to watch and change the functional or methods parameters

let us look at the parameter decorator with an example

function TestParameter(target: any, propertyName: string, CoolParameter: number) {
  console.log(`Cool Parameter: ${CoolParameter}`);
}

class SomeClass {
  exampleMethod(@TestParameter saySomething: string) {
    console.log(`from exampleMethod: ${saySomething}`);
  }
}

const instance = new SomeClass();
instance.exampleMethod('Hello');

Here we have a function TestParameter that console.logs the CoolParameter parameter

and then we have SomeClass with a method named exampleMethod. When the exampleMethod is called the decorator is triggered and the TestParameter function is called which then logs the CoolParameter to the console

Accessor Decorators

Accessor decorators are like method decorators but the only difference is that the Method decorators have keys in their descriptors like

  • Value

  • writable

  • configurable

  • enumerable

    The descriptor in the Accessor decorators has keys

    • get

    • set

    • enumerable

    • configurable

The accessor decorators accept the follow theee parameters

  1. Constructor function or target prototype

  2. property name

  3. property descriptor

  • Dead Simple Chat offers Javascript Chat API and SDK to add in-app chat to your React applications in minutes. Dead Simple Chat is a highly customizable chat solution and can be used for any chat use case.

  • When to use Decorators

    1. Before/After Hooks

    2. Watch property changes and method calls.

    3. Transform parameters

    4. Add extra method or properties

    5. Runtime type validation

    6. Auto serialization and deserialization

    7. Dependency Injection

1. Before /After Hooks

Before / After hooks means the decorator is called before and after the function is called which is quite useful in debugging, logging, and measuring performance

Let us look into it through an example

function ExampleOfBeforeAfterHookFunction(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Before: ${propertyName}`);
    const result = originalMethod.apply(this, args);
    console.log(`After: ${propertyName}`);
    return result;
  };
}

class SomeClass {
  @ExampleOfBeforeAfterHookFunction
  coolFunction() {
    console.log('Inside coolFunction');
  }
}

const instance = new SomeClass();
instance.coolFunction();

Here we have the ExampleOfBeforeAfterHookFunction function

where we are storing the originalMethod in the orignalMethod const

Then we replace the original method with a new function implement that takes n number of arguments using the ...args property

then we log a before property name and then we call the originalMethod function and then we log the after property name.

This is how you can implement the before / after hooks method

2. Watch property changes and method calls.

Watch is a property decorator that logs the property as well as the method calls of a decorated method

This is especially useful when debugging and performance monitoring.

Let us learn more about Watch property and how it works using an example

function see(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`function has been called: ${propertyName} with args:`, args);
    return originalMethod.apply(this, args);
  };
}

class SomeClass {
  myProperty: string;

  @see
  setProperty(value: string) {
    console.log('Set the value to ');
    this.myProperty = value;
  }
}

const instance = new SomeClass();
instance.setProperty('Awesome Great work Done');

3. Transform Parameters

In transform parameters function applies the transformation function to a specific parameter before the method is called

Let us learn more about transform parameters with the help of an example

function CoolFunc(SomeFunc: (arg: any) => any): ParameterDecorator {
  return (target: any, propertyKey: any, parameterIndex:any) => {
    const originalMethod = target[propertyKey];

    target[propertyKey] = function (...args: any[]) {
      args[parameterIndex] = SomeFunc(args[parameterIndex]);
      return originalMethod.apply(this, args);
    };
  };
}

class MyClass {
  myMethod(@CoolFunc((x) => x * 2) num: number) {
    console.log(`Transformed parameter: ${num}`);
  }
}

const instance = new MyClass();
instance.myMethod(5);

What is happening here

The method decorator see logs the names and arguments of the decorated method

When the method see is called in the instance of MyClass it logs the method name, and sets the property value of myproperty

4. Add extra method or property

You can add an extra method or property using the decorators. Here is how you can achieve this using an example

function AddMethod() {
  return function (constructor: Function) {
    constructor.prototype.AddMethod = function () {
      console.log('Added a new method the property by the decorator');
    };
  };
}

@AddMethod()
class SomeClass {}

const instance = new SomeClass();
(instance as any).AddMethod();

the code has a function called AddMethod which adds a function ot the prototype of the decorated class which in this case is SomeClass

When an instance of SomeClass is called the addMethod can be called on it. The add method logs a message to the console

5. Runtime Type Validation

Runtime type validation is ensuring the type of values of the parameters and variables and const and data used in the function during run time matches the values of the expected type

unlike static type validation which happens during software compilation the run time type validation happens when the function or programing is running thus it is helpful in validating external input data

Let us learn more about Runtime Type Validation using an example

function SomeFunc(target: any, propertyKey: string, parameterIndex: number) {
  const originalMethod = target[propertyKey];

  target[propertyKey] = function (...args: any[]) {
    if (typeof args[parameterIndex] !== 'number') {
      throw new Error(`An error has occoured ${parameterIndex}`);
    }
    return originalMethod.apply(this, args);
  };
}

class TestClass {
  calculateArea(@SomeFunc radius: number) {
    return Math.PI * radius * radius;
  }
}

const instance = new TestClass();

try {
  instance.calculateArea('this is NaN'); 
} catch (error) {
  console.error(error.message);
}

console.log(instance.calculateArea(6));

6. Auto serialization and deserialization

Auto serialization and deserialization refers to the practice of converting from one data type to another data type deserialization in order to store the data in a particular format

then converting again from one data type to the orignal data type for the purpose of rendering the data or other function can consume the data

Here is an example explaining how decorators can be used to do this

function SerializeFunc(constructor: Function) {
  constructor.prototype.serialize = function () {
    return JSON.stringify(this);
  };

  constructor.deserialize = function (json: string) {
    return new constructor(JSON.parse(json));
  };
}

function DeserializeFunc(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (serializedData: string) {
    const deserializedData = JSON.parse(serializedData);
    return originalMethod.apply(this, deserializedData);
  };
}

@SerializeFunc
class Person {
  constructor(public name: string, public age: number) {}

  @SerializeFunc
  greet(data: { name: string; age: number }) {
    return `Awesome, people call me ${data.name} my age is ${data.age} years old.`;
  }
}

const man = new Man('Jim Class', 43);
const serializedMan = man.serialize();

console.log(serializedMan); // {"name":"Jim Class","age":43}

const deserializedMan = (Man as any).deserialize(serializedMan);
console.log(deserializedMan.greet()); 

const serializedData = '{"name":"Don markell","age":56}';
console.log(man.greet(serializedData));

7. Dependency Injection

Decorators can also be used for dependency injection. Dependency injection is the practice of providing the functions and Objects that a method depends upon to it as parameter

this helps in decoupling the code and makes it cleaner

Decorators help in dependency injection by providing a way to inject dependencies directly into class constructors without changing class implementation

This makes them easier to maintain and test

Dead Simple Chat offers Javascript Chat API and SDK to add in-app chat to your React applications in minutes. Dead Simple Chat is a highly customizable chat solution and can be used for any chat use case.

Advantages of using decorators

  1. Cross-Cutting concerns

When parts of a program relay on many other parts of the program. Like a program might relay on other classes methods or properties then this could be a use case for decorators and they provide an easy way to maintain the code and provide improved readability

2. Dependency Injection

Dependency injection is a software design pattern where an object or a function that relies on other objects or functions receives then

This is like an inversion of control and results in a loosely coupled program and a code base that is modular

Here also decorators can in useful

3. Validation

Decorators can be used to validate inputs or the state of an Object.

4. Code organization

The code that doesn't belong in the main function or logic can be encapsulated using decorators thus making the code more readable and maintainable

Dead Simple Chat offers Javascript Chat API and SDK to add in-app chat to your React applications in minutes. Dead Simple Chat is a highly customizable chat solution and can be used for any chat use case.

Conclusion

In this article we have learnt how to use typescript decorators and we also have used a number of examples to make them easier to understand

I hope you find this article useful

Thanks for reading the article