Injector
Goal: Inject dependencies into constructor.
The injector determines how dependencies are passed to constructors. The container supports three injection strategies, each suited for different use cases and coding styles.
Injection Strategies
The container supports three injection strategies, each implementing the IInjector interface:
Uses @inject decorators
+ reflect-metadata"] Simple["SimpleInjector
Passes container
as first arg"] Proxy["ProxyInjector
Matches param names
to dependency keys"] IInjector -->|implements| Metadata IInjector -->|implements| Simple IInjector -->|implements| Proxy Metadata -->|injects| Instance1["Instance with
decorated params"] Simple -->|injects| Instance2["Instance with
container param"] Proxy -->|injects| Instance3["Instance with
destructured params"] style IInjector fill:#0366d6,color:#fff style Metadata fill:#28a745,color:#fff style Simple fill:#ffc107,color:#000 style Proxy fill:#17a2b8,color:#fff
Available Injectors
- MetadataInjector (default): Uses
@injectdecorators with reflection metadata - ProxyInjector: Injects dependencies as a dictionary object, perfect for destructuring
- SimpleInjector: Passes the container as the first constructor argument for manual resolution
Metadata Injector
The default injector uses TypeScript decorators and reflection metadata to inject dependencies. This provides a clean, declarative syntax that’s easy to read and maintain.
Basic Usage
import { Container, inject, Registration as R } from 'ts-ioc-container';
/**
* User Management Domain - Metadata Injection
*
* The MetadataInjector (default) uses TypeScript decorators and reflect-metadata
* to automatically inject dependencies into constructor parameters.
*
* How it works:
* 1. @inject('key') decorator marks a parameter for injection
* 2. Container reads metadata at resolution time
* 3. Dependencies are resolved and passed to constructor
*
* This is the most common pattern in Angular, NestJS, and similar frameworks.
* Requires: "experimentalDecorators" and "emitDecoratorMetadata" in tsconfig.
*/
class Logger {
name = 'Logger';
}
class App {
// @inject tells the container which dependency to resolve for this parameter
constructor(@inject('ILogger') private logger: Logger) {}
// Alternative: inject via function for dynamic resolution
// constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {}
getLoggerName(): string {
return this.logger.name;
}
}
describe('Metadata Injector', function () {
it('should inject dependencies using @inject decorator', function () {
const container = new Container({ tags: ['application'] }).addRegistration(
R.fromClass(Logger).bindToKey('ILogger'),
);
// Container reads @inject metadata and resolves 'ILogger' for the logger parameter
const app = container.resolve(App);
expect(app.getLoggerName()).toBe('Logger');
});
});
import { Container, inject, Registration as R } from 'ts-ioc-container';
/**
* User Management Domain - Metadata Injection
*
* The MetadataInjector (default) uses TypeScript decorators and reflect-metadata
* to automatically inject dependencies into constructor parameters.
*
* How it works:
* 1. @inject('key') decorator marks a parameter for injection
* 2. Container reads metadata at resolution time
* 3. Dependencies are resolved and passed to constructor
*
* This is the most common pattern in Angular, NestJS, and similar frameworks.
* Requires: "experimentalDecorators" and "emitDecoratorMetadata" in tsconfig.
*/
class Logger {
name = 'Logger';
}
class App {
// @inject tells the container which dependency to resolve for this parameter
constructor(@inject('ILogger') private logger: Logger) {}
// Alternative: inject via function for dynamic resolution
// constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {}
getLoggerName(): string {
return this.logger.name;
}
}
describe('Metadata Injector', function () {
it('should inject dependencies using @inject decorator', function () {
const container = new Container({ tags: ['application'] }).addRegistration(
R.fromClass(Logger).bindToKey('ILogger'),
);
// Container reads @inject metadata and resolves 'ILogger' for the logger parameter
const app = container.resolve(App);
expect(app.getLoggerName()).toBe('Logger');
});
});
Property Injection
The Metadata Injector also supports property injection using hooks. This is useful when constructor injection isn’t possible or when you need to inject into base class properties. See the Hooks chapter for detailed information about property injection.
Simple Injector
The Simple Injector passes the container instance as the first constructor parameter, giving you direct access to resolve dependencies manually. This approach is useful when you need more control over dependency resolution.
import { Container, type IContainer, Registration as R, SimpleInjector } from 'ts-ioc-container';
/**
* Command Pattern - Simple Injector
*
* The SimpleInjector passes the container itself as the first argument to the constructor.
* This is useful for:
* - Service Locators (like Command Dispatchers or Routers)
* - Factory classes that need to resolve dependencies dynamically
* - Legacy code migration where passing the container is common
*
* In this example, a CommandDispatcher uses the container to dynamically
* resolve the correct handler for each command type.
*/
interface ICommand {
type: string;
}
interface ICommandHandler {
handle(command: ICommand): string;
}
class CreateUserCommand implements ICommand {
readonly type = 'CreateUser';
constructor(readonly username: string) {}
}
class CreateUserHandler implements ICommandHandler {
handle(command: CreateUserCommand): string {
return `User ${command.username} created`;
}
}
describe('SimpleInjector', function () {
it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
// Dispatcher needs the container to find handlers dynamically based on command type
class CommandDispatcher {
constructor(private container: IContainer) {}
dispatch(command: ICommand): string {
// Dynamically resolve handler: "Handler" + "CreateUser"
const handlerKey = `Handler${command.type}`;
const handler = this.container.resolve<ICommandHandler>(handlerKey);
return handler.handle(command);
}
}
const container = new Container({ injector: new SimpleInjector() })
.addRegistration(R.fromClass(CommandDispatcher).bindToKey('Dispatcher'))
.addRegistration(R.fromClass(CreateUserHandler).bindToKey('HandlerCreateUser'));
const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
const result = dispatcher.dispatch(new CreateUserCommand('alice'));
expect(result).toBe('User alice created');
});
it('should pass additional arguments alongside the container', function () {
// Factory that creates widgets with a specific theme
class WidgetFactory {
constructor(
private container: IContainer,
private theme: string, // Passed as argument during resolve
) {}
createWidget(name: string): string {
return `Widget ${name} with ${this.theme} theme (Container available: ${!!this.container})`;
}
}
const container = new Container({ injector: new SimpleInjector() }).addRegistration(
R.fromClass(WidgetFactory).bindToKey('WidgetFactory'),
);
// Pass "dark" as the theme argument
const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (Container available: true)');
});
});
import { Container, type IContainer, Registration as R, SimpleInjector } from 'ts-ioc-container';
/**
* Command Pattern - Simple Injector
*
* The SimpleInjector passes the container itself as the first argument to the constructor.
* This is useful for:
* - Service Locators (like Command Dispatchers or Routers)
* - Factory classes that need to resolve dependencies dynamically
* - Legacy code migration where passing the container is common
*
* In this example, a CommandDispatcher uses the container to dynamically
* resolve the correct handler for each command type.
*/
interface ICommand {
type: string;
}
interface ICommandHandler {
handle(command: ICommand): string;
}
class CreateUserCommand implements ICommand {
readonly type = 'CreateUser';
constructor(readonly username: string) {}
}
class CreateUserHandler implements ICommandHandler {
handle(command: CreateUserCommand): string {
return `User ${command.username} created`;
}
}
describe('SimpleInjector', function () {
it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
// Dispatcher needs the container to find handlers dynamically based on command type
class CommandDispatcher {
constructor(private container: IContainer) {}
dispatch(command: ICommand): string {
// Dynamically resolve handler: "Handler" + "CreateUser"
const handlerKey = `Handler${command.type}`;
const handler = this.container.resolve<ICommandHandler>(handlerKey);
return handler.handle(command);
}
}
const container = new Container({ injector: new SimpleInjector() })
.addRegistration(R.fromClass(CommandDispatcher).bindToKey('Dispatcher'))
.addRegistration(R.fromClass(CreateUserHandler).bindToKey('HandlerCreateUser'));
const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
const result = dispatcher.dispatch(new CreateUserCommand('alice'));
expect(result).toBe('User alice created');
});
it('should pass additional arguments alongside the container', function () {
// Factory that creates widgets with a specific theme
class WidgetFactory {
constructor(
private container: IContainer,
private theme: string, // Passed as argument during resolve
) {}
createWidget(name: string): string {
return `Widget ${name} with ${this.theme} theme (Container available: ${!!this.container})`;
}
}
const container = new Container({ injector: new SimpleInjector() }).addRegistration(
R.fromClass(WidgetFactory).bindToKey('WidgetFactory'),
);
// Pass "dark" as the theme argument
const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (Container available: true)');
});
});
Use Cases
- Manual dependency resolution
- Dynamic dependency selection
- Legacy code integration
- Framework integration where container access is needed
Proxy Injector
The Proxy Injector matches constructor parameter names to dependency keys and injects them as a dictionary object. This is useful for object destructuring patterns and functional programming styles.
import { Container, ProxyInjector, Registration as R } from 'ts-ioc-container';
/**
* Clean Architecture - Proxy Injector
*
* The ProxyInjector injects dependencies as a single object (props/options pattern).
* This is popular in modern JavaScript/TypeScript (like React props or destructuring).
*
* Advantages:
* - Named parameters are more readable than positional arguments
* - Order of arguments doesn't matter
* - Easy to add/remove dependencies without breaking inheritance chains
* - Works well with "Clean Architecture" adapters
*/
describe('ProxyInjector', function () {
it('should inject dependencies as a props object', function () {
class Logger {
log(msg: string) {
return `Logged: ${msg}`;
}
}
// Dependencies defined as an interface
interface UserControllerDeps {
logger: Logger;
prefix: string;
}
// Controller receives all dependencies in a single object
class UserController {
private logger: Logger;
private prefix: string;
constructor({ logger, prefix }: UserControllerDeps) {
this.logger = logger;
this.prefix = prefix;
}
createUser(name: string): string {
return this.logger.log(`${this.prefix} ${name}`);
}
}
const container = new Container({ injector: new ProxyInjector() })
.addRegistration(R.fromClass(Logger).bindToKey('logger'))
.addRegistration(R.fromValue('USER:').bindToKey('prefix'))
.addRegistration(R.fromClass(UserController).bindToKey('UserController'));
const controller = container.resolve<UserController>('UserController');
expect(controller.createUser('bob')).toBe('Logged: USER: bob');
});
it('should support mixing injected dependencies with runtime arguments', function () {
class Database {}
interface ReportGeneratorDeps {
database: Database;
format: string; // Runtime argument
}
class ReportGenerator {
constructor(public deps: ReportGeneratorDeps) {}
generate(): string {
return `Report in ${this.deps.format}`;
}
}
const container = new Container({ injector: new ProxyInjector() })
.addRegistration(R.fromClass(Database).bindToKey('database'))
.addRegistration(R.fromClass(ReportGenerator).bindToKey('ReportGenerator'));
// "format" is passed at resolution time
const generator = container.resolve<ReportGenerator>('ReportGenerator', {
args: [{ format: 'PDF' }],
});
expect(generator.deps.database).toBeInstanceOf(Database);
expect(generator.generate()).toBe('Report in PDF');
});
it('should resolve array dependencies by alias (convention over configuration)', function () {
// If a property is named "loggersArray", it looks for alias "loggersArray"
// and resolves it as an array of all matches.
class FileLogger {}
class ConsoleLogger {}
interface AppDeps {
loggersArray: any[]; // Injected as array of all loggers
}
class App {
constructor(public deps: AppDeps) {}
}
const container = new Container({ injector: new ProxyInjector() });
// Mocking the behavior for this specific test as ProxyInjector uses resolveByAlias
// which delegates to the container.
// In a real scenario, you'd register multiple loggers with the same alias.
const mockLoggers = [new FileLogger(), new ConsoleLogger()];
container.resolveByAlias = jest.fn().mockReturnValue(mockLoggers);
const app = container.resolve(App);
expect(app.deps.loggersArray).toBe(mockLoggers);
expect(container.resolveByAlias).toHaveBeenCalledWith('loggersArray');
});
});
import { Container, ProxyInjector, Registration as R } from 'ts-ioc-container';
/**
* Clean Architecture - Proxy Injector
*
* The ProxyInjector injects dependencies as a single object (props/options pattern).
* This is popular in modern JavaScript/TypeScript (like React props or destructuring).
*
* Advantages:
* - Named parameters are more readable than positional arguments
* - Order of arguments doesn't matter
* - Easy to add/remove dependencies without breaking inheritance chains
* - Works well with "Clean Architecture" adapters
*/
describe('ProxyInjector', function () {
it('should inject dependencies as a props object', function () {
class Logger {
log(msg: string) {
return `Logged: ${msg}`;
}
}
// Dependencies defined as an interface
interface UserControllerDeps {
logger: Logger;
prefix: string;
}
// Controller receives all dependencies in a single object
class UserController {
private logger: Logger;
private prefix: string;
constructor({ logger, prefix }: UserControllerDeps) {
this.logger = logger;
this.prefix = prefix;
}
createUser(name: string): string {
return this.logger.log(`${this.prefix} ${name}`);
}
}
const container = new Container({ injector: new ProxyInjector() })
.addRegistration(R.fromClass(Logger).bindToKey('logger'))
.addRegistration(R.fromValue('USER:').bindToKey('prefix'))
.addRegistration(R.fromClass(UserController).bindToKey('UserController'));
const controller = container.resolve<UserController>('UserController');
expect(controller.createUser('bob')).toBe('Logged: USER: bob');
});
it('should support mixing injected dependencies with runtime arguments', function () {
class Database {}
interface ReportGeneratorDeps {
database: Database;
format: string; // Runtime argument
}
class ReportGenerator {
constructor(public deps: ReportGeneratorDeps) {}
generate(): string {
return `Report in ${this.deps.format}`;
}
}
const container = new Container({ injector: new ProxyInjector() })
.addRegistration(R.fromClass(Database).bindToKey('database'))
.addRegistration(R.fromClass(ReportGenerator).bindToKey('ReportGenerator'));
// "format" is passed at resolution time
const generator = container.resolve<ReportGenerator>('ReportGenerator', {
args: [{ format: 'PDF' }],
});
expect(generator.deps.database).toBeInstanceOf(Database);
expect(generator.generate()).toBe('Report in PDF');
});
it('should resolve array dependencies by alias (convention over configuration)', function () {
// If a property is named "loggersArray", it looks for alias "loggersArray"
// and resolves it as an array of all matches.
class FileLogger {}
class ConsoleLogger {}
interface AppDeps {
loggersArray: any[]; // Injected as array of all loggers
}
class App {
constructor(public deps: AppDeps) {}
}
const container = new Container({ injector: new ProxyInjector() });
// Mocking the behavior for this specific test as ProxyInjector uses resolveByAlias
// which delegates to the container.
// In a real scenario, you'd register multiple loggers with the same alias.
const mockLoggers = [new FileLogger(), new ConsoleLogger()];
container.resolveByAlias = jest.fn().mockReturnValue(mockLoggers);
const app = container.resolve(App);
expect(app.deps.loggersArray).toBe(mockLoggers);
expect(container.resolveByAlias).toHaveBeenCalledWith('loggersArray');
});
});
Use Cases
- Object destructuring in constructors
- Named parameter patterns
- Functional programming styles
- When you prefer explicit parameter names over decorators
Choosing the Right Injector
| Injector | Best For | Pros | Cons |
|---|---|---|---|
| MetadataInjector | Most applications, TypeScript projects | Clean syntax, type-safe, IntelliSense support | Requires reflect-metadata, decorator support |
| SimpleInjector | Manual control, legacy code, frameworks | Full control, no decorators needed | More verbose, manual resolution |
| ProxyInjector | Destructuring patterns, functional style | Explicit parameter names, no decorators | Less type inference, naming conventions |
Strategy Pattern
The IInjector interface uses the Strategy pattern, allowing different injection strategies to be used interchangeably. This makes the system flexible and extensible.
Custom Injectors
You can create custom injectors by implementing the Injector interface. This allows you to implement injection strategies tailored to your specific needs.
import { type constructor, Container, type IContainer, Injector, ProviderOptions, Registration } from 'ts-ioc-container';
/**
* Advanced - Custom Injector
*
* You can implement your own injection strategy by extending the `Injector` class.
* This is useful for integrating with other frameworks, supporting legacy patterns,
* or implementing custom instantiation logic.
*
* Example: Static Factory Method Pattern
* Some classes prefer to control their own instantiation via a static `create` method
* rather than a public constructor. This custom injector supports that pattern.
*/
interface IFactoryClass<T> {
create(container: IContainer, ...args: any[]): T;
}
class StaticFactoryInjector extends Injector {
createInstance<T>(container: IContainer, target: constructor<T>, { args = [] }: ProviderOptions = {}): T {
// Check if the class has a static 'create' method
const factoryClass = target as unknown as IFactoryClass<T>;
if (typeof factoryClass.create === 'function') {
return factoryClass.create(container, ...args);
}
// Fallback to standard constructor instantiation
return new target(...args);
}
}
describe('Custom Injector', function () {
it('should use static create method for instantiation when available', function () {
class ApiClient {
constructor(
public baseUrl: string,
public timeout: number,
) {}
// Custom factory method
static create(container: IContainer, config: { timeout: number }): ApiClient {
const baseUrl = container.resolve<string>('BaseUrl');
return new ApiClient(baseUrl, config.timeout);
}
}
const container = new Container({ injector: new StaticFactoryInjector() })
.addRegistration(Registration.fromValue('https://api.example.com').bindToKey('BaseUrl'))
.addRegistration(Registration.fromClass(ApiClient));
// Resolve using the custom injector which calls ApiClient.create
const client = container.resolve(ApiClient, { args: [{ timeout: 5000 }] });
expect(client.baseUrl).toBe('https://api.example.com');
expect(client.timeout).toBe(5000);
});
});
import { type constructor, Container, type IContainer, Injector, ProviderOptions, Registration } from 'ts-ioc-container';
/**
* Advanced - Custom Injector
*
* You can implement your own injection strategy by extending the `Injector` class.
* This is useful for integrating with other frameworks, supporting legacy patterns,
* or implementing custom instantiation logic.
*
* Example: Static Factory Method Pattern
* Some classes prefer to control their own instantiation via a static `create` method
* rather than a public constructor. This custom injector supports that pattern.
*/
interface IFactoryClass<T> {
create(container: IContainer, ...args: any[]): T;
}
class StaticFactoryInjector extends Injector {
createInstance<T>(container: IContainer, target: constructor<T>, { args = [] }: ProviderOptions = {}): T {
// Check if the class has a static 'create' method
const factoryClass = target as unknown as IFactoryClass<T>;
if (typeof factoryClass.create === 'function') {
return factoryClass.create(container, ...args);
}
// Fallback to standard constructor instantiation
return new target(...args);
}
}
describe('Custom Injector', function () {
it('should use static create method for instantiation when available', function () {
class ApiClient {
constructor(
public baseUrl: string,
public timeout: number,
) {}
// Custom factory method
static create(container: IContainer, config: { timeout: number }): ApiClient {
const baseUrl = container.resolve<string>('BaseUrl');
return new ApiClient(baseUrl, config.timeout);
}
}
const container = new Container({ injector: new StaticFactoryInjector() })
.addRegistration(Registration.fromValue('https://api.example.com').bindToKey('BaseUrl'))
.addRegistration(Registration.fromClass(ApiClient));
// Resolve using the custom injector which calls ApiClient.create
const client = container.resolve(ApiClient, { args: [{ timeout: 5000 }] });
expect(client.baseUrl).toBe('https://api.example.com');
expect(client.timeout).toBe(5000);
});
});
Extension Points
Custom injectors are one of the key extension points in the architecture. They allow you to:
- Implement framework-specific injection patterns
- Add custom parameter resolution logic
- Integrate with legacy codebases
- Support alternative dependency injection styles
Best Practices
- Use MetadataInjector by default - It provides the best developer experience for most TypeScript projects
- Choose SimpleInjector for manual control - When you need explicit control over dependency resolution
- Use ProxyInjector for destructuring - When you prefer object destructuring patterns
- Be consistent - Stick with one injector strategy throughout your application for maintainability