DEV Community

Ayanabilothman
Ayanabilothman

Posted on

Class Decorator in TypeScript πŸ‘‹

Welcome πŸ€— and first of all, I should mention that this article requires a background on TypeScript, TypeScript Generics and Object Oriented Programming.

Now, what is a decorator? πŸ€” In the real life, decorating something means to change its style of or its functionality. Have you ever changed the design of your room or purchased for an extensible sofa that can be converted into a bed πŸ˜…?
In the world of programming, a decorator is a design pattern, it is just a function that wraps another function to add or modify some features of it.

function decoratorFunc(fn) {
  //some code
  fn();
  //some other code
}
Enter fullscreen mode Exit fullscreen mode

The above function decoratorFunc is a decorator in its simplest form. You can pass any function and add more code to be executed either before or after the main function fn.

So, _what are the types of decoratorsin TypeScript?
_ πŸ€”

  1. class decorator
  2. method decorator
  3. accessor decorator
  4. property decorator
  5. parameter decorator

Which means that a decorator function can be attached to any of the above entities.

In this article we will dive deep into class decorator.
To apply decorators they must be prefixed with the @ symbol and placed immediately before the construct to be decorated.

We now know that the decorator is just a function, so to create a Class Decorator in TypeScript we just write a function, but it must have one argument which is the class itself. πŸ€“

function classDecorator(target: Function) {
  console.log(`This is to decorate class ${target.name}`);
}
Enter fullscreen mode Exit fullscreen mode

This is the simplest class decorator you will ever see πŸ˜‚.
The target parameter is the class constructor "the class that the decorator will wrap".

So let's create a class to make the example clear.

class User {
  constructor(
    public fName: string,
    public lName: string,
  ) {}

  fullName(): string {
    return `${this.fName} ${this.lName}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

To apply the classDecorator decorator to the User class we should write

@classDecorator
class User {
  constructor(
    public fName: string,
    public lName: string,
  ) {}

  fullName(): string {
    return `${this.fName} ${this.lName}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Because we prefix classDecorator with @ and place it before the User class , class User will be passed to the classDecorator.

If you try to execute it, you should see the logs we print inside the decorator.
But it is important to know that the decorator will get executed on the runtime not on the class instantiation.

And voila, you create your first class decorator πŸ₯³

What we have just written is a Decorator Function, what if we need to pass another argument to this function?!πŸ€” and it should take only one which is the class.

In this scenario we will need a Decorator Factory Function again, just a function the only difference that it can take any number of arguments and it returns the decorator function itself πŸ‘‡

function logCustomMessage(message: string) {
        return function (target: Function) {
            console.log(message)
    }
}
Enter fullscreen mode Exit fullscreen mode

and then we pass the argument on calling it. πŸ‘‡

@logCustomMessage('This is a custom message !')
class User {
  constructor(
    public fName: string,
    public lName: string,
  ) {}

  fullName(): string {
    return `${this.fName} ${this.lName}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Hmmm, can a decorator function return values? πŸ€”
Good question πŸ˜…
From the official documentation πŸ‘‡
If the class decorator returns a value, it will replace the class declaration with the provided constructor function.

A decorator function can only return classes

πŸ‘‰ Use Cases πŸ‘ˆ
Let's introduce two examples from which we can take advantage of the decorators and know how to return a class.

Assume we're building a system for a company, so we have structured the main classes, but we need to apply some features to this class that doesn't deeply relate to the logic of the class itself.

Example 1:
We create a Department class that has some members including the code of each department. This code is a unique identifier for each department and must be unique. So, we need to apply a validation feature away from the class logic to handle this case.

The decorator we can write to validate the code. πŸ‘‡

function uniqueCode<C extends new (...args: any[]) => {}>(target: C) {
  return class Mixin extends target{
    static codes: string[] = [];
    // the following constructor will get executed on creating object of the original class
    constructor(...args: any[]) {
      super(args);
      const code = args[1];
      if (Mixin.codes.includes(code)) {
        throw new Error(`Department code must be unique!`);
      }
      Mixin.codes.push(code);
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Department class πŸ‘‡

class Department {
  constructor(public name: string, public code: string) {}
}
Enter fullscreen mode Exit fullscreen mode

And to apply the decorator on the class πŸ‘‡

@uniqueCode
class Department {
  constructor(public name: string, public code: string) {}
}
Enter fullscreen mode Exit fullscreen mode

The uniqueCode decorator piece by piece πŸ‘‡

  • The function definition: we specify type of the argument target, that it will be custom type C which is restricted to new (...args: any[]) => {}. This means that it should be a constructor function that we call using new keyword, and this constructor function can have any numbers of arguments, then after calling it, it will return finally an object.

  • The function body: we just return a new class that extends the original class, to add more features, and there is the place we implement the validation feature.

Now if you try to execute πŸ‘‡

const hrDep = new Department("HR", "H259");
const marketingDep = new Department("Marketing", "H259");
Enter fullscreen mode Exit fullscreen mode

it will throw an error.

NOTE THAT the class we returned from the decorator will replace the original one. Try to execute console.log(Department.toString()) before and after applying the decorator to see the difference.

Example 2:
What if we need to reuse this decorator to be applied to the Employee class as well πŸ€”.
The dynamic information we need is to know the index of the code attribute in each class we will apply the decorator to.

By rewriting uniqueCode decorator and using Decorator Factory Function instead, we can pass extra arguments.

function uniqueCode(codeIndex: number) {
  return function <C extends new (...args: any[]) => {}>(target: C) {
    return class Mixin extends target {
      static codes: string[] = [];
      constructor(...args: any[]) {
        super(args);
        const code = args[codeIndex];
        if (Mixin.codes.includes(code)) {
          throw new Error(`${target.name} code must be unique!`);
        }
        Mixin.codes.push(code);
      }
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

Now we can pass the index of the code attribute according to its definition in the original class.

Applying the dynamic decorator to any class that has code attribute πŸ‘‡

// 1 is the index of code in the constructor parameters
@uniqueCode(1) 
class Department {
  constructor(public name: string, public code: string) {}

  info() {
    return {
      name: this.name,
      code: this.code,
    };
  }
}

// 2 is the index of code in the constructor parameters
@uniqueCode(2) 
class Employee {
  constructor(
    public name: string,
    public salary: number,
    public code: string
  ) {}
}

Enter fullscreen mode Exit fullscreen mode

And finally we are at the end πŸ˜„. You can implement great ideas using decorators and be safe away from the original logic of the class. Try it now πŸ™‚

Top comments (1)

Collapse
 
develekko profile image
Housam Ramadan

Amazing 😍