Skip to content

marcusrettig/box

Repository files navigation

Box

Box is a minimalistic, type-safe, zero-dependency, service container.

Note: This library is still in it's early stages and breaking changes are expected.

Breaking change: In release 0.2.0, the inject() function was changed to inject a single service instead of the entire container: inject(name). To inject the entire service container you should not use the injectAll() function instead (added in 0.2.1). See example below.

Install

With npm:

npm install @marcusrettig/box
Other package managers

With pnpm:

pnpm add @marcusrettig/box

With yarn:

yarn add @marcusrettig/box

Simple Example

import {Box} from '@marcusrettig/box';

class Database {
  findAll<T>(table: string): T[] {
    if (table === 'employees') {
      return [{name: 'Michael Scott'}] as T[];
    }

    throw new Error(`Table ${table} does not exist`);
  }
}

class EmployeeService {
  // Services can inject other services using the inject function.
  // The type is automatically inferred, in this case to Database.
  database = container.inject('database');

  findAll() {
    return this.database.findAll<{name: string}>('employees');
  }
}

const container = new Box({
  database: Box.class(Database),
  employeeService: Box.class(EmployeeService),
});

function start() {
  // The type for $ is automatically inferred to:
  // { database: Database; employeeService: EmployeeService }
  const $ = container.init({});

  const employees = $.employeeService.findAll();

  console.log(employees);
}

start();
Run this example

npx tsx examples/simple.ts

Injecting all dependencies

Release 0.2.0 introduced a breaking change where the inject() function now injects a single named dependency instead of the entire service container. Release 0.2.1 adds a function called injectAll which injects the full container. Please note that some dependencies may not be initialized when injecting the container, so it is unsafe to access it during the injection context.

import {Box} from '@marcusrettig/box';

class EmployeeService {
  $ = container.injectAll();

  greet(name: string) {
    return this.$.greeting + ' ' + name;
  }
}

const container = new Box({
  greeting: Box.value('Hello'),
  employeeService: Box.class(EmployeeService),
});

const $ = container.init({});

console.log($.employeeService.greet('Michael Scott'));
Run this example

npx tsx examples/inject-all.ts

Design Goals

  • Type-Safe: Detect all type errors at compile time (given that you follow the two rules described in the next section). Say goodbye to "No provider found" and similar errors.
  • Minimalistic: The library consists of fewer than 100 lines of code (excluding types and comments). It should be easy for anyone to understand what goes on under the hood.
  • Zero-dependency: No external dependencies makes it easy to use the library without a package manager such as npm, for example in the Deno runtime.

Injection Context

A service can access another service by injecting it using the inject() function. The benefit of this approach is that the type of the injected service is automatically inferred, and that the service is guaranteed to have been provided, eliminating the "No provider found" errors which you've probably seen in more dynamic dependency injection systems. This however comes with two caveats:

  1. The inject() function can only be called inside an injection context, such as a constructor, property initializer, or inside a factory function.
  2. Circular dependencies are not allowed. For example, if service A injects service B which injects service C which injects service A an error will be thrown.

The concept of injection context is similar to that in the Dependency Injection system of Angular 16 and you can read more about it in the Angular Documentation.

TLDR: Only call inject() in a constructor, property initializer, or factory function. Avoid circular dependencies.

The following examples demonstrates the injection context concept:

// Property initializer:
class EmployeeService {
  // Inside injection context:
  database = container.inject('database');

  findAll() {
    // Outside injection context (after initialization):
    return this.database.findAll('employees');
  }
}

// Factory function:
class createEmployeeService() {
  // Inside injection context:
  const database = container.inject('database');

  return {
    findAll() {
      // Outside injection context (after initialization):
      return database.findAll('employees');
    }
  }
}

About

Minimalistic type-safe service container

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published