RSDI - Dependency Injection Container
Simple and powerful dependency injection container for JavaScript/TypeScript.
Getting Started
Given that you have classes and factories in your application
class CookieStorage {}
class AuthStorage {
constructor(storage: CookieStorage) {}
}
class Logger {}
class DummyLogger extends Logger {}
function loggerFactory(container: IDIContainer): Logger {
const env = container.get("ENV");
if (env === "test") {
return new DummyLogger();
}
return new Logger();
}
function UsersRepoFactory(knex: Knex): UsersRepo {
return {
async findById(id: number) {
await knex("users").where({ id });
},
};
}
Your DI container initialisation will include
import DIContainer, { object, use, factory, IDIContainer } from "rsdi";
export default function configureDI() {
const container: DIContainer = new DIContainer();
container.add({
ENV: "test",
Storage: object(CookieStorage),
AuthStorage: object(AuthStorage).construct(
use(Storage)
),
knex: knex(),
Logger: factory(loggerFactory),
UsersRepo: factory((container: IDIContainer) => {
return UsersRepoFactory(container.get("knex"));
}),
});
return container;
}
An entry point of your application will include
const container = configureDI();
const env: string = container.get("ENV");
const authStorage: AuthStorage = container.get(AuthStorage);
const logger: Logger = container.get(loggerFactory);
All resolvers are resolved only once and their result persists over the life of the container.
Features
- Simple but powerful
- Does not requires decorators
- Great types resolution
- Works great with both javascript and typescript
Motivation
Popular dependency injection libraries use reflect-metadata
that allows to fetch argument types and based on
those types and do autowiring. Autowiring is a nice feature but the trade-off is decorators.
@injectable()
class Foo {}
Why component Foo should know that it's injectable?
Your business logic depends on a specific framework that is not part of your domain model and can change.
More thoughts in a dedicated article
Usage
Raw values
Dependencies are set as raw values. No lazy initialisation happens. Container keeps and return raw values.
import DIContainer from "rsdi";
const container: DIContainer = new DIContainer();
container.add({
ENV: "PRODUCTION",
HTTP_PORT: 3000,
storage: new CookieStorage(),
});
const env: string = container.get("ENV");
const port: number = container.get("HTTP_PORT");
const authStorage: AuthStorage = container.get(AuthStorage);
Object resolver
object(ClassName)
- constructs an instance of the given class. The simplest scenario it calls the class constructor new ClassName()
.
When you need to pass arguments to the constructor, you can use construct
method. You can refer to the already defined
dependencies via the use
helper, or you can pass raw values.
If you need to call object method after initialization you can use method
it will be called after constructor.
class ControllerContainer {
constructor(authStorage: AuthStorage, logger: Logger) {}
add(controller: Controller) {
this.controllers.push(controller);
}
}
const container: DIContainer = new DIContainer();
container.add({
Storage: object(CookieStorage),
AuthStorage: object(AuthStorage).construct(
use(Storage)
),
UsersController: object(UserController),
PostsController: object(PostsController),
ControllerContainer: object(MainCliCommand)
.construct(use(AuthStorage), new Logger())
.method("add", use(UsersController))
.method("add", use(PostsController)),
});
Factory resolver
You can use factory resolver when you need more flexibility during initialisation. container: IDIContainer
will be
pass as an argument to the factory method. So you can resolve other dependencies inside the factory function.
const container: DIContainer = new DIContainer();
container.add({
BrowserHistory: factory(configureHistory),
});
function configureHistory(container: IDIContainer): History {
const history = createBrowserHistory();
const env = container.get("ENV");
if (env === "production") {
}
return history;
}
const history = container.get<History>("BrowserHistory");
Typescript type resolution
container.get
resolves type based on a configured container values
container.add({ key1: "value1", key2: 123, Foo: new Foo() });
const s: string = container.get("key1");
const i: number = container.get("key2");
const f: Foo = container.get("Foo");
container.get
and use
helper resolve type based on a given type name. Convention over configuration.
const container: DIContainer = new DIContainer();
container.add({
Bar: new Bar(),
Foo: new Bar(),
});
let bar: Bar = container.get(Bar);
let foo: Foo = container.get(Foo);
use
example
class Foo {
constructor(private readonly bar: Bar) {}
}
const container: DIContainer = new DIContainer();
container.add({
Bar: new Bar(),
Foo: object(Foo).construct(use(Bar)),
});
container.get
and use
helper resolve type based on a given factory return type.
function myFactory() {
return { a: 123 };
}
container.add({
myFactory: factory((container: IDIContainer) => {
return myFactory();
}),
});
let { a } = container.get(myFactory);
use
example
function customFunction() {
return { b: 123 };
}
const definition: DependencyResolver<{ b: number }> = use(customFunction);
Async factory resolver
RSDI intentionally does not provide the ability to resolve asynchronous dependencies. The container works with
resources. All resources will be used sooner or later. The lazy initialization feature won't be of much benefit
in this case. At the same time, mixing synchronous and asynchronous resolution will cause confusion primarily for
the consumers.
The following approach will work in most scenarios.
class UserRepository {
public constructor(private readonly dbConnection: any) {}
async findUser() {
return await this.dbConnection.find();
}
}
import { createConnections } from "my-orm-library";
import DIContainer, { factory, use, IDIContainer } from "rsdi";
async function configureDI() {
const dbConnection = await createConnections();
const container = new DIContainer();
container.addDefinitions({
DbConnection: dbConnection,
UserRepository: object(UserRepository).construct(use("DbConnection")),
});
return container;
}
const diContainer = await configureDI();
const userRepository = diContainer.get<UserRepository>("UserRepository");