Typesafe, (almost) Zero Cost Dependency Injection in TypeScript | Pure DI in TypeScript Node.js
Tags: typescript, productivity, architecture, node
Published: 2023-01-09 15:04:11

## Introduction

In this set of articles, I'm going to share my own experience with implementing and using Typesafe Dependency Injection with Registry in TypeScript Node.js backend applications.

So, let's get started with it.

> P.S. you can read more about Dependency Inversion vs. Inversion of Control vs. Dependency Injection [here](https://medium.com/ssense-tech/dependency-injection-vs-dependency-inversion-vs-inversion-of-control-lets-set-the-record-straight-5dc818dc32d1) and [here](https://www.martinfowler.com/articles/injection.html).

## Some Ways of Dependency Injection in TypeScript

The are basically 3 different ways to perform Dependency Injection in TypeScript:

- Higher-order function Arguments Injection
- Class Constructor Injection
- Class Property Injection (will not be covered here since it causes the [Temporal Coupling code smell](https://blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling/))

### Higher-order function

Example:

```ts
export function createOrderReceiptGenerator(orderService: OrderService) {
  return {
    generate(orderId: ID) {
      const order = orderService.findById(orderId);

      return orderService.generate(order);
    },
  };
}
```

Static linking example:

```ts
export function main() {
  const orderService = createOrderService();
  const orderReceiptGenerator = createOrderReceiptGenerator(orderService);

  const reciept = await orderReceiptGenerator.generate();
}
```

### Class Constructor Injection

Generally the same idea, but instead of closure, the context is handled by the JavaScript `class` feature.

```ts
export class OrderReceiptGenerator {
  constructor(private readonly orderService: OrderService) {}

  public generate(orderId: ID) {
    const order = this.orderService.findById(orderId);

    return this.orderService.generate(order);
  }
}
```

Static linking example (aka. Pure DI):

```ts
export function main() {
  const orderService = new DefaultOrderService();
  const orderReceiptGenerator = new DefaultOrderReceiptGenerator(orderService);

  const reciept = await orderReceiptGenerator.generate();
}
```

## IoC Container

IoC Container - is a service locator where you can register services by a token and later resolve them by the token:

```ts
interface Container {
  register(token: unknown, config: RegistrationConfig): this;
  resolve<T>(token: unknown): T; // throws wnen one of the token in the chain cannot be resolved
}
```

One of the nice features of IoC containers is that you can define lifetimes for your services.
We usually have the settings for the dependency _Lifetime_ as part of `RegistrationConfig`.
Well-known lifetimes:

- _Transient_ - New instance every time
- _Scoped_ - Instance per (HTTP request)/(another invocation)
- _Singleton_ - Instance per application lifetime

### With Decorators

This is the most common way to perform Dependency Injection in Typescript.

> Unlike Java and C#, when TypeScript is transpiled to JavaScript the type information gets lost, making a TypeScript interface become nothing in JS. Because of this, it's not possible to use the `interface` itself as a token in the IoC Container. [Decorators](https://github.com/tc39/proposal-decorators) come in handy here to configure the service metadata in the Container.

There are a bunch of libraries that use decorators:

- [inversify](https://inversify.io/)
- [tsyringe](https://github.com/microsoft/tsyringe)
- [typedi](https://github.com/typestack/typedi)
- [injection-js](https://github.com/mgechev/injection-js)
- [ioc](https://github.com/owja/ioc)
- [diod](https://github.com/artberri/diod)
- [etc.](https://github.com/topics/dependency-injection?l=typescript)

Example of using `inversify` decorators:

```ts
// order-service.ts
const OrderService = "OrderService";

export interface OrderService {
  findOrderById(orderId: ID): Promise<Order>;
  generateReceipt(order: Order): Promise<Receipt>;
  // ... others
}
```

```ts
// order-receipt-generator.ts
import { inject } from "inversify"; // <-- extra external dependency

export class OrderReceiptGenerator {
  constructor(
    @inject(OrderService) private readonly orderService: OrderService
  ) {}

  // generate receipt
}
```

Dynamic linking example:

```ts
export function main() {
  const container = new Container();

  container
    .bind<OrderService>(OrderService)
    .to(DefaultOrderService)
    .inSingletonScope();

  container
    .bind<OrderReceiptGenerator>(OrderReceiptGenerator)
    .toSelf()
    .inTransientScope();

  // ...later...

  const orderReceiptGenerator = container.resolve(OrderReceiptGenerator);

  const reciept = await orderReceiptGenerator.generate();
}
```

Unfortunately, the approach with decorators has some issues:

1. The decorator is still an experimental feature in TypeScript and it's not implemented in JavaScript. However, they are on [stage 3](https://tc39.es/process-document/) now.
2. Using the decorators requires importing the [reflect-metadata](https://www.npmjs.com/package/reflect-metadata) package
3. The decorators have performance overhead
4. The decorators make constructors more verbose and less readable
5. The decorators make a coupling between the business code and the IoC library
6. Very hard to override the token in `inject` since the metadata is bonded with the parent class itself

It's worth saying that problems 4-6 have workarounds but they are often not pretty.

### Without Decorators

You might also find some libraries that don't depend on decorators, like:

[DI-compiler](https://github.com/wessberg/DI-compiler)

- Pros:
  - Typesafe at transpile time
  - Requires no extra dependencies on business code
- Cons:
  - Requires extra transpile step
  - Not typesafe at dev time

[typed-inject](https://github.com/nicojs/typed-inject)

- Pros:
  - Typesafe at dev time and transpile time
  - Requires no extra dependencies on business code
- Cons:
  - Requires adding specific `public static inject` construction to class
  - Works only with [Literal Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) tokens

### Disadvantages

Overall, using the IoC Container is nice and fun, but it also has some disadvantages:

- Usually not typesafe, aka might lead to runtime errors if a desired token is not defined in the container
- Hard to know the dependency lifetime when injecting it
- Injecting short-lifetime dependency into long-lifetime service (for ex. _Transient_ dependency into _Singleton_ service) might lead to unpredicted behavior
- Runtime performance overhead
- IoC Container is [Service Locator](https://en.wikipedia.org/wiki/Service_locator_pattern) which is considered to be an anti-pattern when used directly

**Should we hold and think a little?**

Seems like at this point we have a lot of potential issues with the IoC Container approach. At the same time, the `Class Constructor Injection` seemed very clean but the static linking configuration is hard and messy. How can we achieve the configuration simplicity of the IoC Container with dev-time type safety?

## Removing Excess

Before we go to the solution, let's talk about the lifetimes.

Previously, I've mentioned 3 lifetimes: _Transient_, _Scoped_, and _Singleton_, but do we really need them given the disadvantages we have using them all?
If we simplify everything to _Singleton_ this might solve most of the problems. So, here is what we can do then:

- Use Factory when _Transient_ dependency is necessary
- Replace _Scoped_ dependency with a scope provider

### Use Factory when _Transient_ dependency is necessary

_Transient_ dependencies are tricky. They only work properly when the whole chain of resolution is _Transient_, otherwise, it might lead to unexpected problems.
So my solution here would be to use a factory when the new instance is required.

### Replace _Scoped_ dependency with a scope provider

The one example of using _Scoped_ dependency that comes to my mind, it's HTTP request level caching for libs like [dataloader](https://github.com/graphql/dataloader).

This problem seems tough initially, but fortunately, we can actually solve this easily by using Node.js [async_hooks](https://nodejs.org/api/async_hooks.html#class-asynclocalstorage) feature.
I will provide the solution in the next article.

At this point all our dependencies in the container become _Singleton_ and we can basically replace the container with a single application state object and create it at application startup time (like with did with static linking).

## Registry

This approach is inspired by the [article](https://audunhalland.github.io/blog/testability-reimagining-oop-design-patterns-in-rust/) and I call it _Registry_.

Let's start with the example of REST APIs built over [fastify](https://www.fastify.io/):

```ts
// orders-routes.ts
export function ordersRoutes(deps: {
  orderReceiptGenerator: OrderReceiptGenerator;
}): FastifyPluginCallback {
  return (fastify, _, next) => {
    fastify.get("/orders/:orderId/receipt", async (request, reply) => {
      const { orderId } = request.params as {
        orderId: string;
      };

      const receipt = await deps.orderReceiptGenerator.generateReceipt(orderId);

      if (!receipt) {
        return reply.status(404).send();
      }

      return reply.send(receipt);
    });

    next();
  };
}

// create-app-registry.ts
export function createAppRegistry() {
  const orderService = new DefaultOrderService();
  const orderReceiptGenerator = new DefaultOrderReceiptGenerator(orderService);

  // will automatically infer the types and fail at transpile type if services are missing or invalid.
  return {
    orderService,
    orderReceiptGenerator,
  };
}

// index.ts

async function main() {
  const registry = createAppRegistry();

  const server = Fastify({});
  server.register(ordersRoutes(registry));

  try {
    await server.listen({ port: PORT });

    console.log("listening on port", PORT);
  } catch (err) {
    server.log.error(err);
    process.exit(1);
  }
}

main();
```

Note that I pass the whole `registry` object into `ordersRoutes`, while `ordersRoutes` itself depends only on a slice of services to avoid unnecessary coupling:

```ts
export function ordersRoutes(deps: {
  orderReceiptGenerator: OrderReceiptGenerator;
}): FastifyPluginCallback {
  ...
```

Here we have the zero-cost type safety, with no runtime overhead.

However, this approach has one issue. The dependency graph might grow very fast and the `registry` composing might become painful.

### RegistryComposer

To solve the problem we can use the following simple `RegistryComposer` class:

```ts
export class RegistryComposer<TNeeds extends object = object> {
  private readonly creators: CreateServices<TNeeds, object>[] = [];

  add<TServices extends object>(
    createServices: CreateServices<TNeeds, TServices>
  ): RegistryComposer<Combine<TNeeds, TServices>> {
    this.creators.push(createServices);

    return this as any;
  }

  compose(): Readonly<TNeeds> {
    return Object.freeze(
      this.creators.reduce((state, createServices) => {
        return Object.assign(state, createServices(state));
      }, {} as any)
    );
  }
}

type CreateServices<TNeeds, TServices extends object> = (
  needs: TNeeds
) => TServices;

type Combine<TSource extends object, TWith extends object> = Norm<
  Omit<TSource, keyof TWith> & TWith
>;

type Norm<T> = T extends object
  ? {
      [P in keyof T]: T[P];
    }
  : never;
```

> Note: You can find complete source code [here](https://github.com/vad3x/typesafe-dependency-injection-in-typescript-samples/blob/main/samples/part1-typesafe-dependency-injection/src/registry-composer.ts).

The `RegistryComposer` provides the ability to chain state mutation. Every new call in the chain knows about the previous state modifications.
The `CreateServices` function type is a mapper function to create a new Registry state. The `needs` argument defines what services the registration depends on, and the function returns new services as a Record.

> Note: The `Combine` type is a helper to override field types when a field with the same name added.

> Note: The `Norm` type is a helper to remove `&` (intersections) from resulting TypeScript hints.

Calling `compose` finally composes the `registry` object:

```ts
// create-app-registry.ts
export function createAppRegistry() {
  return new RegistryComposer()
    .add(() => {
      orderService: new DefaultOrderService();
    })
    .add(
      // knowns that the state already contains orderService
      ({ orderService }) => ({
        orderReceiptGenerator: new DefaultOrderReceiptGenerator(orderService),
      })
    )
    .compose();
}
```

Let's refactor the code to make it prettier:

```ts
// create-app-registry.ts
export function createAppRegistry() {
  return new RegistryComposer()
    .add(orderService())
    .add(orderReceiptGenerator())
    .compose();
}

function orderService() {
  return () => {
    const orderService = new DefaultOrderService();

    return {
      orderService,
    };
  };
}

function orderReceiptGenerator{
  return (needs: { orderService: OrderService }) => {
    const orderReceiptGenerator = new DefaultOrderReceiptGenerator(
      needs.orderService
    );

    return {
      orderReceiptGenerator,
    };
  };
}
```

Looks much better now, we should also move the functions into their files and write tests.

### Why almost zero cost?

Obviously, we sacrifice the cold-start performance a little to have the `RegistryComposer` and nice-looking `add` functions.

## Function Injection

Very common the services tend to grow much if not paid attention (not following the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)).

```ts
// order-service.ts

export interface OrderService {
  findOrderById(orderId: ID): Promise<Order>;
  addProductToOrder(productId: ID): Promise<void>;
  findByProduct(productId: ID): Promise<readonly Order[]>;
  findByUser(userId: ID): Promise<readonly Order[]>;
  findByCategoryType(categoryType: string): Promise<readonly Order[]>;
  generateReceipt(order: Order): Promise<Receipt>;
  generateStats(): Promise<OrdersStats>;
  // ... others
}
```

Client code:

```ts
// order-receipt-generator.ts

export class OrderReceiptGenerator {
  constructor(private readonly orderService: OrderService) {}

  // generate receipt
}
```

To fix the issue, as a first step, we can just break up the `OrderService` interface into smaller interfaces.

```ts
// order-finder-service.ts

export interface OrderService {
  findByProduct(productId: ID): Promise<readonly Order[]>;
  findByUser(userId: ID): Promise<readonly Order[]>;
  findByCategoryType(categoryType: string): Promise<readonly Order[]>;
}

// order-receipt-service.ts

export interface OrderRecipeService {
  generateReceipt(order: Order): Promise<Receipt>;
}

// ... etc
```

The first doubt comes here with the naming, how should we name the interfaces now properly to emphasize the grouping?
Alternatively, we might also move every method into its own interface like:

```ts
// find-orders-by-product-service.ts

export interface FindOrdersByProductService {
  findByProduct(productId: ID): Promise<readonly Order[]>;
}

// find-orders-by-user-service.ts

export interface FindOrdersByUserService {
  findByUser(userId: ID): Promise<readonly Order[]>;
}

// ... etc
```

Hmm, seems very verbose now...

What if we could break up the service interface completely to remove the coupling?

Hopefully, unlike languages like C#, TypeScript is functional, meaning we can simply break up the interface to function types, so `OrderService` becomes:

> Note: In C# we can use `delegate` for it.

```ts
// find-order-by-id.ts

export type FindOrderById = (orderId: ID) => Promise<Order>;

// ... others...

// generate-report.ts

export type GenerateReceipt = (order: Order) => Promise<Receipt>;
```

Looks much cleaner now. Besides, it makes the API simpler, it also makes the API client constructor more understandable:

```ts
// order-receipt-generator.ts

export class OrderReceiptGenerator {
  constructor(
    private readonly findOrderById: FindOrderById,
    private readonly generateReceipt: GenerateReceipt
  ) {}

  public generate(orderId: ID) {
    const order = this.findOrderById(orderId);

    return this.generateReceipt(order);
  }
}
```

Additionally, it solves another problem with understanding of `OrderReceiptGenerator`. In the classical (service injection) approach, it's not possible to know what methods of `OrderService` are going to be called in `OrderReceiptGenerator` without looking into each method code. However, with the functional injection approach, we can just check the `OrderReceiptGenerator` constructor dependencies to easily grasp.

Additionally, we can now see if `OrderReceiptGenerator` class becomes too complex - when it has too many dependencies.

### Adding function to the Registry

To add a function to the Registry we can use the following code snippet

```ts
// stub-find-order-by-id.ts

export class StubFindOrderById {
  findOrderById: FindOrderById = (id) => {
    return Promise.resolve({
      id,
    });
  };
}

// registry/stub-find-order-by-id.ts

export function stubFindOrderById() {
  return () => {
    const { findOrderById } = new StubFindOrderById();

    return {
      findOrderById,
    };
  };
}
```

We can go even further now, and move Fastify server creation to the `RegistryComposer` as well:

```ts
// fastify-server.ts
export function fastifyServer() {
  return (deps: Parameters<typeof ordersRoutes>[0]) => {
    const server = fastify({});

    server.register(ordersRoutes(deps));

    return {
      fastifyServer: server,
    };
  };
}

// create-app-registry.ts
export function createAppRegistry() {
  return new RegistryComposer()
    .add(orderService())
    .add(orderReceiptGenerator())
    .add(fastifyServer())
    .compose();
}

// index.ts
async function main() {
  const { fastifyServer } = createAppRegistry();

  try {
    await fastifyServer.listen({ port: PORT });

    console.log("listening on port", PORT);
  } catch (err) {
    fastifyServer.log.error(err);
    process.exit(1);
  }
}

main();
```

## Conclusion

In this article, we learned how to use Dependency Injection with Registry on TypeScript.
The approach we implemented has the following characteristics:

- typesafe, meaning all the dependencies are resolved at developing/transpile time
- no framework/library dependencies to implement the injection (Pure DI)
- Easy to know the dependency lifetime since all dependencies are "_Singleton_" like in Registry
- Easy to create multiple instances of the same service with different token
- Easy to chain dependencies with the same token (for ex. for implementing middleware pattern)
- Zero-cost overhead (almost)

In the following articles, I'm going to cover the topics of scoped-like dependencies, logging, configuration, benchmarks, and using the registry within [hexagonal](https://alistair.cockburn.us/hexagonal-architecture/)/[clean](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) architecture projects.

Sample code can be found in the [repository](https://github.com/vad3x/typesafe-dependency-injection-in-typescript-samples/blob/main/samples/part1-typesafe-dependency-injection/src/index.ts).