Skip to content

Repository files navigation

@hramini/ioc

A lightweight TypeScript Inversion of Control (IoC) library built on top of InversifyJS. It provides a clean, framework-agnostic facade for dependency injection with constructor and property injection support via decorators.

Table of Contents


Installation

npm install @hramini/ioc

Requirements

  • TypeScript with experimentalDecorators: true in tsconfig.json
  • reflect-metadata must be imported once at the entry point of your application, before any other import
  • Node.js or a browser environment with decorator support

Quick Start

import 'reflect-metadata';
import { Container, Ioc, InversifyContainer } from '@hramini/ioc';

interface IGreeter {
  greet(): string;
}

@Ioc.injectable()
class Greeter implements IGreeter {
  greet(): string {
    return 'Hello, World!';
  }
}

const container = new Container(new InversifyContainer());

container.singleton<IGreeter>({ key: 'IGreeter', Provider: Greeter });

const greeter = container.get<IGreeter>({ key: 'IGreeter' });

console.log({ greeter: greeter.greet() }); // { greeter: 'Hello, World!' }

API Reference

Container

The main facade for managing dependency bindings and resolutions. It delegates all operations to a provided IApplicationContainer implementation.

new Container(containerProvider: IApplicationContainer)
Method Signature Description
bind bind<T>(param: IContainerBindParam<T>): void Registers a transient binding. A new instance is created on every get call.
singleton singleton<T>(param: IContainerBindParam<T>): void Registers a singleton binding. The same instance is returned on every get call.
get get<T>(param: IContainerGetParam<T>): T Resolves and returns the registered instance for the given key.

IContainerBindParam<T>

{
  key: string;         // Unique identifier string for this binding
  Provider: new (...args: any[]) => T;  // The class to instantiate
}

IContainerGetParam<T>

{
  key: string | Constructor<T>;  // The identifier used during bind/singleton
}

InversifyContainer

The built-in InversifyJS-backed implementation of the container. Pass it to Container as the provider.

import { Container, InversifyContainer } from '@hramini/ioc';

const container = new Container(new InversifyContainer());

Note: All InversifyContainer instances share a single underlying Inversify container (process-wide singleton). Bindings registered through one instance are accessible from any other.


Ioc

A static utility class that provides TypeScript decorators for dependency injection.

Method Decorator Type Description
Ioc.injectable() Class decorator Marks a class as injectable so InversifyJS can instantiate it. Required on every class registered with bind/singleton.
Ioc.inject(identifier) Parameter decorator Constructor parameter injection. Use on a constructor parameter to inject a dependency by its key.
Ioc.lazyInject(identifier) Property decorator Property injection. Use on a class property to lazily resolve the dependency when first accessed.

identifier

The identifier passed to Ioc.inject and Ioc.lazyInject can be:

  • A string — must match the key used in bind/singleton
  • A symbol
  • A class constructor

Usage Patterns

Transient Bindings

A new instance is created every time get is called.

import 'reflect-metadata';
import { Container, Ioc, InversifyContainer } from '@hramini/ioc';

interface ILogger {
  log(message: string): void;
}

@Ioc.injectable()
class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log({ message });
  }
}

const container = new Container(new InversifyContainer());

container.bind<ILogger>({ key: 'ILogger', Provider: ConsoleLogger });

const logger1 = container.get<ILogger>({ key: 'ILogger' });
const logger2 = container.get<ILogger>({ key: 'ILogger' });

// logger1 and logger2 are different instances

Singleton Bindings

The same instance is returned on every get call.

import 'reflect-metadata';
import { Container, Ioc, InversifyContainer } from '@hramini/ioc';

interface IConfig {
  getApiUrl(): string;
}

@Ioc.injectable()
class AppConfig implements IConfig {
  private apiUrl = 'https://api.example.com';

  getApiUrl(): string {
    return this.apiUrl;
  }
}

const container = new Container(new InversifyContainer());

container.singleton<IConfig>({ key: 'IConfig', Provider: AppConfig });

const config1 = container.get<IConfig>({ key: 'IConfig' });
const config2 = container.get<IConfig>({ key: 'IConfig' });

// config1 === config2 (same instance)

Constructor Injection

Use @Ioc.inject on constructor parameters to inject dependencies when the class is resolved via the container.

import 'reflect-metadata';
import { Container, Ioc, InversifyContainer } from '@hramini/ioc';

interface IRepository {
  findAll(): string[];
}

interface IService {
  getItems(): string[];
}

@Ioc.injectable()
class UserRepository implements IRepository {
  findAll(): string[] {
    return ['Alice', 'Bob'];
  }
}

@Ioc.injectable()
class UserService implements IService {
  constructor(
    @Ioc.inject('IRepository') private readonly repository: IRepository
  ) {}

  getItems(): string[] {
    return this.repository.findAll();
  }
}

const container = new Container(new InversifyContainer());

container.singleton<IRepository>({ key: 'IRepository', Provider: UserRepository });
container.singleton<IService>({ key: 'IService', Provider: UserService });

const service = container.get<IService>({ key: 'IService' });

console.log({ items: service.getItems() }); // { items: ['Alice', 'Bob'] }

Property Injection (Lazy)

Use @Ioc.lazyInject to inject a dependency as a class property. The dependency is resolved lazily on first access — the class does not need to be resolved through the container itself.

import 'reflect-metadata';
import { Container, Ioc, InversifyContainer } from '@hramini/ioc';

interface IMessageService {
  getMessage(): string;
}

@Ioc.injectable()
class WelcomeService implements IMessageService {
  getMessage(): string {
    return 'Welcome!';
  }
}

class AppController {
  @Ioc.lazyInject('IMessageService')
  private messageService: IMessageService;

  run(): void {
    console.log({ message: this.messageService.getMessage() });
  }
}

const container = new Container(new InversifyContainer());

container.singleton<IMessageService>({ key: 'IMessageService', Provider: WelcomeService });

const controller = new AppController();

controller.run(); // { message: 'Welcome!' }

Note: @Ioc.lazyInject is especially useful for classes that are instantiated outside of the container (e.g. via new), since they still get their dependencies resolved from the shared container.


Full Example

import 'reflect-metadata';
import { Container, Ioc, InversifyContainer } from '@hramini/ioc';

interface IStorage {
  save(value: string): void;
  load(): string;
}

interface IProcessor {
  process(input: string): string;
}

@Ioc.injectable()
class MemoryStorage implements IStorage {
  private data = '';

  save(value: string): void {
    this.data = value;
  }

  load(): string {
    return this.data;
  }
}

@Ioc.injectable()
class UpperCaseProcessor implements IProcessor {
  process(input: string): string {
    return input.toUpperCase();
  }
}

@Ioc.injectable()
class App {
  constructor(
    @Ioc.inject('IStorage') private readonly storage: IStorage,
    @Ioc.inject('IProcessor') private readonly processor: IProcessor
  ) {}

  run(input: string): void {
    const processed = this.processor.process(input);
    this.storage.save(processed);
    console.log({ result: this.storage.load() });
  }
}

const container = new Container(new InversifyContainer());

container.singleton<IStorage>({ key: 'IStorage', Provider: MemoryStorage });
container.singleton<IProcessor>({ key: 'IProcessor', Provider: UpperCaseProcessor });
container.singleton<App>({ key: 'App', Provider: App });

const app = container.get<App>({ key: 'App' });

app.run('hello world'); // { result: 'HELLO WORLD' }

TypeScript Configuration

Add the following to your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "emitDecoratorMetadata": true
  }
}

strictPropertyInitialization: false is recommended when using @Ioc.lazyInject on class properties, since TypeScript cannot verify they are initialized in the constructor.

And import reflect-metadata once at the very top of your application entry file:

import 'reflect-metadata';

License

MIT © Hamidreza Amini

About

This repository contains the full structure of inversion of control

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages