Skip to content

A minimum, type-safe and straightforward dependency injection container for TypeScript

Notifications You must be signed in to change notification settings

ikenox/mini-di-container-ts

Repository files navigation

npm version CI License: MIT

mini-di-container

A minimum, type-safe and straightforward dependency injection container for TypeScript.

npm install mini-di-container

Philosophy

  • Typesafe: This package provides fully typed interfaces. If there are no compilation errors, then your code runs as you expected.
  • No annotation: This package is NOT annotation-based DI library.
  • Simple: Few public interfaces and parameters. When you read the small example code below, you will know everything about this library.
  • Plain: Few implicit rules and no proprietary syntax. If you can read TypeScript, then you can use this library intuitively.

Key features

  • Instance management: Dependency instances are instanciated and cached per container.
  • Lazy evaluation: Processes are lazy as much as possible. Each dependency instances are not instantiated until it is actually used.
  • Scope management: You can easily achieve singleton scoped, request scoped or any scoped contaniner you want. You can also use external parameters (e.g. request object) to build your dependencies.

Usage

import { type Infer, scope } from 'mini-di-container';

// Your types, classes and interfaces to be managed with DI container
interface Logger {
  log(message: string): void;
}
type GreetingConfig = {
  greetingWord: 'Hello' | 'Hi';
};
class GreetingService {
  constructor(readonly config: GreetingConfig) {}
  greet(name: string): string {
    return `${this.config.greetingWord}, ${name}.`;
  }
}
class TalkingService {
  constructor(
    readonly greetingService: GreetingService,
    readonly fromName: string
  ) {}
  talkTo(toName: string): string {
    return `${this.fromName} said: ${this.greetingService.greet(toName)}`;
  }
}

// Defines a new container scope, here example is a singleton-scoped.
const singletonScope = scope()
  .provide({
    // builder methods of dependencies.
    logger: (): Logger => ({
      log: console.log,
    }),
    config: (): GreetingConfig => ({ greetingWord: 'Hello' }),
  })
  .provide({
    // You can use already defined dependencies to build another dependencies.
    greetingService: ({ config }): GreetingService =>
      new GreetingService(config),
  });
// Instanciate singleton-scoped container.
const singletonContainer = singletonScope.instanciate({});

// Define another scope. You can specify scope-specific parameter. As an example,
// here is `{ request: Request }`.
const requestScope = scope<{ request: Request }>()
  // Provide other-scoped dependencies in this scope together
  // Note that the merged singleton-scoped dependencies are still singleton
  .static(singletonContainer)
  .provide({
    // Define request-scoped dependencies.
    talkingService: ({ greetingService }, { request }) =>
      new TalkingService(
        greetingService,
        request.headers.get('x-greeter-name') ?? 'anonymous'
      ),
  });

// The request-scoped container is not instanciated yet, since it is instanciated
// per a request.

// Your request handler as an example.
// Here assuming the request header contains `X-Greeter-Name: Alice`.
function requestHandler(request: Request) {
  // Instanciate the request-scoped container per request, with the scope-specific
  // external parameters.
  const requestScopedContainer = requestScope.instanciate({
    request,
  });

  // Of course, you can use each dependency instances directly.
  const { logger, config, talkingService } = requestScopedContainer;
  logger.log(config.greetingWord); // => 'Hello'
  logger.log(talkingService.talkTo('Bob')); // => 'Alice said: Hello, Bob.'

  // Another usage is passing the container itself to a downstream method.
  // This pattern is useful e.g. when the middreware method can't know which
  // dependencies will be used in the downstream.
  logger.log(doGreeting('Carol', requestScopedContainer));
  // => 'Alice said: Hello, Carol.'
}

type Dependencies = Infer<typeof requestScope>;
function doGreeting(
  toName: string,
  { logger, greetingService }: Dependencies
): string {
  logger.log('doGreeting is called');
  return greetingService.greet(toName);
}

License

MIT

About

A minimum, type-safe and straightforward dependency injection container for TypeScript

Resources

Stars

Watchers

Forks

Packages

No packages published