Using tsyringe for dependency injection without using the class syntax
Published: 2020-07-20 20:16:41

`tsyringe` is a really cool library that does automagical dependency injection for classes, etc.

The entire premise of this library compared to others, is that using the powerful reflection features of TypeScript, they can examine the argument list for a class constructor, and automagically populate the argument list for you, relieving you of the burden of passing things around by yourself.

This is ideal, because in tests, you can simply re-define some "provider" to inject stubs, mocks or other test doubles into the containers, so your classes don't talk directly to the internet, or launch redis missiles, or whatever other whacky side-effects your "normal" implementations might have.

I'm working on a tiny toy app that doesn't use _class_ based syntax though, it rather comprises dozens of tiny small functions, 5-10 lines of virtually stateless things that maybe throw a few keys in Redis, or make a single API request, or whatever.

Here tsyringe can be helpful too, but the docs don't make it super easy.

The trick is to eschew all the fancy [decorator syntax](https://github.com/microsoft/tsyringe#decorators) and simply use the `container` directly, check this out:

``` typescript

// File: di.ts

import "reflect-metadata";
import { container } from "tsyringe";
import { createRedisClient, RedisClient } from "./redisClient";
import env, { Env } from "./env";
import fetch from "node-fetch";
import rp, { RequestPromiseAPI } from "request-promise-native";

// re-export the container, so people must import this file
// and not accidentally get `container' directly from tsyringe
export { container };

export { RedisClient };
container.register<RedisClient>("RedisClient", {
  useFactory: (_) => createRedisClient(),
});

export { Env };
container.register<Env>("env", {
  useValue: env,
});

export { RequestPromiseAPI };
container.register<RequestPromiseAPI>("rp", {
  useValue: rp,
});

export type FetchAPI = typeof fetch;
container.register<FetchAPI>("fetch", {
  useValue: fetch,
});
```

Most these examples use `useValue` here, which should be pretty self-explanatory. The `useFactory` one could also use a `useValue`, but I want to defer connecting to Redis until someone actually uses this item from the container. That makes shared tests, that possibly don't need the (albeit tiny) overhead of connecting to Redis, can avoid doing so.

Check the docs `useFactory` receives an instance of the `container`, so if I needed to pull Redis config out of the `env`, for example, I'd have access to `container.resolve<Env>("env")` in that factory function to help me configure my Redis client properly.

---

## Re-exporting types

The lines 

``` typescript
import env, { Env } from "./env";
// ...snip...
export { Env };
// ...snip...
export { RequestPromiseAPI };
```

and others, are just to help keep the imports in files that consume the DI clean, I wouldn't want to have an import referring to my `di.ts` for the thing itself, but have to get the type from the 3rd party package or some other file. The type re-exporting was a late addition to this approach, but I think it makes the consumers cleaner.

## Using the DI container

From any regular file then, we do this:

``` typescript
import { container, Env, RequestPromiseAPI, RedisClient } from "../di";
import url from "url";

interface AuthenticationTokens {
  readonly id_token: string;
  readonly access_token: string;
}

const env = container.resolve<Env>("env");

function authHeader(): string {
  return Buffer.from(
    [env.AWS_COGNITO_APP_ID, env.AWS_COGNITO_APP_SECRET].join(":")
  ).toString("base64");
}

export async function authenticate({
  code,
  redirect_uri,
}: AuthenticateParams): Promise<AuthenticationTokens> {
  var options = { "... snip ...": "" }

  const rp = container.resolve<RequestPromiseAPI>("rp");
  const redisClient = container.resolve<RedisClient>("RedisClient");

  return await rp(options)
    .then((r: any) => {
      const { id_token, access_token, refresh_token } = JSON.parse(r);

      const key = `access_token_refresh-${access_token}`;

      redisClient
        .multi()
        .set(key, refresh_token)
        .expire(key, 30 * 86400) // days × seconds in a day
        .exec();

      return { id_token, access_token };
    });
}
```

There's quite a lot of code here, but just take away that we're using the DI for three different things in a relatively simple 15 or so lines of code.

---

## Testing

When it comes to testing, you can simply override those things in your test set-up:

``` typescript
import { container, RedisClient, RequestPromiseAPI } from "../di";
import { authenticate } from "./authenticate";
var sinon = require("sinon");

describe("authenticate helpers", () => {
  let redisSpy = sinon.spy();
  let rpFake = sinon.fake.resolves(JSON.stringify({ hello: "world" }));

  beforeEach(() => {
    container.register<RedisClient>("RedisClient", { useValue: redisSpy });
    container.register<RequestPromiseAPI>("rp", { useValue: rpFake });
  });

  afterEach(() => {
    // Restore the default sandbox here
    sinon.restore();
  });

  it("it calls out to some pre-defined thing", (done) => {
    authenticate({
      redirect_uri: "htps://www.example.com/",
      code: "0xDEADBEEF",
    }).then(() => done());
  });
});
```

No exhaustive walk-through of the test code, it should be self explanatory. We're simply forcing the container to provide stubs, fakes and doubles in place of the real implementations. 

`rp` has a tricky API for this, and I'm rusty with sinon.js, but this seems to work for me so far.

For sure the elegant `@injectable` decorators and things for the class-based syntax are fun, but one can certainly still benefit from tools such as tsyringe without needing to adopt a specific way of laying out your classes and functions.