tiny-ioc
is a lightweight (less than 1kB) dependency injection / IOC container for TypeScript.
- ✅ 100% TypeScript
- ✅ lightweight - < 1kB
- ✅ bind & unbind dependencies
- ✅ bind singletons and reset if necessary
- ✅ bind factories
- ✅ support for swapping dependencies
- ✅ simplfies testing / mocking
npm i @drewjbartlett/tiny-ioc --save
Create a container.ts
file that creates, binds, and then exports the container.
// container.ts
import { createContainer, Scope } from '@drewjbartlett/tiny-ioc';
const container = createContainer();
container.bind(MyClass, () => new MyClass(), Scope.Singleton);
container.bindFactory(DataSource, () => new DataSource(container.get(HttpClient)));
container.bindSingleton(HttpClient, () => new HttpClient());
export { container }
// some-other-file.ts
import { container } from 'path/to/container';
import { DataSource } from 'path/to/data-source';
export async function makeRequest() {
try {
const dataSource = container.get(DataSource);
return await dataSource.get('/foo/bar');
} catch (e) {
//
}
}
Bind a dependency with a given scope.
import { createContainer, Scope } from '@drewjbartlett/tiny-ioc';
const container = createContainer();
container.bind(SomeClass, () => new SomeClass(container.get(AnotherClass)), Scope.Singleton);
container.bind(AnotherClass, () => new AnotherClass(), Scope.Factory);
Bind a dependency to the container as a singleton.
class Total {
constructor(public readonly count: number) {}
}
let count = 0;
container.bindSingleton(
Total,
() => {
count++;
return new Total(count);
},
);
container.get(Total).count; // 1
container.get(Total).count; // 1
container.get(Total).count; // 1
container.get(Total).count; // 1
Bind a dependency to the container as a factory. Each time the dependency is resolved the container will call the factory function.
class Total {
constructor(public readonly count: number) {}
}
let count = 0;
container.bindFactory(
Total,
() => {
count++;
return new Total(count);
},
);
container.get(Total).count; // 1
container.get(Total).count; // 2
container.get(Total).count; // 3
container.get(Total).count; // 4
Only bind the given value if there is not already a binding.
container.bindFactory(HttpClient, () => new HttpClient({ baseURL: 'baseURL 1' }));
container.bindOnce(HttpClient, () => new HttpClient({ baseURL: 'baseURL 2' }), Scope.Singleton);
container.get(HttpClient).baseURL // 'baseURL 1'
Attempt to resolve a given binding. Will throw a NotBoundException
if there is no binding found.
container.get(SomeDependency);
Reset a singleton value.
If a value has been previously resolved and is bound as a singleton, this will keep the binding but reset the singleton value until the next resolve. Take the example below. Each time the singleton dependency is built the count will increase. Since it's a singleton count
will always be 1. After resetting the singleton the new value is 2 since the factory function is called again.
let count = 0;
container.bindSingleton(
Total,
() => {
count++;
return new Total(count);
},
);
container.get(Total).count // 1
container.get(Total).count // 1
container.resetSingleton(Total);
container.get(Total).count // 2
Determine if a binding exists or not.
container.bound(HttpClient); // false
container.bindFactory(HttpClient, () => new HttpClient());
container.bound(HttpClient); // true
Remove the given binding from the container entirely.
container.bindFactory(HttpClient, () => new HttpClient());
container.get(HttpClient); // HttpClient
container.unbind(HttpClient);
container.get(HttpClient); // throws NotBoundException
Swap the old binding's value with the new value. This is useful when testing.
There may be times where swapping a dependency is necessary. Especially when testing. swap
allows for swapping out a dependency by a given class name.
class Tesla extends Car {
}
class Rivian extends Car {
}
container.bindFactory(Car, () => new Tesla());
container.get(Car); // Tesla
container.swap(Car, () => new Rivian());
container.get(Car); // Rivian
Unit testing is made very simple when using tiny-ioc
. You can simply swap out the real dependency for any mock dependency and the tests will reference your mock instead of the real thing.
// make-request.ts
import { container } from 'path/to/container';
import { HttpClient } from 'path/to/http-client';
export async function makeRequest() {
try {
const dataSource = container.get(HttpClient);
return await dataSource.get('/foo/bar');
} catch (e) {
//
}
}
// make-request.test.ts
import { container } from 'path/to/container';
import { HttpClient } from 'path/to/http-client';
import { makeRequest } from 'path/top/make-request';
class DummyHttpClient {
get(url: string) {
return dummyData;
}
}
it('should make the request', () => {
container.swap(HttpClient, () => new DummyHttpClient());
await myRequest(); // calls .get() on DummyHttpClient instead of HttpClient
})