Inheritance

We try to provide developers with useful error feedback like:

Error: Missing required @injectable annotation in: SamuraiMaster

This works fine in most cases but it causes some problem when using inheritance.

For example, the following code snippet throws a misleading error:

The number of constructor arguments in a derived class must be >= than the number of constructor arguments of its base class.

@injectable()
class Warrior {
    public rank: string;
    public constructor(rank: string) { // args count  = 1
        this.rank = rank;
    }
}

@injectable()
class SamuraiMaster extends Warrior {
    public constructor() { // args count = 0
        super("master");
    }
}

In order to overcome this issues InversifyJS restricts the usage of inheritance with two rules:

A derived class must explicitly declare its constructor.

The number of constructor arguments in a derived class must be >= than the number of constructor arguments of its base class.

If you don't follow these rules an exception will be thrown:

Error: The number of constructor arguments in the derived class SamuraiMaster must be >= than the number of constructor arguments of its base class.

The users have a few ways to overcome this limitation available:

WORKAROUND A) Use the @unmanaged decorator

The @unmanaged() decorator allow users to flag that an argument will be manually injected into a base class. We use the word "unmanaged" because InversifyJS does not have control under user provided values and it doesn't manage their injection.

The following code snippet showcases how to apply this decorator:

import { Container, injectable, unmanaged } from "../src/inversify";

const BaseId = "Base";

@injectable()
class Base {
    public prop: string;
    public constructor(@unmanaged() arg: string) {
        this.prop = arg;
    }
}

@injectable()
class Derived extends Base {
    public constructor() {
        super("unmanaged-injected-value");
    }
}

container.bind<Base>(BaseId).to(Derived);
let derived = container.get<Base>(BaseId);

derived instanceof Derived2; // true
derived.prop; // "unmanaged-injected-value"

WORKAROUND B) Property setter

You can use the public, protected or private access modifier and a property setter to avoid injecting into the base class:

@injectable()
class Warrior {
    protected rank: string;
    public constructor() { // args count = 0
        this.rank = null;
    }
}

@injectable()
class SamuraiMaster extends Warrior {
    public constructor() { // args count = 0
        super();
        this.rank = "master";
    }
}

WORKAROUND C) Property injection

We can also use property injection to avoid injecting into the base class:

@injectable()
class Warrior {
    protected rank: string;
    public constructor() {} // args count = 0
}

let TYPES = { Rank: "Rank" };

@injectable()
class SamuraiMaster extends Warrior {
    @injectNamed(TYPES.Rank, "master")
    @named("master")
    protected rank: string;

    public constructor() { // args count = 0
        super();
    }
}

container
    .bind<string>(TYPES.Rank)
    .toConstantValue("master")
    .whenTargetNamed("master");

WORKAROUND D) Inject into the derived class

If we don't want to avoid injecting into the base class we can inject into the derived class and then into the base class using its constructor (super).

@injectable()
class Warrior {
    protected rank: string;
    public constructor(rank: string) { // args count = 1
        this.rank = rank;
    }
}

let TYPES = { Rank: "Rank" };

@injectable()
class SamuraiMaster extends Warrior {
    public constructor(
        @inject(TYPES.Rank) @named("master") rank: string // args count = 1
    ) {
        super(rank);
    }
}

container
    .bind<string>(TYPES.Rank)
    .toConstantValue("master")
    .whenTargetNamed("master");

The following should also work:

@injectable()
class Warrior {
    protected rank: string;
    public constructor(rank: string) { // args count = 1
        this.rank = rank;
    }
}

interface Weapon {
    name: string;
}

@injectable()
class Katana implements Weapon {
    public name: string;
    public constructor() {
        this.name = "Katana";
    }
}

let TYPES = {
    Rank: "Rank",
    Weapon: "Weapon",
};

@injectable()
class SamuraiMaster extends Warrior {
    public weapon: Weapon;
    public constructor(
        @inject(TYPES.Rank) @named("master") rank: string, // args count = 2
        @inject(TYPES.Weapon) weapon: Weapon
    ) {
        super(rank);
        this.weapon = weapon;
    }
}

container.bind<Weapon>(TYPES.Weapon).to(Katana);

container
    .bind<string>(TYPES.Rank)
    .toConstantValue("master")
    .whenTargetNamed("master");

WORKAROUND E) Skip Base class @injectable checks

Setting the skipBaseClassChecks option to true for the container will disable all checking of base classes. This means it will be completely up to the developer to ensure that the super() constructor is called with the correct arguments and at the correct time.

// Not injectable
class UnmanagedBase {
    public constructor(public unmanagedDependency: string) {}
}

@injectable()
class InjectableDerived extends UnmanagedBase {
    public constructor() // Any arguments defined here will be injected like normal
    {
        super("Don't forget me...");
    }
}

const container = new Container({ skipBaseClassChecks: true });
container.bind(InjectableDerived).toSelf();

This will work, and you'll be able to use your InjectableDerived class just like normal, including injecting dependencies from elsewhere in the container through the constructor. The one caveat is that you must make sure your UnmanagedBase receives the correct arguments.

What can I do when my base class is provided by a third party module?

In some cases, you may get errors about missing annotations in classes provided by a third party module like:

Error: Missing required @injectable annotation in: SamuraiMaster

You can overcome this problem using the decorate function:

import { decorate, injectable } from "inversify";
import SomeClass from "some-module";

decorate(injectable(), SomeClass);
return SomeClass;

Check out the JS example page on the wiki for more info.

results matching ""

    No results matching ""