Automatic Dependency Injection in JavaScript

Published 6/1/2019

This article is part of a series:

In the previous post we were implementing our very own ioc container by creating bindings with ioc.bind and ioc.singleton. But this setup can be a little cumbersome. That's why many frameworks also come with automatic dependency injection.

Laravel can do this thanks to PHP's typehinting mechanism

public function __construct(UserRepository $users)
{
    $this->users = $users;
}

Angular makes use of TypeScript's emitDecorateMetadata.

class Pterodactyls {}

@Component({...})
class Park {
    constructor(x: Pterodactyls, y: string) {}
}

But these luxuries don't come in vanilla JavaScript. So in this article we will implement automatic injection in a similar fashion it was done on the MVC framework Adonis.js.

You can find the complete code on the same GitHub as in the last post.

We start off with (a little improved version of) the code from last time:

module.exports = function createIoC(rootPath) {
    return {
        _container: new Map,
        _fakes: new Map,
        bind(key, callback) {
            this._container.set(key, {callback, singleton: false})
        },
        singleton(key, callback) {
            this._container.set(key, {callback, singleton: true})
        },
        fake(key, callback) {
            const item = this._container.get(key)
            this._fakes.set(key, {callback, singleton: item ? item.singleton : false})
        },
        restore(key) {
            this._fakes.delete(key)
        },
        _findInContainer(namespace) {
            if (this._fakes.has(namespace)) {
                return this._fakes.get(namespace)
            }

            return this._container.get(namespace)
        },
        use(namespace) {
            const item = this._findInContainer(namespace)

            if (item) {
                if (item.singleton && !item.instance) {
                    item.instance = item.callback()
                }
                return item.singleton ? item.instance : item.callback()
            }

            return require(path.join(rootPath, namespace))
        }
    }
}

The idea is to avoid newing up classes manually and using a new method ioc.make instead. Let's write the simplest test we can think of.

describe('auto injection', function() {
    it('can new up classes', function() {
        const SimpleClass = ioc.use('test/modules/SimpleClass')
        const test = ioc.make(SimpleClass)
        expect(test).to.be.instanceOf(SimpleClass)
    })
})

And SimpleClass looks like this

// test/modules/SimpleClass.js

class SimpleClass {}

module.exports = SimpleClass

Running the test should fail because we have not yet implemented ioc.make. Let's implement it in index.js

const ioc = {
    // ...
    make(object) {
        return new object
    }
}

The test passes! But it is a little annoying to always have to first do ioc.use and then ioc.make to new up classes. So let's make it possible to pass a string into ioc.make that will resolve the dependency inside.

A new test!

it('can make classes using the filepath instead of the class declaration', function() {
    const test = ioc.make('test/modules/SimpleClass')
    expect(test).to.be.instanceOf(ioc.use('test/modules/SimpleClass'))
})

and ioc.make becomes

if (typeof object === 'string') {
    object = this.use(object)
}
            
return new object

Nice! With this, we can already new up classes. And the best thing is, they are fakable because ioc.use first looks in the fake container that we can fill with ioc.fake.

With that out of the way, let's build the automatic injection mechanism. The test:

it('should auto inject classes found in static inject', function() {
        const injectsSimpleClass = ioc.make('test/modules/InjectsSimpleClass')

        expect( injectsSimpleClass.simpleClass ).to.be.instanceOf( ioc.use('test/modules/SimpleClass') )
})

And we have to create the class InjectsSimpleClass.js

// test/modules/InjectsSimpleClass.js

class InjectsSimpleClass {
    static get inject() {
        return ['test/modules/SimpleClass']
    }

    constructor(simpleClass) {
        this.simpleClass = simpleClass
    }
}

module.exports = InjectsSimpleClass

The idea is that we statically define all the classes that need to be injected. These will be resolved by the ioc container and newed up as well.

ioc.make will become:

if (typeof object === 'string') {
    object = this.use(object)
}

// if the object does not have a static inject property, let's just new up the class
if (!Array.isArray(object.inject)) {
    return new object
}

// resolve everything that needs to be injected
const dependencies = object.inject.map(path => {
    const classDeclaration = this.use(path)
    return new classDeclaration
})

return new object(...dependencies)

Not bad. But something about return new classDeclaration seems wrong... What if this injected class also has dependencies to resolve? This sounds like a classic case for recursion! Let's try it out with a new test.

it('should auto inject recursively', function() {
    const recursiveInjection = ioc.make('test/modules/RecursiveInjection')
    expect(recursiveInjection.injectsSimpleClass.simpleClass).to.be.instanceOf(
            ioc.use('test/modules/SimpleClass')
        )
    })

And we have to create a new file to help us with the test.

// test/modules/RecursiveInjection.js

class RecursiveInjection {

    static get inject() {
        return ['test/modules/InjectsSimpleClass']
    }

    constructor(injectsSimpleClass) {
        this.injectsSimpleClass = injectsSimpleClass
    }
}

module.exports = RecursiveInjection

The test will currently fail saying AssertionError: expected undefined to be an instance of SimpleClass. All we have to do is switch out

const dependencies = object.inject.map(path => {
    const classDeclaration = this.use(path)
    return new classDeclaration
})

with

const dependencies = object.inject.map(path => this.make(path))

Altogether, the make method looks like this

if (typeof object === 'string') {
    object = this.use(object)
}

// if the object does not have a static inject property, let's just new up the class
if (!Array.isArray(object.inject)) {
    return new object
}

// resolve everything that needs to be injected
const dependencies = object.inject.map(path => this.make(path))

return new object(...dependencies)

And that's pretty much it! The version in the repo handles some more things like not newing up non-classes, being able to pass additional arguments, aliasing etc. But this should cover the basics of automatic injection. It's surprising how little code is necessary to achieve this.