# Getting started

# Installation

Install peer dependencies: npm install @apoyo/std

Install package: npm install @apoyo/ioc

# Motivation

Today, a lot of solutions exists for dependency injection in JS/TS, the most popular solutions being:

  • Nestjs
  • Inversify
  • Typedi
  • etc...

However, very few DI solutions exist that are fully type-safe and easy to use. This solution also completely forgoes decorators to achieve higher type-safeness and better decoupling by encouraging the use of abstractions.

# Features

# Ease of use

This library is very easy to use while still being fully typed.

In the example below, we organize our providers in "modules" / "namespaces":

import { Provider } from '@apoyo/ioc'

export class ConfigurationModule {
  private static ENV = Provider.fromFactory(loadEnvironment, [])
  
  static HTTP = Provider.fromFactory(configureHttp, [ConfigurationModule.ENV])
  static LOGGER = Provider.fromFactory(configureLogger, [ConfigurationModule.LOGGER])
}

You can then resolve these Providers through a Container:

const container = new Container()

// Only now do we instantiate `ConfigurationModule.HTTP` (and its dependencies)
const httpConfig = await container.get(ConfigurationModule.HTTP)

Note: Keep in mind that providers are simply "factories". They don't execute anything by themselves, nor should they: they only "wire" up our dependencies / tell our program how to instantiate everything.

# Integrations

This library is very easy to use with existing third-party libraries:

export class MailerModule {
  static MAILER = Provider.fromFactory(createNodemailer, [ConfigurationModule.MAILER])
}

# Composition

This library encourages composition: As a provider is a simple variable, we can also pass them as parameters to functions and dynamically create new ones:

Example:

export class LoggerModule {
  static LOGGER = Provider.fromFactory(createPinoLogger, [ConfigurationModule.LOGGER])

  static child(contextName: string): Provider<Logger> {
    return Provider.from(async (container): Logger => {
      const logger = await container.get(LoggerModule.LOGGER)
      return logger.child({
        name: contextName
      })
    })
  }
}

export class MailerModule {
  private static LOGGER = LoggerModule.child('Mailer')

  static MAILER = Provider.fromFactory(createNodemailer, [
    ConfigurationModule.MAILER, 
    MailerModule.LOGGER
  ])
}

# Resource management

This library has inbuild resource management, to automatically cleaned up disposable resources when the container is closed:

export class HttpModule {
  static APP = Provider.fromFactory(createApp, [LoggerModule.LOGGER])

  static SERVER = pipe(
    Provider.fromFactory(createServer, [HttpModule.APP, ConfigurationModule.HTTP]),
    Provider.asResource({
      priority: ShutdownPriority.HIGH,
      close: (server) => server.close()
    })
  )
}

You may also specify a shutdown priority to control the order in which your resources are cleaned up, from the highest priority first to the lowest priority at last.

# No decorators

This library does not offer decorators support: while this is opiniated, there are not supported for multiple reasons:

  • Your implementations should not be aware of the framework / IoC container system you use. Decorators go against this practice, as they need to be applied on your implementations directly.

  • Decorators encourage higher coupling, by making it harder to depend on an abstraction, instead of an implementation: In fact, most of the time, when using decorators, you inject classes and not interfaces.

  • It is easy to introduce run-time errors with decorators:

@Injectable()
class MyService {
  // We forgot to tell our IoC how to provide / create a Mailer instance.
  // Typescript does not complain, however you WILL receive a run-time error.
  constructor(private readonly mailer: Mailer) {}
}
  • Decorators can not verify that the token you inject correspond to the given type:
@Injectable()
class MyService {
  // How should we know if MY_TOKEN is a "string"? It may as well be a "number". 
  // Typescript cannot statically ensure that we used the correct type here.
  // It also makes injecting non-class variables less intuitif, because you need to know which token is associated to which value.
  constructor(@Inject(MY_TOKEN) private readonly myToken: string) {}
}
  • Decorators can only be applied to your classes. They won't work for primitives or factories or classes from third-party libraries. As such, if you want to create providers for third-party libraries (for example), you will need to use a different API, which means you sometimes use decorators and sometimes a different API. This makes your code less consistent.

This library makes this choice very simple: There is only one way to create providers.

# Circular dependencies

This library does not support circular dependencies. As such, the following is impossible:

// This does not work
export class MyModule {
  static A = Provider.fromFactory((b) => b, [MyModule.B])
  static B = Provider.fromFactory((a) => a, [MyModule.A])
}

// This does not work either
const a = b
const b = a

Circular dependencies will not be supported in the future either! Most of the time, circular dependencies can easily be avoided by splitting up your code correctly.