Dependency Injection Container
Getting Started
class CookieStorage {}
class AuthStorage {
constructor(storage: CookieStorage) {}
}
import DIContainer, { object, get, factory, IDIContainer } from "rsdi";
export default function configureDI() {
const container = new DIContainer();
container.addDefinitions({
"ENV": "PRODUCTION",
"AuthStorage": object(AuthStorage).construct(
get("Storage")
),
"Storage": object(CookieStorage),
"BrowserHistory": factory(configureHistory),
});
return container;
}
function configureHistory(container: IDIContainer): History {
const history = createBrowserHistory();
const env = container.get("ENV");
if (env === "production") {
}
return history;
}
const container = configureDI();
const env = container.get<string>("ENV");
const authStorage = container.get<AuthStorage>("AuthStorage");
const history = container.get<History>("BrowserHistory");
All definitions are resolved once and their result persists over the life of the container.
Features
- Simple but powerful
- Does not requires decorators
- Works great with both javascript and typescript
Motivation
Popular solutions like inversify
or tsyringe
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.
Disadvantages of other solutions
- Those solutions work with typescript only. Since they rely on argument types that we don't have in Javascript.
- I have to update my tsconfig because one package requires it.
- Let my components know about injections.
@injectable()
class Foo {
}
Why component Foo should know that it's injectable?
More details thoughts in my blog article
Usage
Raw values
import DIContainer from "rsdi";
const container = new DIContainer();
container.addDefinitions({
"ENV": "PRODUCTION",
"AuthStorage": new AuthStorage(),
"BrowserHistory": createBrowserHistory(),
});
const env = container.get<string>("ENV");
const authStorage = container.get<AuthStorage>("AuthStorage");
const authStorage = container.get<History>("BrowserHistory");
When you specify raw values (i.e. don't use object
, factory
definitions) rsdi
will resolve it as it is.
Object definition
import DIContainer, { object, get } from "rsdi";
const container = new DIContainer();
container.addDefinitions({
"Storage": object(CookieStorage),
"AuthStorage": object(AuthStorage).construct(
get("Storage")
),
"UsersController": object(UserController),
"PostsController": object(PostsController),
"ControllerContainer": object(ControllerContainer)
.method('addController', get("UsersController"))
.method('addController', get("PostsController"))
});
object(ClassName)
- the simplest scenario that will call 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 get
helper.
If you need to call object method after initialization you can use method
it will be called after constructor.
Factory definition
You can use factory definition 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.
import DIContainer, { factory, IDIContainer } from "rsdi";
const container = new DIContainer();
container.addDefinitions({
"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");
Async factory definition
RSDI intentionally does not provide the ability to resolve asynchronous dependencies. The container works with
resources. All resources will be needed 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 themselves.
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, IDIContainer } from "rsdi";
async function configureDI() {}
const dbConnection = await createConnections();
const container = new DIContainer();
container.addDefinitions({
"DbConnection": dbConnection,
"UserRepository": object(UserRepository).construct(
get("DbConnection")
)
});
return container;
}
const diContainer = await configureDI();
const userRepository = diContainer.get<UserRepository>("UserRepository");