Inversifyjs: Why must the number of constructor arguments in a derived class be greater than or equal to that of its base class?

Created on 1 Apr 2017  路  7Comments  路  Source: inversify/InversifyJS

I would like to know why the number of constructor arguments for a derived class MUST match or exceed that of the base class. Here is my setup:

RepositoryBase

export class RepositoryBase<T> implements IRepositoryBase<T> {
  protected readonly _repository: Repository<T>;
  constructor(type: { new (): T; }) {
    this._repository = getConnectionManager().get().getRepository<T>(type);
  }
}

ModeratorRepository

@injectable()
export class ModeratorRepository extends RepositoryBase<Moderator> implements IModeratorRepository {
  constructor() {
      super(Moderator);
  }
}

I feel like that's a pretty straightforward setup: The derived repository is injectable and facilitates the generic nature of the base class by passing in the type required by typeorm's repository fetching method.

In fact, I don't want RepositoryBase to be injectable at all, as I'm going to refactor it into an external module to be used across several of my apps. Without making the base class injectable, I got the "missing injectable annotation on ModeratorRepository" error, and after very reluctantly making it injectable, I encountered the "number of constructor arguments in the derived class ModeratorRepository must be >= than the number of constructor arguments of its base class" error.

I would love for the base class to not be injectable and especially to not have to hack up my code to satisfy the constructor argument requirement.

Edit: I apologize for the poorly formatted code. I can't figure out how to format it appropriately.

question

Most helpful comment

OK, the @unmanaged() decorator works, but I find it a little irritating. If this was needed for an actual functional reason, that would be one thing, but from your explanation, it seems like you are simply annoying those who understand how inheritance works, just to baby those who do not.

There are a myriad of ways that an unskilled/ignorant/inept developer could screw up their code, and this is a very strange specific example that you're trying to catch through your infrastructure when really it should be caught by the developer when they're debugging and finding that their parameter value is missing. Besides, if they are using TypeScript and their base class parameters are not optional, it will give them a compilation error, which is where this kind of thing should be found! It is not the responsibility of your library to find it for them.

P.S. thanks for the library! I find it very helpful to manage dependencies, especially in the request scope.

All 7 comments

Hi,

The error:

The number of constructor arguments in a derived class be greater than or equal to that of its base class

Is something that we added to ensure that a developer won't forget to pass some arguments to the super constructor. We did this to prevent people from wasting time trying to figure out why some injection was missing. By default, it forces both the derived and super constructors to match but that is just the default behavior.

We implemented a decorator named @unmanaged() to allow your use case. In your derived constructor, you are injecting something into the base class via its super constructor. It is important to understand that is injection is done by your code and not by the IoC container. For this reason, we consider this an "unmanaged" injection since the injection is not managed by inversify:

import { unmanaged, injectable } from "inversify";

export class RepositoryBase<T> implements IRepositoryBase<T> {
  protected readonly _repository: Repository<T>;
  constructor(@unmanaged() type: { new (): T; }) { // This injection is not managed by inversify!
    this._repository = getConnectionManager().get().getRepository<T>(type);
  }
}

@injectable()
export class ModeratorRepository extends RepositoryBase<Moderator> implements IModeratorRepository {
  constructor() {
      super(Moderator); // injected by the developer not by the IoC container library
  }
}

I'm closing this issue but please use the comments if you need additional info.
ps: I have edited your question check the markdown to see how to format the code

@remojansen Thanks so much for your help! That did the trick. I think I can live with putting extra decorators in my common RepositoryBase.

OK, the @unmanaged() decorator works, but I find it a little irritating. If this was needed for an actual functional reason, that would be one thing, but from your explanation, it seems like you are simply annoying those who understand how inheritance works, just to baby those who do not.

There are a myriad of ways that an unskilled/ignorant/inept developer could screw up their code, and this is a very strange specific example that you're trying to catch through your infrastructure when really it should be caught by the developer when they're debugging and finding that their parameter value is missing. Besides, if they are using TypeScript and their base class parameters are not optional, it will give them a compilation error, which is where this kind of thing should be found! It is not the responsibility of your library to find it for them.

P.S. thanks for the library! I find it very helpful to manage dependencies, especially in the request scope.

is there any way this annotation can be added to the derived class rather than the base class? Specifically, what if the base class is in a library you don't want to modify or add inversify annotations to?

I've got code like this for the base classes:

decorate(injectable(), LibClass1);
decorate(injectable(), LibClass2);
decorate(injectable(), LibClass3);

Which is annoying but managable, but I don't see how to decorate the constructor parameters with @unmanaged().

From a compositional perspective it seems like something that is more appropriate on the derived class anyway.

@remojansen I agree with @holotrek. This is not only out-of-scope for a DI library, but also a design bug. What happens when you have not only A extends ABase, but also, GreaterA extends A? Isn't it valid to have GreaterA pass constant parameters to A's constructor while still wanting Inversify to manage all of A's constructor parameters so A, itself, might be injected?

How does Inversify currently satisfy this use-case? Anyway, thanks for a great library.

I'd like to reiterate this. This should at most be a compile time warning at most, not the current design.

The container option skipBaseClassChecks: true disables this. See #841

Was this page helpful?
0 / 5 - 0 ratings