Skip to content

Dependency injection done right.

License

Notifications You must be signed in to change notification settings

TypeFox/djinject

Repository files navigation

npm version build Gitpod ready-to-code


Djinject empowers developers designing decoupled applications and frameworks. Djinject's main goal is increasing the developer experience by offering a tiny, yet powerful API, keeping dependencies in central module definitions and by using TypeScript's type system to restrain runtime challenges.

djinject inversify
minified minified size minified size
minzipped minzipped size minzipped size
typesafe
requirements none decorators
style functional imperative
API surface area tiny non-trivial

Features

  • type-safe
  • tiny footprint
  • property injection
  • rebinding dependencies
  • dependency cycle detection
  • lazy and eager initialization
  • no magic, no global state
  • no decorators
  • no dependencies
  • no configuration
  • no boilerplate

Quickstart

The first step is to add djinject to your application.

npm i djinject

Bascially, the only thing needed is to define modules of factories and finally call inject. The resulting container provides concrete instances.

import { inject } from 'djinject';

// create an inversion of control container
const container = inject({
    hi: () => 'Hi',
    sayHi: () => (name: string) => `${container.hi} ${name}!`
});

// prints 'Hi Djinject!'
console.log(container.sayHi('Djinject'));

API

Terminology

The inject function is turning modules into a container. A module is a plain vanilla JS object, composed of nested groups and dependency factories. Factories may return any JS value, e.g. constants, singletons and providers. Unlike Inversify, there is no need to decorate classes.

import { inject, Module } from 'djinject';

// Defining a _context_ of dependencies
type Context = {
    group: {
        value: Value // any JS type, here a class
    }
}

// A _module_ contains nested _groups_ (optional) and _factories_
const module = {
    group: {
        // a factory of type Factory<Context, Value>
        value: (ctx: Context) => new Value(ctx)
    }
} satisfies Module<Context>;

// A _container_ of type Container<[Module<Context>]> = Context
const container = inject(module);

// Values can be obtained from the container
const value = container.group.value;

Context

A container provides each factory with a parameter called context.

type C = {
    value: string
}

// ❌ compiler error: value is missing
const container = inject({
    factory: (ctx: C) => () => ctx.value
});

The context of type C provides a value that can't be resolved. The inject call is type-checked by TS the way that the completeness of the arguments is checked.

Such missing dependencies need to be provided by adding additional modules to the inject call.

// ✅ fixed, value is defined
const container = inject({
    createValue: (ctx: C) => () => ctx.value
}, {
    value: () => '🧞‍♀️'
});

Now the compiler is satisfied and we can start using the container.

// prints 🧞‍♀️
console.log(container.createValue());

You might have noticed that the container automatically injects itself as the context when calling the createValue function.

Eager vs lazy initialization

A dependency container.group.value is lazily initialized when first accessed on the container. Initialize a factory eagerly at the time of the inject call by wrapping it in an init call. Hint: groups can be eagerly initialized as well.

A use case for eager initialization would be to ensure that side effects take place during the initialization of the container.

import { init, inject, Module } from 'djinject';

type C = {
    logger: string
}

const module = {
    service: init(() => {
        console.log('Service initialized');
    })
} satisfies Module<C>;

const ctr = inject(module);

console.log('App started');

ctr.service

In the eager case, the output is

Service initialized
App started

In the lazy case, the output is

App started
Service initialized

Please note that eager factories overwrite lazy factories vice versa when rebinding them using additional modules in the inject call.

Rebinding dependencies

The main advantage of dependency injection arises from the fact that an application is able to rebind dependencies. That way the structure of a system can be fixated while the behavior can be changed.

The main vehicle for rebinding dependencies is the inject function which receives a variable amount of modules.

The behavior of an application can be enhanced by overwriting existing functionality using additional modules.

type C = {
    test: () => void
    eval: (a: number, b: number) => number
}

const m1 = {
    test: (ctx) => () => {
        console.log(ctx.eval(1, 1));
    },
    eval: () => (a, b) => a + b
} satisfies Module<C, C>; // requires C

const m2 = {
    eval: () => (a, b) => a * b
} satisfies Module<C>; // partial C

const ctr = inject(m1, m2);

// = 1
ctr.test();

About

Dependency injection done right.

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published