thiagobustamante / typescript-ioc

A Lightweight annotation-based dependency injection container for typescript.
MIT License
526 stars 64 forks source link

Class cannot be invoked without 'new' for target ES6 of compilerOptions #2

Closed serebro closed 7 years ago

serebro commented 7 years ago

Hi Thiago,

When i tried to run this code with target es6 of compilerOptions I got the error TypeError: Class constructor SimppleInject cannot be invoked without 'new'

import {AutoWired} from "typescript-ioc";
@AutoWired
class SimppleInject {}
const instance: SimppleInject = new SimppleInject();

I use typescript 2.1.5.

tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./dist",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": [
    "src/**/*.ts",
    "gulp/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

Thanks for your IoC!

thiagobustamante commented 7 years ago

Hi,

Thanks for your feedback. I did not test it with target ES6 yet. But I will fix this as soon as possible.

thiagobustamante commented 7 years ago

It is fixed on 0.2.8 version

serebro commented 7 years ago

Very cool! Thanks.

thiagobustamante commented 7 years ago

Sorry, but I found another problem related.

with ES5, I can do this, when creating an object inside the container:

     IoCContainer.applyInjections(this, target);
     target.apply(this, args);            

Note that I apply all class injections before run the class constructor (the target on the given code).

This is nice because it allow me to write things like:

@AutoWired
class Test {
    @Inject private test2: Test2

     constructor() {
          console.log(this.test2);
     }
}

I can access the injected property test2 inside the constructor.

The problem is: ES6 classes does not allow this kind of instantiation. The 'this' property does not exists before the new command is called (and the object created)

So, if I want to make it work for ES6 classes, I have to change the above example to:

@AutoWired
class Test {
    private test2: Test2

     constructor(@Inject test2: Test2) {
          this.test2 = test2;
          console.log(this.test2);
     }
}

I am really trying to find a solution for this. This commit solves the ES6 problem but it breaks the compatibility, once the injected properties stop to work on constructors.

Any suggestions?

If the only solution is to apply the injections after the object creation, we will have to create a new major version that breaks the compatibility (or keep using ES5 :( )

thiagobustamante commented 7 years ago

I found a new solution.

I dont't apply the injections inside a modified constructor anymore.

I changed the @Inject annotation to modify the class prototype to handle these injections as a class property. So, now I have a get and set functions to handle injected properties. It solves the es6 problem (as I don't need to change the constructor anymore) and has a very nice side effect: It increase the performance, once now the container only have to inject a property if it is already used.

This is the method that handle the @Inject decorator on a class property:

static injectProperty(target: Function, key: string, propertyType: Function) {
      const propKey = `__${key}`;
      Object.defineProperty(target.prototype, key, {
          enumerable: true,
          get: function(){
               return this[propKey]?this[propKey]:this[propKey]=IoCContainer.get(propertyType);
          },
          set: (newValue) => {
              this[propKey] = newValue;
          } 
      });
 }

So, if you write:

@AutoWired
class SimpleInject {
   @Inject
   aDateProperty: Date;
   constructor() {
      if (this.aDateProperty)
         console.log('It works'); 
   }
}

The IoC Container will only inject the property aDateProperty when it is first accessed (in this example, when the constructor executes).