typescriptdecoratortypescript-decoratorreflect-metadatatsyringe

tsyringe container.resolve creates a new instance on call in custom decorator


I'm trying to create a custom decorator for tsyringe to inject through properties.

My code:

import '@abraham/reflection';
import {container} from 'tsyringe';

/**
 * Inject class through property
 *
 * @param targetClass
 * @returns PropertyDecorator
 */
export const inject = (targetClass: new (...args: any[]) => any): PropertyDecorator => {
    if (!targetClass || targetClass === undefined) {
        throw new Error(`${targetClass} class is undefined`);
    }

    return (target: any, propertyKey: string | symbol): PropertyDecorator => {
        return Object.defineProperty(target, propertyKey, {
            get: () => container.resolve((targetClass))
        });
    };
};

Service

import { injectable } from 'tsyringe';

@injectable()
export class Test {
    constructor() {
        console.log('hi from test');
    }

    a(): void {
        console.log('a');
    }

    b(): void {
        console.log('b');
    }
}

Calling Class

import React, { ReactElement } from 'react';
import Head from 'next/head';
import { Test } from '../_service/test.service';
import { inject } from '../decorators/inject';

/**
 * @class Home
 */
export default class Home extends React.Component {
  @inject(Test)
  private readonly test!: Test;

  /**
   * Render Home
   *
   * @returns ReactElement<any>
   */
  render(): ReactElement<any>  {
    this.test.a();
    this.test.b();
    return (
      <div>
        <Head>
          <title>home</title>
        </Head>
        <main>
          <p>test</p>
        </main>
      </div>
    );
  }
}

Console Output

hi from test
a
hi from test
b

Current behavior: Everytime I call a method of my injected class, my constructor gets called again. So that means my class gets new initialized on every new call.

When I use following code everything seems to be working

import React, { ReactElement } from 'react';
import Head from 'next/head';
import { Test } from '../_service/test.service';
import { container } from 'tsyringe';

/**
 * @class Home
 */
export default class Home extends React.Component {
  private readonly test: Test = container.resolve(Test);

  /**
   * Render Home
   *
   * @returns ReactElement<any>
   */
  render(): ReactElement<any>  {
    this.test.a();
    this.test.b();
    return (
      <div>
        <Head>
          <title>home</title>
        </Head>
        <main>
          <p>test</p>
        </main>
      </div>
    );
  }
}

Console Output

hi from test
a
b

Any reasons why this is happening?


Solution

  • After a hour of sleep and a lot of coffee with some research.... I found the solution!

    I was trying to manipulate the getter instead of setting the property value. That resulted to the following code:

    /**
     * Inject dependency through property
     *
     * @param targetClass
     * @returns PropertyDecorator
     */
    export const inject = (targetClass: InjectionToken): PropertyDecorator => {
        if (!targetClass || targetClass === undefined) {
            throw new Error(`${targetClass} class is undefined`);
        }
    
        return (target: any, propertyKey: string | symbol): PropertyDescriptor | undefined => {
            if (!Reflect.deleteProperty(target, propertyKey)) {
                throw new Error(`Could not delete property ${String(propertyKey)} in class ${target.constructor.name}`);
            }
    
            const options: PropertyDescriptor = {
                value: container.resolve(targetClass)
            };
    
            if (!Reflect.defineProperty(target, propertyKey, options)) {
                throw new Error(`Could not define ${String(propertyKey)} property in class ${targetClass.toString()}`);
            }
    
            return Reflect.getOwnPropertyDescriptor(target, propertyKey);
        };
    };