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.
- Installation
- Requirements
- Quick Start
- API Reference
- Usage Patterns
- Full Example
- TypeScript Configuration
- License
npm install @hramini/ioc- TypeScript with
experimentalDecorators: trueintsconfig.json reflect-metadatamust be imported once at the entry point of your application, before any other import- Node.js or a browser environment with decorator support
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!' }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. |
{
key: string; // Unique identifier string for this binding
Provider: new (...args: any[]) => T; // The class to instantiate
}{
key: string | Constructor<T>; // The identifier used during bind/singleton
}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
InversifyContainerinstances share a single underlying Inversify container (process-wide singleton). Bindings registered through one instance are accessible from any other.
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. |
The identifier passed to Ioc.inject and Ioc.lazyInject can be:
- A string — must match the
keyused inbind/singleton - A symbol
- A class constructor
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 instancesThe 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)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'] }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.lazyInjectis especially useful for classes that are instantiated outside of the container (e.g. vianew), since they still get their dependencies resolved from the shared container.
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' }Add the following to your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"emitDecoratorMetadata": true
}
}
strictPropertyInitialization: falseis recommended when using@Ioc.lazyInjecton 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';