Security News
Weekly Downloads Now Available in npm Package Search Results
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
ts-ioc-container
Advanced tools
typescript
tagged scopes
@inject
tags
lazy
@provider
singleton
args
argsFn
visible
alias
decorate
@register
@hook
@onConstruct
@onDispose
npm install ts-ioc-container reflect-metadata
yarn add ts-ioc-container reflect-metadata
Just put it in the entrypoint file of your project. It should be the first line of the code.
import 'reflect-metadata';
And tsconfig.json
should have next options:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
IContainer
consists of:
import 'reflect-metadata';
import { IContainer, by, Container, inject, MetadataInjector, Registration as R } from 'ts-ioc-container';
describe('Basic usage', function () {
class Logger {
name = 'Logger';
}
it('should inject dependencies', function () {
class App {
constructor(@inject(by.key('ILogger')) public logger: Logger) {}
}
const container = new Container(new MetadataInjector()).add(R.toClass(Logger).fromKey('ILogger'));
expect(container.resolve(App).logger.name).toBe('Logger');
});
it('should inject multiple dependencies', function () {
class App {
constructor(@inject(by.keys(['ILogger1', 'ILogger2'])) public loggers: Logger[]) {}
}
const container = new Container(new MetadataInjector())
.add(R.toClass(Logger).fromKey('ILogger1'))
.add(R.toClass(Logger).fromKey('ILogger2'));
expect(container.resolve(App).loggers).toHaveLength(2);
});
it('should inject current scope', function () {
const root = new Container(new MetadataInjector(), { tags: ['root'] });
class App {
constructor(@inject(by.scope.current) public scope: IContainer) {}
}
const app = root.resolve(App);
expect(app.scope).toBe(root);
});
});
Sometimes you need to create a scope of container. For example, when you want to create a scope per request in web application. You can assign tags to scope and provider and resolve dependencies only from certain scope.
clone
and isValid
to clone itself and check if it's valid for certain scope accordingly.import 'reflect-metadata';
import {
IContainer,
inject,
singleton,
Container,
DependencyNotFoundError,
key,
provider,
MetadataInjector,
Registration as R,
by,
scope,
register,
Tag,
} from 'ts-ioc-container';
@register(key('ILogger'), scope((s) => s.hasTag('child')))
@provider(singleton())
class Logger {}
describe('Scopes', function () {
it('should resolve dependencies from scope', function () {
const root = new Container(new MetadataInjector(), { tags: ['root'] }).add(R.toClass(Logger));
const child = root.createScope({ tags: ['child'] });
expect(child.resolve('ILogger')).toBe(child.resolve('ILogger'));
expect(() => root.resolve('ILogger')).toThrow(DependencyNotFoundError);
});
it('should inject new scope', function () {
const root = new Container(new MetadataInjector(), { tags: ['root'] });
class App {
constructor(@inject(by.scope.create({ tags: ['child'] })) public scope: IContainer) {}
}
const app = root.resolve(App);
expect(app.scope).not.toBe(root);
expect(app.scope.hasTag('child')).toBe(true);
});
it('should get path by reduceToRoot', () => {
const root = new Container(new MetadataInjector(), { tags: ['root'] });
const child = root.createScope({ tags: ['child'] });
const grandChild = child.createScope({ tags: ['grandChild'] });
const collectTags = (acc: Array<Tag[]>, c: IContainer) => [...acc, Array.from(c.tags)];
const tagsPath = grandChild.reduceToRoot(collectTags, []);
expect(tagsPath).toEqual([['root'], ['child'], ['grandChild']]);
const actual = root.findChild((s) => {
const current = tagsPath.shift() ?? [];
const a = current.every((t) => s.hasTag(t));
const b = tagsPath.length === 0;
return a && b;
});
expect(actual).toBe(grandChild);
});
it('should be able to create scope idempotently', () => {
const root = new Container(new MetadataInjector(), { tags: ['root'] });
const child1 = root.createScope({ tags: ['child', 'a'], idempotent: true });
const child2 = root.createScope({ tags: ['child', 'a'], idempotent: true });
const child3 = root.createScope({ tags: ['child'], idempotent: true });
expect(child1).toBe(child2);
expect(child3).not.toBe(child2);
});
});
Sometimes you want to get all instances from container and its scopes. For example, when you want to dispose all instances of container.
import 'reflect-metadata';
import { inject, key, Registration as R, Container, MetadataInjector, by, register } from 'ts-ioc-container';
describe('Instances', function () {
@register(key('ILogger'))
class Logger {}
it('should return injected instances', () => {
const container = new Container(new MetadataInjector()).add(R.toClass(Logger));
const scope = container.createScope();
const logger1 = container.resolve('ILogger');
const logger2 = scope.resolve('ILogger');
expect(scope.getInstances().length).toBe(1);
expect(container.getInstances().length).toBe(2);
});
it('should return injected instances by decorator', () => {
const isLogger = (instance: unknown) => instance instanceof Logger;
class App {
constructor(@inject(by.instances(isLogger)) public loggers: Logger[]) {}
}
const container = new Container(new MetadataInjector()).add(R.toClass(Logger));
const logger0 = container.resolve('ILogger');
const logger1 = container.resolve('ILogger');
const app = container.resolve(App);
expect(app.loggers).toHaveLength(2);
expect(app.loggers[0]).toBe(logger0);
expect(app.loggers[1]).toBe(logger1);
});
});
Sometimes you want to dispose container and all its scopes. For example, when you want to prevent memory leaks. Or you want to ensure that nobody can use container after it was disposed.
import 'reflect-metadata';
import { Container, ContainerDisposedError, MetadataInjector, Registration as R } from 'ts-ioc-container';
class Logger {}
describe('Disposing', function () {
it('should container and make it unavailable for the further usage', function () {
const root = new Container(new MetadataInjector(), { tags: ['root'] }).add(R.toClass(Logger).fromKey('ILogger'));
const child = root.createScope({ tags: ['child'] });
const logger = child.resolve('ILogger');
root.dispose();
expect(() => child.resolve('ILogger')).toThrow(ContainerDisposedError);
expect(() => root.resolve('ILogger')).toThrow(ContainerDisposedError);
expect(root.getInstances().length).toBe(0);
});
});
Sometimes you want to create dependency only when somebody want to invoke it's method or property. This is what lazy
is for.
import { by, Container, inject, MetadataInjector, provider, Registration as R, singleton } from 'ts-ioc-container';
describe('lazy provider', () => {
@provider(singleton())
class Flag {
isSet = false;
set() {
this.isSet = true;
}
}
class Service {
name = 'Service';
constructor(@inject(by.key('Flag')) private flag: Flag) {
this.flag.set();
}
greet() {
return 'Hello';
}
}
class App {
constructor(@inject(by.key('Service', { lazy: true })) public service: Service) {}
run() {
return this.service.greet();
}
}
function createContainer() {
const container = new Container(new MetadataInjector());
container.add(R.toClass(Flag)).add(R.toClass(Service));
return container;
}
it('should not create an instance until method is not invoked', () => {
// Arrange
const container = createContainer();
// Act
const app = container.resolve(App);
const flag = container.resolve<Flag>('Flag');
// Assert
expect(flag.isSet).toBe(false);
});
it('should create an instance only when some method/property is invoked', () => {
// Arrange
const container = createContainer();
// Act
const app = container.resolve(App);
const flag = container.resolve<Flag>('Flag');
// Assert
expect(app.run()).toBe('Hello');
expect(flag.isSet).toBe(true);
});
it('should not create instance on every method invoked', () => {
// Arrange
const container = createContainer();
// Act
const app = container.resolve(App);
// Assert
expect(app.run()).toBe('Hello');
expect(app.run()).toBe('Hello');
expect(container.getInstances().filter((x) => x instanceof Service).length).toBe(1);
});
it('should create instance when property is invoked', () => {
// Arrange
const container = createContainer();
// Act
const app = container.resolve(App);
const flag = container.resolve<Flag>('Flag');
// Assert
expect(app.service.name).toBe('Service');
expect(flag.isSet).toBe(true);
});
});
IInjector
is used to describe how dependencies should be injected to constructor.
MetadataInjector
- injects dependencies using @inject
decoratorProxyInjector
- injects dependencies as dictionary Record<string, unknown>
SimpleInjector
- just passes container to constructor with others argumentsThis type of injector uses @inject
decorator to mark where dependencies should be injected. It's bases on reflect-metadata
package. That's why I call it MetadataInjector
.
Also you can inject property.
import 'reflect-metadata';
import { by, Container, inject, MetadataInjector, Registration as R } from 'ts-ioc-container';
class Logger {
name = 'Logger';
}
class App {
constructor(@inject(by.key('ILogger')) private logger: Logger) {}
// OR
// constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {
// }
getLoggerName(): string {
return this.logger.name;
}
}
describe('Reflection Injector', function () {
it('should inject dependencies by @inject decorator', function () {
const container = new Container(new MetadataInjector()).add(R.toClass(Logger).fromKey('ILogger'));
const app = container.resolve(App);
expect(app.getLoggerName()).toBe('Logger');
});
});
This type of injector just passes container to constructor with others arguments.
import 'reflect-metadata';
import { Container, IContainer, Registration as R, SimpleInjector } from 'ts-ioc-container';
describe('SimpleInjector', function () {
it('should pass container as first parameter', function () {
class App {
constructor(public container: IContainer) {}
}
const container = new Container(new SimpleInjector()).add(R.toClass(App).fromKey('App'));
const app = container.resolve<App>('App');
expect(app.container).toBeInstanceOf(Container);
});
it('should pass parameters alongside with container', function () {
class App {
constructor(
container: IContainer,
public greeting: string,
) {}
}
const container = new Container(new SimpleInjector()).add(R.toClass(App).fromKey('App'));
const app = container.resolve<App>('App', { args: ['Hello world'] });
expect(app.greeting).toBe('Hello world');
});
});
This type of injector injects dependencies as dictionary Record<string, unknown>
.
import 'reflect-metadata';
import { Container, ProxyInjector, args, Registration as R } from 'ts-ioc-container';
describe('ProxyInjector', function () {
it('should pass dependency to constructor as dictionary', function () {
class Logger {}
class App {
logger: Logger;
constructor({ logger }: { logger: Logger }) {
this.logger = logger;
}
}
const container = new Container(new ProxyInjector()).add(R.toClass(Logger).fromKey('logger'));
const app = container.resolve(App);
expect(app.logger).toBeInstanceOf(Logger);
});
it('should pass arguments as objects', function () {
class Logger {}
class App {
logger: Logger;
greeting: string;
constructor({
logger,
greetingTemplate,
name,
}: {
logger: Logger;
greetingTemplate: (name: string) => string;
name: string;
}) {
this.logger = logger;
this.greeting = greetingTemplate(name);
}
}
const greetingTemplate = (name: string) => `Hello ${name}`;
const container = new Container(new ProxyInjector())
.add(R.toClass(App).fromKey('App').pipe(args({ greetingTemplate })))
.add(R.toClass(Logger).fromKey('logger'));
const app = container.resolve<App>('App', { args: [{ name: `world` }] });
expect(app.greeting).toBe('Hello world');
});
});
Provider is dependency factory which creates dependency.
@provider()
Provider.fromClass(Logger)
Provider.fromValue(logger)
new Provider((container, ...args) => container.resolve(Logger, {args}))
import 'reflect-metadata';
import { singleton, Container, Provider, MetadataInjector, scope } from 'ts-ioc-container';
class Logger {}
describe('Provider', function () {
it('can be registered as a function', function () {
const container = new Container(new MetadataInjector()).register('ILogger', new Provider(() => new Logger()));
expect(container.resolve('ILogger')).not.toBe(container.resolve('ILogger'));
});
it('can be registered as a value', function () {
const container = new Container(new MetadataInjector()).register('ILogger', Provider.fromValue(new Logger()));
expect(container.resolve('ILogger')).toBe(container.resolve('ILogger'));
});
it('can be registered as a class', function () {
const container = new Container(new MetadataInjector()).register('ILogger', Provider.fromClass(Logger));
expect(container.resolve('ILogger')).not.toBe(container.resolve('ILogger'));
});
it('can be featured by pipe method', function () {
const root = new Container(new MetadataInjector(), { tags: ['root'] }).register(
'ILogger',
Provider.fromClass(Logger).pipe(singleton()),
);
expect(root.resolve('ILogger')).toBe(root.resolve('ILogger'));
});
});
Sometimes you need to create only one instance of dependency per scope. For example, you want to create only one logger per scope.
import 'reflect-metadata';
import { singleton, Container, key, provider, MetadataInjector, Registration as R, register } from 'ts-ioc-container';
@register(key('logger'))
@provider(singleton())
class Logger {}
describe('Singleton', function () {
function createContainer() {
return new Container(new MetadataInjector());
}
it('should resolve the same container per every request', function () {
const container = createContainer().add(R.toClass(Logger));
expect(container.resolve('logger')).toBe(container.resolve('logger'));
});
it('should resolve different dependency per scope', function () {
const container = createContainer().add(R.toClass(Logger));
const child = container.createScope();
expect(container.resolve('logger')).not.toBe(child.resolve('logger'));
});
it('should resolve the same dependency for scope', function () {
const container = createContainer().add(R.toClass(Logger));
const child = container.createScope();
expect(child.resolve('logger')).toBe(child.resolve('logger'));
});
});
Sometimes you want to bind some arguments to provider. This is what ArgsProvider
is for.
@provider(args('someArgument'))
@provider(argsFn((container) => [container.resolve(Logger), 'someValue']))
Provider.fromClass(Logger).pipe(args('someArgument'))
resolve
method.import 'reflect-metadata';
import {
args,
argsFn,
Container,
DependencyKey,
inject,
key,
MetadataInjector,
MultiCache,
provider,
register,
Registration as R,
singleton,
} from 'ts-ioc-container';
@register(key('logger'))
class Logger {
constructor(
public name: string,
public type?: string,
) {}
}
describe('ArgsProvider', function () {
function createContainer() {
return new Container(new MetadataInjector());
}
it('can assign argument function to provider', function () {
const root = createContainer().add(R.toClass(Logger).pipe(argsFn((container, ...args) => ['name'])));
const logger = root.createScope().resolve<Logger>('logger');
expect(logger.name).toBe('name');
});
it('can assign argument to provider', function () {
const root = createContainer().add(R.toClass(Logger).pipe(args('name')));
const logger = root.resolve<Logger>('logger');
expect(logger.name).toBe('name');
});
it('should set provider arguments with highest priority in compare to resolve arguments', function () {
const root = createContainer().add(R.toClass(Logger).pipe(args('name')));
const logger = root.resolve<Logger>('logger', { args: ['file'] });
expect(logger.name).toBe('name');
expect(logger.type).toBe('file');
});
it('should resolve dependency by passing arguments resolve from container by another argument', function () {
interface IRepository {
name: string;
}
@register(key('UserRepository'))
class UserRepository implements IRepository {
name = 'UserRepository';
}
@register(key('TodoRepository'))
class TodoRepository implements IRepository {
name = 'TodoRepository';
}
@register(key('EntityManager'))
@provider(argsFn((container, token) => [container.resolve(token as DependencyKey)]))
class EntityManager {
constructor(public repository: IRepository) {}
}
class Main {
constructor(
@inject((s) => s.resolve('EntityManager', { args: ['UserRepository'] })) public userEntities: EntityManager,
@inject((s) => s.resolve('EntityManager', { args: ['TodoRepository'] })) public todoEntities: EntityManager,
) {}
}
const root = createContainer()
.add(R.toClass(EntityManager))
.add(R.toClass(UserRepository))
.add(R.toClass(TodoRepository));
const main = root.resolve(Main);
expect(main.userEntities.repository).toBeInstanceOf(UserRepository);
expect(main.todoEntities.repository).toBeInstanceOf(TodoRepository);
});
it('should resolve memoized dependency by passing arguments resolve from container by another argument', function () {
interface IRepository {
name: string;
}
@register(key('UserRepository'))
class UserRepository implements IRepository {
name = 'UserRepository';
}
@register(key('TodoRepository'))
class TodoRepository implements IRepository {
name = 'TodoRepository';
}
@register(key('EntityManager'))
@provider(
argsFn((container, token) => [container.resolve(token as DependencyKey)]),
singleton(() => new MultiCache((...args: unknown[]) => args[0] as DependencyKey)),
)
class EntityManager {
constructor(public repository: IRepository) {}
}
class Main {
constructor(
@inject((s) => s.resolve('EntityManager', { args: ['UserRepository'] })) public userEntities: EntityManager,
@inject((s) => s.resolve('EntityManager', { args: ['TodoRepository'] })) public todoEntities: EntityManager,
) {}
}
const root = createContainer()
.add(R.toClass(EntityManager))
.add(R.toClass(UserRepository))
.add(R.toClass(TodoRepository));
const main = root.resolve(Main);
const userRepository = root.resolve<EntityManager>('EntityManager', { args: ['UserRepository'] }).repository;
expect(userRepository).toBeInstanceOf(UserRepository);
expect(main.userEntities.repository).toBe(userRepository);
const todoRepository = root.resolve<EntityManager>('EntityManager', { args: ['TodoRepository'] }).repository;
expect(todoRepository).toBeInstanceOf(TodoRepository);
expect(main.todoEntities.repository).toBe(todoRepository);
});
});
Sometimes you want to hide dependency if somebody wants to resolve it from certain scope
@provider(visible(({ isParent, child }) => isParent || child.hasTag('root')))
- dependency will be accessible from scope root
or from scope where it's registeredProvider.fromClass(Logger).pipe(visible(({ isParent, child }) => isParent || child.hasTag('root')))
import 'reflect-metadata';
import {
Container,
DependencyNotFoundError,
key,
MetadataInjector,
provider,
register,
Registration as R,
scope,
singleton,
visible,
} from 'ts-ioc-container';
describe('Visibility', function () {
it('should hide from children', () => {
@register(key('logger'), scope((s) => s.hasTag('root')))
@provider(singleton(), visible(({ isParent }) => isParent))
class FileLogger {}
const parent = new Container(new MetadataInjector(), { tags: ['root'] }).add(R.toClass(FileLogger));
const child = parent.createScope({ tags: ['child'] });
expect(() => child.resolve('logger')).toThrowError(DependencyNotFoundError);
expect(parent.resolve('logger')).toBeInstanceOf(FileLogger);
});
});
Alias is needed to group keys
@provider(alias('logger'))
helper assigns logger
alias to registration.by.aliases((it) => it.has('logger') || it.has('a'))
resolves dependencies which have logger
or a
aliasesProvider.fromClass(Logger).pipe(alias('logger'))
import 'reflect-metadata';
import {
alias,
byAlias,
byAliases,
Container,
DependencyNotFoundError,
IMemo,
IMemoKey,
inject,
MetadataInjector,
Provider,
provider,
register,
Registration as R,
scope,
} from 'ts-ioc-container';
import { constant } from '../../lib/utils';
describe('alias', () => {
const IMiddlewareKey = 'IMiddleware';
const middleware = provider(alias(IMiddlewareKey));
interface IMiddleware {
applyTo(application: IApplication): void;
}
interface IApplication {
use(module: IMiddleware): void;
markMiddlewareAsApplied(name: string): void;
}
@middleware
class LoggerMiddleware implements IMiddleware {
applyTo(application: IApplication): void {
application.markMiddlewareAsApplied('LoggerMiddleware');
}
}
@middleware
class ErrorHandlerMiddleware implements IMiddleware {
applyTo(application: IApplication): void {
application.markMiddlewareAsApplied('ErrorHandlerMiddleware');
}
}
it('should resolve by some alias', () => {
class App implements IApplication {
private appliedMiddleware: Set<string> = new Set();
constructor(@inject(byAliases((it) => it.has(IMiddlewareKey))) public middleware: IMiddleware[]) {}
markMiddlewareAsApplied(name: string): void {
this.appliedMiddleware.add(name);
}
isMiddlewareApplied(name: string): boolean {
return this.appliedMiddleware.has(name);
}
use(module: IMiddleware): void {
module.applyTo(this);
}
run() {
for (const module of this.middleware) {
module.applyTo(this);
}
}
}
const container = new Container(new MetadataInjector())
.add(R.toClass(LoggerMiddleware))
.add(R.toClass(ErrorHandlerMiddleware));
const app = container.resolve(App);
app.run();
expect(app.isMiddlewareApplied('LoggerMiddleware')).toBe(true);
expect(app.isMiddlewareApplied('ErrorHandlerMiddleware')).toBe(true);
});
it('should resolve by some alias', () => {
@provider(alias('ILogger'))
class FileLogger {}
const container = new Container(new MetadataInjector()).add(R.toClass(FileLogger));
expect(byAlias((aliases) => aliases.has('ILogger'))(container)).toBeInstanceOf(FileLogger);
expect(() => byAlias((aliases) => aliases.has('logger'))(container)).toThrowError(DependencyNotFoundError);
});
it('should resolve by memoized alias', () => {
@provider(alias('ILogger'))
@register(scope((s) => s.hasTag('root')))
class FileLogger {}
@provider(alias('ILogger'))
@register(scope((s) => s.hasTag('child')))
class DbLogger {}
const container = new Container(new MetadataInjector(), { tags: ['root'] })
.register(IMemoKey, Provider.fromValue<IMemo>(new Map()))
.add(R.toClass(FileLogger))
.add(R.toClass(DbLogger));
const result1 = byAlias((aliases) => aliases.has('ILogger'), { memoize: constant('ILogger') })(container);
const child = container.createScope({ tags: ['child'] });
const result2 = byAlias((aliases) => aliases.has('ILogger'), { memoize: constant('ILogger') })(child);
const result3 = byAlias((aliases) => aliases.has('ILogger'))(child);
expect(result1).toBeInstanceOf(FileLogger);
expect(result2).toBeInstanceOf(FileLogger);
expect(result3).toBeInstanceOf(DbLogger);
});
it('should resolve by memoized aliases', () => {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface ILogger {}
@provider(alias('ILogger'))
class FileLogger implements ILogger {}
@provider(alias('ILogger'))
class DbLogger implements ILogger {}
class App {
constructor(
@inject(byAliases((it) => it.has('ILogger'), { memoize: constant('ILogger') })) public loggers: ILogger[],
) {}
}
const container = new Container(new MetadataInjector())
.register(IMemoKey, Provider.fromValue<IMemo>(new Map()))
.add(R.toClass(FileLogger));
const loggers = container.resolve(App).loggers;
container.add(R.toClass(DbLogger));
const loggers2 = container.resolve(App).loggers;
expect(loggers).toEqual(loggers2);
});
});
Sometimes you want to decorate you class with some logic. This is what DecoratorProvider
is for.
@provider(decorate((instance, container) => new LoggerDecorator(instance)))
import {
by,
Container,
decorate,
IContainer,
inject,
key,
MetadataInjector,
provider,
register,
Registration as R,
singleton,
} from 'ts-ioc-container';
describe('lazy provider', () => {
@provider(singleton())
class Logger {
private logs: string[] = [];
log(message: string) {
this.logs.push(message);
}
printLogs() {
return this.logs.join(',');
}
}
interface IRepository {
save(item: Todo): Promise<void>;
}
interface Todo {
id: string;
text: string;
}
class LogRepository implements IRepository {
constructor(
private repository: IRepository,
@inject(by.key('Logger')) private logger: Logger,
) {}
async save(item: Todo): Promise<void> {
this.logger.log(item.id);
return this.repository.save(item);
}
}
const logRepo = (dep: IRepository, scope: IContainer) => scope.resolve(LogRepository, { args: [dep] });
@register(key('IRepository'))
@provider(decorate(logRepo))
class TodoRepository implements IRepository {
async save(item: Todo): Promise<void> {
console.log('Saving todo item', item);
}
}
class App {
constructor(@inject(by.key('IRepository')) public repository: IRepository) {}
async run() {
await this.repository.save({ id: '1', text: 'Hello' });
await this.repository.save({ id: '2', text: 'Hello' });
}
}
function createContainer() {
const container = new Container(new MetadataInjector());
container.add(R.toClass(TodoRepository)).add(R.toClass(Logger));
return container;
}
it('should decorate repo by logger middleware', async () => {
// Arrange
const container = createContainer();
// Act
const app = container.resolve(App);
const logger = container.resolve<Logger>('Logger');
await app.run();
// Assert
expect(logger.printLogs()).toBe('1,2');
});
});
Registration is provider factory which registers provider in container.
@register(key('logger'))
Registration.fromClass(Logger).to('logger')
Registration.fromClass(Logger)
Registration.fromValue(Logger)
Registration.fromFn((container, ...args) => container.resolve(Logger, {args}))
Sometimes you want to register provider with certain key. This is what key
is for.
import 'reflect-metadata';
import { Container, key, MetadataInjector, provider, register, Registration as R, scope, singleton } from 'ts-ioc-container';
import { DependencyMissingKeyError } from '../../lib/errors/DependencyMissingKeyError';
describe('Registration module', function () {
const createContainer = () => new Container(new MetadataInjector(), { tags: ['root'] });
it('should register class', function () {
@register(key('ILogger'), scope((s) => s.hasTag('root')))
@provider(singleton())
class Logger {}
const root = createContainer().add(R.toClass(Logger));
expect(root.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register value', function () {
const root = createContainer().add(R.toValue('smth').fromKey('ISmth'));
expect(root.resolve('ISmth')).toBe('smth');
});
it('should register fn', function () {
const root = createContainer().add(R.toFn(() => 'smth').fromKey('ISmth'));
expect(root.resolve('ISmth')).toBe('smth');
});
it('should raise an error if key is not provider', () => {
expect(() => {
createContainer().add(R.toValue('smth'));
}).toThrowError(DependencyMissingKeyError);
});
it('should register dependency by class name if @key is not provided', function () {
class FileLogger {}
const root = createContainer().add(R.toClass(FileLogger));
expect(root.resolve('FileLogger')).toBeInstanceOf(FileLogger);
});
it('should assign additional key which redirects to original one', function () {
@register(key('ILogger', 'Logger'))
@provider(singleton())
class Logger {}
const root = createContainer().add(R.toClass(Logger));
expect(root.resolve('Logger')).toBeInstanceOf(Logger);
expect(root.resolve('Logger')).toBe(root.resolve('ILogger'));
});
});
Sometimes you need to register provider only in scope which matches to certain condition and their sub scopes. Especially if you want to register dependency as singleton for some tags, for example root
@register(scope((container) => container.hasTag('root'))
- register provider only in root scopeRegistration.fromClass(Logger).when((container) => container.hasTag('root'))
import 'reflect-metadata';
import { singleton, Container, key, provider, MetadataInjector, Registration as R, scope, register } from 'ts-ioc-container';
@register(key('ILogger'), scope((s) => s.hasTag('root')))
@provider(singleton()) // the same as .pipe(singleton(), scope((s) => s.hasTag('root')))
class Logger {}
describe('ScopeProvider', function () {
it('should return the same instance', function () {
const root = new Container(new MetadataInjector(), { tags: ['root'] }).add(R.toClass(Logger));
const child = root.createScope();
expect(root.resolve('ILogger')).toBe(child.resolve('ILogger'));
});
});
Sometimes you want to encapsulate registration logic in separate module. This is what IContainerModule
is for.
import 'reflect-metadata';
import { IContainerModule, Registration as R, IContainer, key, Container, MetadataInjector, register } from 'ts-ioc-container';
@register(key('ILogger'))
class Logger {}
@register(key('ILogger'))
class TestLogger {}
class Production implements IContainerModule {
applyTo(container: IContainer): void {
container.add(R.toClass(Logger));
}
}
class Development implements IContainerModule {
applyTo(container: IContainer): void {
container.add(R.toClass(TestLogger));
}
}
describe('Container Modules', function () {
function createContainer(isProduction: boolean) {
return new Container(new MetadataInjector()).use(isProduction ? new Production() : new Development());
}
it('should register production dependencies', function () {
const container = createContainer(true);
expect(container.resolve('ILogger')).toBeInstanceOf(Logger);
});
it('should register development dependencies', function () {
const container = createContainer(false);
expect(container.resolve('ILogger')).toBeInstanceOf(TestLogger);
});
});
Sometimes you need to invoke methods after construct or dispose of class. This is what hooks are for.
import 'reflect-metadata';
import {
constructor,
Container,
hook,
IContainer,
IInjector,
InjectOptions,
key,
MetadataInjector,
register,
Registration as R,
runHooks,
} from 'ts-ioc-container';
class MyInjector implements IInjector {
private injector = new MetadataInjector();
resolve<T>(container: IContainer, value: constructor<T>, options: InjectOptions): T {
const instance = this.injector.resolve(container, value, options);
runHooks(instance as object, 'onConstruct', { scope: container });
return instance;
}
}
@register(key('logger'))
class Logger {
isReady = false;
@hook('onConstruct', (context) => {
context.invokeMethod({ args: [] });
}) // <--- or extract it to @onConstruct
initialize() {
this.isReady = true;
}
log(message: string): void {
console.log(message);
}
}
describe('onConstruct', function () {
it('should make logger be ready on resolve', function () {
const container = new Container(new MyInjector()).add(R.toClass(Logger));
const logger = container.resolve<Logger>('logger');
expect(logger.isReady).toBe(true);
});
});
import 'reflect-metadata';
import {
by,
Container,
hook,
inject,
key,
MetadataInjector,
provider,
register,
Registration as R,
runHooks,
singleton,
} from 'ts-ioc-container';
@register(key('logsRepo'))
@provider(singleton())
class LogsRepo {
savedLogs: string[] = [];
saveLogs(messages: string[]) {
this.savedLogs.push(...messages);
}
}
@register(key('logger'))
class Logger {
@hook('onDispose', ({ instance, methodName }) => {
// @ts-ignore
instance[methodName].push('world');
}) // <--- or extract it to @onDispose
private messages: string[] = [];
constructor(@inject(by.key('logsRepo')) private logsRepo: LogsRepo) {}
log(@inject(by.key('logsRepo')) message: string): void {
this.messages.push(message);
}
size(): number {
return this.messages.length;
}
@hook('onDispose', (c) => {
c.invokeMethod({ args: [] });
}) // <--- or extract it to @onDispose
async save(): Promise<void> {
this.logsRepo.saveLogs(this.messages);
}
}
describe('onDispose', function () {
it('should invoke hooks on all instances', async function () {
const container = new Container(new MetadataInjector()).add(R.toClass(Logger)).add(R.toClass(LogsRepo));
const logger = container.resolve<Logger>('logger');
logger.log('Hello');
for (const instance of container.getInstances()) {
runHooks(instance as object, 'onDispose', { scope: container });
}
expect(container.resolve<LogsRepo>('logsRepo').savedLogs.join(',')).toBe('Hello,world');
});
});
import { by, Container, hook, injectProp, MetadataInjector, Registration, runHooks, runHooksAsync } from 'ts-ioc-container';
describe('inject property', () => {
it('should inject property', () => {
class App {
@hook('onInit', injectProp(by.key('greeting')))
greeting!: string;
}
const expected = 'Hello world!';
const container = new Container(new MetadataInjector()).add(Registration.toValue(expected).fromKey('greeting'));
const app = container.resolve(App);
runHooks(app as object, 'onInit', { scope: container });
expect(app.greeting).toBe(expected);
});
});
Sometimes you need to automatically mock all dependencies in container. This is what AutoMockedContainer
is for.
import { AutoMockedContainer, Container, DependencyKey, MetadataInjector } from 'ts-ioc-container';
import { IMock, Mock } from 'moq.ts';
export class MoqContainer extends AutoMockedContainer {
private mocks = new Map<DependencyKey, IMock<any>>();
resolve<T>(key: DependencyKey): T {
return this.resolveMock<T>(key).object();
}
resolveMock<T>(key: DependencyKey): IMock<T> {
if (!this.mocks.has(key)) {
this.mocks.set(key, new Mock());
}
return this.mocks.get(key) as IMock<T>;
}
}
interface IEngine {
getRegistrationNumber(): string;
}
describe('Mocking', () => {
it('should auto-mock dependencies', () => {
const mockContainer = new MoqContainer();
const container = new Container(new MetadataInjector(), { parent: mockContainer });
const engineMock = mockContainer.resolveMock<IEngine>('IEngine');
engineMock.setup((i) => i.getRegistrationNumber()).returns('123');
const engine = container.resolve<IEngine>('IEngine');
expect(engine.getRegistrationNumber()).toBe('123');
});
});
FAQs
Typescript IoC container
We found that ts-ioc-container demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
Security News
A Stanford study reveals 9.5% of engineers contribute almost nothing, costing tech $90B annually, with remote work fueling the rise of "ghost engineers."
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.