DEV Community

Michael Z
Michael Z

Posted on • Edited on • Originally published at michaelzanggl.com

Automatic Dependency Injection in JavaScript

Originally posted at michaelzanggl.com. Subscribe to my newsletter to never miss out on new content.

In the previous post of this series 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;
}
Enter fullscreen mode Exit fullscreen mode

Angular makes use of TypeScript's emitDecorateMetadata.

class Pterodactyls {}

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

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    })
})
Enter fullscreen mode Exit fullscreen mode

And SimpleClass looks like this

// test/modules/SimpleClass.js

class SimpleClass {}

module.exports = SimpleClass
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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'))
})
Enter fullscreen mode Exit fullscreen mode

and ioc.make becomes

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

return new object
Enter fullscreen mode Exit fullscreen mode

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') )
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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')
        )
    })
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

with

const dependencies = object.inject.map(path => this.make(path))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)