README
factory-di
This library contains function to create some kind of Dependency Injection Containres. These containers do not use any global scope or metadata.
Advantages
- strict type checking that all required dependencies are registered
- lightweight
- no global containers
- no decorators
Simple example
foo.ts:
import { Class } from 'factory-di';
interface Database {
// ...
}
class Foo {
constructor(private database: Database) {}
}
// creates container which can create Foo instances
// and declare that database parameter of interface Database should be registered via token 'database'
export default Class(Foo, 'database');
database.ts:
import { Class } from 'factory-di';
class Database {
constructor(
private host: string,
private login: string,
private password: string
) {}
}
// creates container which can create Database instances and declare tokens for all parameters
export default Class(Database, 'dbHost', 'dbLogin', 'dbPassword');
index.ts:
import { singleton, constant } from 'factory-di';
import fooContainer from './foo';
import databaseContainer from './database';
// you can not create Foo via fooContainer here because its dependency 'database' is not registered yet
// this line would cause TS error
// fooContainer.resolve();
// creates a new container (each call of register function create a new independent containers)
const containerWithDatabase = fooContainer
// registers database as a singleton
.register('database', singleton(databaseContainer))
// you must register also all dependencies of 'database'
// or a call of resolve would cause TS error too
// you can register static values via 'constant' function
.register('dbHost', constant('<your_host>'))
.register('dbLogin', constant('<your_login>'))
.register('dbPassword', constant('<your_password>'));
// now you can create Foo
const fooInstance = containerWithDatabase.resolve();
// or you can create Database
const database = containerWithDatabase.resolve('database');
Containers
Containers are objects which can create some values. They are similar to factories. They do not use the global scope as regular Dependency Injection Containers.
Each container has a main value. It can be created via a resolve
call without parameters.
class Foo {}
const fooInstance = Class(Foo).resolve();
A container can have any list of dependencies. You should declare all dependencies when you create a container.
class Bar {
constructor(public foo: Foo) {}
}
// create a container and declare Foo dependency for the only argument of Bar constructor
const container = Class(Bar, 'Foo');
Each dependency has a token. You can use a string or a symbol as a dependency token.
Before you can call the resolve
method you must registed all declared dependencies. You can do it via the register
method. It receives a dependency token and a dependency container.
class Bar {
constructor(public foo: Foo) {}
}
const container = Class(Bar, 'Foo')
// registers Foo dependency with a new container
.register('Foo', Class(Foo));
const barInstance = container.resolve();
Multiple dependencies registrations can be union to a single 'register' method call.
class Bar {
constructor(public dep1: string, public dep2: number) {}
}
const container = Class(SomeClass, 'dep1', 'dep2')
// registers all dependencies via object
.register({
dep1: constant('myStringValue'),
dep2: constant(123),
});
If you want to register a simple value as a dependency via the register
method you can omit the constant
function call. This example is equal to the previous.
const container = Class(SomeClass, 'dep1', 'dep2')
// registers all dependencies via object
.register({
dep1: 'myStringValue',
dep2: 123,
});
If a container has unregistered dependencies you can pass them directly to the resolve
meethod to create a main value of the container.
const instance = Class(SomeClass, 'dep1', 'dep2').resolve({
dep1: 'myStringValue',
dep2: 123,
});
Also you can create any declared dependency. To do that you need to pass a token of the dependency.
const fooInstance = container.resolve('Foo');
Each call of the register
method returns a new inpedendent container.
Containers can have many levels of nesting. You can use the resolve
function to create values from any level of nesting.
Dependencies can be registered via a root container or via any nested container.
class Foo {}
class Bar {
constructor(public foo: Foo) {}
}
class Root {
constructor(public bar: Bar) {}
}
const root1 = Class(Root, 'bar')
.register('bar', Class(Bar, 'foo'))
.register('foo', Class(Foo)) // register foo via the Root container
.resolve();
const root2 = Class(Root, 'bar')
.register(
'bar',
Class(Bar, 'foo').register('foo', Class(Foo)) // register foo via the Bar container
)
.resolve();
If some dependency is registered twice in different child containers then each child container receives its own dependency value.
class Foo1 {
constructor(public str: string) {}
}
class Foo2 {
constructor(public str: string) {}
}
class Root {
constructor(public foo1: Foo1, public foo2: Foo2) {}
}
const root = Class(Root, 'foo1', 'foo2')
// register 'strValue1' via foo1
.register('foo1', Class(Foo, 'str').register('str', constant('strValue1')))
// register 'strValue2' via foo2
.register('foo2', Class(Foo, 'str').register('str', constant('strValue2')))
.resolve();
// each child instance has its own string value
root.foo1.str; // 'strValue1'
root.foo2.str; // 'strValue2'
If some dependency is registered via a parent container and via a any child container then the parent dependency value is used for parent and children containers.
class Foo1 {
constructor(public str: string) {}
}
class Root {
constructor(public foo1: Foo1) {}
}
const root = Class(Root, 'foo1', 'foo2')
// register 'strValue1' via foo1
.register('foo1', Class(Foo, 'str').register('str', constant('strValue1')))
// register 'strValueRoot'
.register('str', constant('strValueRoot'))
.resolve();
root.foo1.str; // 'strValueRoot'
Class
The Class
function can be used to create containers which create some class instances.
There are two form of the Class
function.
Each constructor dependency as a separate argument
The first form can be used for constructors which receive each dependency as a separate argument. The simplified type of the first form.
function Class(
// the first argument is a class constructor
Constructor: { new (...args: any[]): any },
// other arguments - list of tokens for each argument of the constructor
...tokens: Array<string | symbol>
): Container;
Example.
class MyClass {
constructor(public param1: string, public param2: number) {}
}
const myClassContainer = Class(MyClass, 'param1Token', 'param2Token');
const myClassInstance = myClassContainer
.register('param1Token', constant('strValue'))
.register('param2Token', constant(123))
.resolve();
Object with constructor dependencies
The second form can be used for constructors which receive an object with dependencies as the only argument. The simplified type of the second form.
function Class(
// the first argument is a class constructor
Constructor: { new (params: Record<string, any>): any },
// map of tokens where
// keys - keys of the constructor argument
// values - tokens for the corresponding argument
tokensMap: Record<string, string | symbol>
): Container;
Example.
interface MyClassParams {
strParam: string;
numParam: number;
}
class MyClass {
constructor(public params: MyClassParams) {}
}
const myClassContainer = Class(MyClass, {
strParam: 'param1Token',
numParam: 'param2Token',
});
const myClassInstance = myClassContainer
.register('param1Token', constant('strValue'))
.register('param2Token', constant(123))
.resolve();
myClassInstance.params.strParam; // 'strValue'
myClassInstance.params.numParam; // 123
Also dependencies can be registered inside the Class
function call. Then field names of the class first parameter will be used as tokens.
interface MyClassParams {
strParam: string;
numParam: number;
}
class MyClass {
constructor(public params: MyClassParams) {}
}
/**
* container has 2 unregistered dependencies with tokens
* - param1Token
* - param2Token
*/
const container1 = Class(MyClass, {
strParam: 'param1Token',
numParam: 'param2Token',
});
/**
* container has 2 registered dependencies with tokens
* - strParam
* - numParam
*/
const container2 = Class(MyClass, {
strParam: constant('strValue'),
numParam: constant(123),
});
computedValue
The computedValue
function can be used to create containers for any computed values.
There are two form of the computedValue
function.
Each computedValue dependency as a separate argument
The first form can be used for functions which receive each dependency as a separate argument. The simplified type of the first form.
function computedValue(
// the first argument is a function which creates some value
create: (...args: any[]): any,
// other arguments - list of tokens for each argument of the create function
...tokens: Array<string | symbol>
): Container;
Example.
const myContainer = computedValue(
// function can return any value
(param1: string, param2: number) => new MyClass(param1, param2),
'param1Token',
'param2Token'
);
const myValue = myContainer
.register('param1Token', constant('strValue'))
.register('param2Token', constant(123))
.resolve();
Object with computedValue dependencies
The second form can be used for functions which receive an object with dependencies as the only argument. The simplified type of the second form.
function computedValue(
// the first argument is a function which creates some value
create: (params: Record<string, any>): any,
// map of tokens where
// keys - keys of the create function argument
// values - tokens for the corresponding argument
tokensMap: Record<string, string | symbol>
): Container;
Example.
interface MyFactoryMethodParams {
strParam: string;
numParam: number;
}
const myContainer = computedValue(
// function can return any value
(params: MyFactoryMethodParams) => new MyClass(params),
{
strParam: 'param1Token',
numParam: 'param2Token',
}
);
const myValue = myContainer
.register('param1Token', constant('strValue'))
.register('param2Token', constant(123))
.resolve();
myValue.params.strParam; // 'strValue'
myValue.params.numParam; // 123
Also dependencies can be registered inside the computedValue
function call. Then field names of the factory method first parameter will be used as tokens.
interface MyFactoryMethodParams {
strParam: string;
numParam: number;
}
/**
* container has 2 unregistered dependencies with tokens
* - param1Token
* - param2Token
*/
const container1 = computedValue(
(params: MyFactoryMethodParams) => new MyClass(params),
{
strParam: 'param1Token',
numParam: 'param2Token',
}
);
/**
* container has 2 registered dependencies with tokens
* - strParam
* - numParam
*/
const container2 = computedValue(
(params: MyFactoryMethodParams) => new MyClass(params),
{
strParam: constant('strValue'),
numParam: constant(123),
}
);
constant
The constant
function can be used to create a container for some immutable value. The common case - to pass a constant container as a dependency of some other container.
function constant(value: any): Container;
Example.
const myConstantContainer = constant(99);
computedValue((num: number) => new MyClass(num), 'numValue')
.register('numValue', myConstantContainer)
.resolve();
factory
The factory
function can be used to create Factory method or some Factories.
type Resolve = (token: string | symbol) => any;
function factory(
// the only argument - a function which returns a value (usually a factory or a factory method)
create: (resolve: Resolve): any,
): Container;
The create
function (the only argument of factory
) receives the resolve
method. The resolve
method receives a dependency token and returns its value.
Example.
import { FactoryResolve, factory } from 'factory-di';
import { repositoryContainer } from './repository';
import { MyClass } from './myClass';
// dependencies of the factory method
interface MyFactoryMethodDependencies {
repository: Repository;
}
const myFactoryMethod = factory(
(resolve: FactoryResolve<MyFactoryMethodDependencies>) => {
// the factory method receives an id and return an instance
return (id: string) =>
new MyClass({
repository: resolve('repository'),
id,
});
}
)
// register the only dependency
.register('repository', repositoryContainer)
.resolve();
const myClassInstance1 = myFactoryMethod('id1');
const myClassInstance2 = myFactoryMethod('id2');
singleton
If you need some dependency to be singleton you can wrap any dependency container with the singleton
function.
import { Class, singleton } from 'factory-di';
class Foo {}
class Bar {
constructor(public foo: Foo) {}
}
const container = Class(Bar, 'Foo').register(
Bar,
// wrap Foo container with singleton
singleton(Class(Foo))
);
// now each Bar instance reveices the same Foo instance
const barInstance1 = container.resolve();
const barInstance2 = container.resolve();
Each call of singleton
creates an independent instance container. If you register some dependency via two nested containers with two calls of singleton
. Then these two nested containers will receive 2 different instances.
If you need nested containers receive the same singleton instance you should once wrap some container with singleton
and pass the wrapped container as a dependency to other containers. Or you should register a singleton once via a root container.
Clear singletons
To clear singleton instances you can get a Singleton Manager instance. You can get it via SingletonManagerKey
.
import { SingletonManagerKey } from 'factory-di';
const singletonManager = rootContainer.resolve(SingletonManagerKey);
A Singleton Manager instance has the only method 'clear' which delete all singleton instances created inside this container and inside all nested containers.