Support static typing / analysis with dependency injection I am working on creating a test library, and I found DI to be pretty convenient, maybe even to create more functional coding patterns.

Normally we can have an awkward before-hook setup in Node.js testing libraries, and it's always a little unclear when things actually get initialized and refactoring can be harder than it should be:

let client;

before(function(){
    return client = ...     // client requires asynchronous initalization
});

it('tests client', function(){
   assert(client.isConnected);   // client is declared / initialized...right?
});


I've always hated that pattern, sometimes just for ergonomic reasons. It just seems awkward to me. Because client will be in scope in places where it is not yet initialized.

I thought about avoiding this pattern and finally devised a way around it, it basically looks like this:

describe('parent', function(){

    inject(() =&gt; {
        return {
            client: new Client({port: 7033}).ensure()  // returns a promise
        }
    });

    describe('child',  (client) =&gt; {

       // client has been initialized, and is injected here

        it('tests client', function(){
          // ok we know for sure client is defined
          assert(client.isConnected);
        });

    });

});


as you can see in any child block, we can inject parameters from the parent block.

This is simply a nicety, and makes things easier to grok and less error prone. Maybe even more functional programming style, not sure.

My real concern is supporting static typing, with TypeScript and allowing for at least some static analysis for correctness.

My question is - does anyone know if there is some ES7 or ES8 feature (maybe annotations/decorators?) that can somehow allow for some name checking or type checking in this kind of scenario?

I don't want to create some crazy feature that is so dynamic that it's hard to use. 

Basically the way it works currently is that Promise.resolve is called for key/value returned from the inject hook, then this value is injected into the child blocks, by name. So the parameter to the child block callback function will have to match the key of the returned object from the inject hook. This is an API feature in progress and looking for constructive criticism. 

To clarify, this would be an alternative API design option:

   inject('client', () =&gt; {
        return new Client({port: 7033}).ensure()
    });


then we could use the string 'client' as the matching value to determine where to inject the client variable. Of course, you would only be able to return a single value from this, and not a tuple.

I am also looking for a good way to implement this with callbacks instead of promises. I am not going to reinvent the wheel, so somewhere along the way we should use the async library or similar, here is what I think is the best option:

   inject.cb(j =&gt; {
        async.parallel({a: foo(), b: bar()}, j);
    });


    describe('child',  (a, b) =&gt; {

       // a and b have been initialized, and are injected here

    });


this way my library could work with any other library that fired a final callback with this signature:

function complete(err, results){}


where results is a simple key-value map; j would be the library callback which accepts the above arguments.

Another issue of course, is handling immutability, because the same object(s) will get injected into multiple places:

    inject('client', () =&gt; {
        return new Client({port: 7033}).ensure()
    });


    describe('child1',  (client) =&gt; {

       // client is the same object as the below child block

    });

    describe('child2',  (client) =&gt; {

       // client is the same object as the above child block

    });


So should make client immutable somehow.
As far as I can tell, the use case you are targeting is already supported by the completion callback, even when you are using promises.  I have had good luck using the following pattern to ensure that asynchronously initialized, promise based libraries are fully initialized before testing begins:

let client;

// Note the function now accepts argument `done`
before(function(done){
    client = new Client({port: 7033}).ensure().then(done)
                                     .catch(function (reason) {
                                         console.error(reason);
                                     });
});

it('tests client', function(){
    assert(client.isConnected);   // we know client is initialized
});


As you correctly pointed out this is similar to returning the promise. 

let client;

before(function(){
    client = new Client({port: 7033}).ensure()
                                     .catch(function (reason) {
                                         console.error(reason);
                                         throw reason;
                                     });
    return client;
});

it('tests client', function(){
    assert(client.isConnected);   // we know client is initialized
});


In either case, the initialization should be guaranteed complete before testing continues.  So unless I am missing something, there should not be any ambiguity about the initialization state.

If you wish to initialize before each test, it works just as well with beforeEach(...).  Also, note that there is a timeout that will cause test failure if done is not called quickly enough.  Reference the docs if you need to extend the timeout as there are a variety of patterns.

While I think the existing pattern fully addresses your use case, you might prefer the argument injection for stylistic reasons.  In that case I propose you can create a wrapper that calls the test function in the context of the client's then handler.

function provide_client(testFunc) {
    return function (done) {
        // Here I assume that the initialized client is passed through `then`.
        // If not fetch it from where ever appropriate.
        new Client({port: 7033}).ensure().then(function (client) {
            testFunc(client);
            done();
        }).catch(function (reason) {
            console.error(reason);
        });
    };
}

// Application
it('tests client', provide_client(function (client) {
    assert(client.isConnected);
}));