Decorator

Setup

To enable decorator, you need to use

{
  "compilerOptions": {
	...
    "experimentalDecorators": true,
  },
}

in tsconfig.json. Without it the value passed in the decorator function is not correct.

How to use

Constructor decorator

Constructor decorator are the decorator that was used for classes. For example:

import { Service } from "./dependencyInjection";


@Service
class Student {
    private name: string;

    constructor() {
        this.name = "some student";
    }

    getName() {
        return this.name;
    }
}

The @Service here is constructor decorator

To declare a constructor decorator, we use the following syntax:

export const Service = <T extends { new(...args: any[]): any }>(constructor: T)=> {
    dependencyManager.register(constructor.name, new constructor());
}

Field decorator

Field decorator takes in different arguments. For example:

import { Autowired } from "./dependencyInjection";

class MyClass {
    @Autowired
    private student: Student;

    method() {
        console.log("my method", this.student.getName())
    }
}

We can code it like this:

export const Autowired = (target: any, key: string) => {
    Object.defineProperty(target, key, {
        get: function() {
            // Search for className
            const instance = dependencyManager.resolve(Reflect.getMetadata('design:type', target, key).name);

            if (!instance) {
                throw new Error(`Instance ${key} is not autowired`)
            }

            return instance;
        },
    })
}

In here target will be the actual object, in this case the instance of MyClass and key will be student as the field name.

In here we use Reflect to get the type name of student which is Student since in Service we store it as class name.

To use this Reflect with meta-data type, we need to turn on

{
  "compilerOptions": {
	...
    "emitDecoratorMetadata": true
  },
}

And install reflect-metadata

Method decorator

import { Initialise } from "./dependencyInjection";
class School {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  @Initialise
  initialise() {
    console.log("test")
    return new School("test");
  }

  getName() {
    return this.name;
  }
}
export const Initialise = (
  target: any,
  key: any,
  descriptor: PropertyDescriptor,
) => {
  const instance = descriptor.value.apply();
  dependencyManager.register(instance.constructor.name, instance);
  return descriptor;
};

In here:

  • target will just be the class itself (School)
  • key will be the function name initialise
  • descriptor will have the descriptor.value which points to the original function.
    • We can call descriptor.value.apply() to trigger the original function

Complete Example

Complete Example of Dependency Injection in typescript using annotation:

dependencyInjection.ts

import "reflect-metadata";

class DependencyManager {
  container: Map<string, any>;

  constructor() {
    this.container = new Map();
  }

  register(token: string, instance: any): void {
    this.container.set(token, instance);
  }

  resolve<T>(token: string): T {
    return this.container.get(token);
  }
}

const dependencyManager = new DependencyManager();

export const Service = <T extends { new (...args: any[]): any }>(
  constructor: T,
) => {
  dependencyManager.register(constructor.name, new constructor());
};

export const Autowired = (target: any, key: string) => {
  Object.defineProperty(target, key, {
    get: function () {
      // Search for className
      const instance = dependencyManager.resolve(
        Reflect.getMetadata("design:type", target, key).name,
      );

      if (!instance) {
        throw new Error(`Instance ${key} is not autowired`);
      }

      return instance;
    },
  });
};

export const Initialise = (
  target: any,
  key: any,
  descriptor: PropertyDescriptor,
) => {
  const instance = descriptor.value.apply();
  dependencyManager.register(instance.constructor.name, instance);
  return descriptor;
};

index.ts

import { Autowired, Initialise, Service } from "./dependencyInjection";

@Service
class Student {
  private name: string;

  constructor() {
    this.name = "some student";
  }

  getName() {
    return this.name;
  }
}

class School {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  @Initialise
  initialise() {
    return new School("test");
  }

  getName() {
    return this.name;
  }
}

class MyClass {
  @Autowired
  private student: Student;

  @Autowired
  private school: School;

  method() {
    console.log("my method", this.student.getName());
    console.log("school", this.school.getName());
  }
}

const myClass = new MyClass();
myClass.method();

Will print out

> [email protected] start
> ts-node index.ts

my method some student
school test

learn/typescript/decorator at master ยท rockmanvnx6/learn (github.com)