nicojs / typed-inject

Type safe dependency injection for TypeScript
Apache License 2.0
431 stars 23 forks source link

Difference between resolve and injectClass #42

Closed etiennelenhart closed 2 years ago

etiennelenhart commented 2 years ago
class Foo {
    bar() {
        console.log('Foobar!')
    }
}

class FooFake extends Foo {
    override bar() {
        console.log('Foo what?')
    }
}

class Baz {
    static inject = ['foo'] as const
    constructor(protected readonly foo: Foo) {}

    boo() {
        this.foo.bar()
    }
}

const appInjector = createInjector().provideClass('foo', Foo).provideClass('baz', Baz).provideClass('foo', FooFake)
const baz = appInjector.resolve('baz')
const baz2 = appInjector.injectClass(Baz)

baz.boo() // prints 'Foobar!'
baz2.boo() // prints 'Foo what?' (expected)

Is this difference in behavior intentional? Makes it really hard to consistently overwrite dependencies for testing etc. Always using injectClass by convention would be an option, but in order to get an instance of the overwritten class I'd have to use resolve('foo'). Or is it possible to somehow bind FooFake as Foo?

snnsnn commented 2 years ago

The injectClass instantiates a class using values from the injector and returns it. I don't think it overwrites any instance created for provideClass binding. Basically it is a shortcut for resolving values and creating a new instance with those values like so:

const foo = appInjector.resolve('foo');
const baz = new Baz(foo);

If you look at the function definition you will see it:

const args: any[] = this.resolveParametersToInject(Class, providedIn);
return new Class(...(args as any));

Documentation says:

Any instance created with injectClass or injectFactory will not be
disposed when dispose is called. You were responsible for creating it,
so you are also responsible for the disposing of it.
nicojs commented 2 years ago

@snnsnn is right, the main difference is that injectClass does not mark that class as injectable. Also, you would need to dispose of resources yourself (if you find yourself in the situation that you need that).

Makes it really hard to consistently overwrite dependencies for testing etc

Your example works as intended. By design, it isn't possible to override dependencies earlier in the dependency injection tree (it is really hard to make it type-safe in that case). I think in general, you want the Injector you use during testing to be independent of the one you use for your production code. In StrykerJS we have a separate TestInjector instance for unit and integration testing, see https://github.com/stryker-mutator/stryker-js/blob/master/packages/test-helpers/src/test-injector.ts

Hope this clears it up for you. If you think we can improve our docs, please let me know how (or create a PR 😇)

etiennelenhart commented 2 years ago

Thanks for the clarification. I guess providing a different injector actually is the better approach.