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 { bindTo, Container, inject, register, 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.
*/
@register(bindTo('ILogger'))
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));
// Container reads @inject metadata and resolves 'ILogger' for the logger parameter
const app = container.resolve(App);
expect(app.getLoggerName()).toBe('Logger');
});
});
import { bindTo, Container, inject, register, 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.
*/
@register(bindTo('ILogger'))
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));
// Container reads @inject metadata and resolves 'ILogger' for the logger parameter
const app = container.resolve(App);
expect(app.getLoggerName()).toBe('Logger');
});
});
args(index) and argsFn — Positional Arg Injection
args(index) and argsFn(fn) are thin shortcuts for an InjectFn. Every InjectFn receives (scope, options) as parameters, where options.args holds the runtime args array passed via ProviderOptions:
class Service {
constructor(
// equivalent to args(0)
@inject((scope, { args = [] }) => args[0]) value: string,
// equivalent to argsFn((a, b) => (a as number) + (b as number))
@inject((scope, { args = [] }) => (args[0] as number) + (args[1] as number)) sum: number,
) {}
}
Use them with @inject when you want a constructor parameter to receive a positional argument from the resolution context rather than a registered dependency.
Constructor parameters that should pick up positional args from ProviderOptions must be annotated with @inject(args(index)). Parameters without @inject resolve to undefined. InjectionToken values passed in args are resolved automatically before they reach the constructor.
import 'reflect-metadata';
import {
args,
appendArgs,
appendArgsFn,
argsFn,
Container,
inject,
register,
Registration as R,
SingleToken,
} from 'ts-ioc-container';
describe('inject helpers', () => {
function createContainer() {
return new Container();
}
describe('args(index)', () => {
it('resolves InjectionToken args before reaching @inject(args(...))', () => {
const ValueToken = new SingleToken<string>('value');
class Service {
constructor(@inject(args(0)) public value: string) {}
}
const ServiceToken = new SingleToken<Service>('Service');
const container = createContainer()
.addRegistration(R.fromValue('injected').bindTo(ValueToken))
.addRegistration(R.fromClass(Service).bindTo(ServiceToken));
const instance = ServiceToken.args(ValueToken).resolve(container);
expect(instance.value).toBe('injected');
});
it('returns undefined for out-of-bounds index', () => {
@register(appendArgs('only'))
class Service {
constructor(@inject(args(5)) public value: unknown) {}
}
const container = createContainer().addRegistration(R.fromClass(Service));
expect(container.resolve<Service>('Service').value).toBeUndefined();
});
});
describe('argsFn', () => {
it('receives an empty array when no args are provided', () => {
class Service {
constructor(@inject(argsFn((...a) => a.length)) public count: number) {}
}
const container = createContainer().addRegistration(R.fromClass(Service));
expect(container.resolve<Service>('Service').count).toBe(0);
});
it('can transform args into a complex object', () => {
@register(appendArgsFn(() => ['x', 'y']))
class Service {
constructor(@inject(argsFn((a, b) => ({ first: a, second: b }))) public data: unknown) {}
}
const container = createContainer().addRegistration(R.fromClass(Service));
expect(container.resolve<Service>('Service').data).toEqual({ first: 'x', second: 'y' });
});
});
});
import 'reflect-metadata';
import {
args,
appendArgs,
appendArgsFn,
argsFn,
Container,
inject,
register,
Registration as R,
SingleToken,
} from 'ts-ioc-container';
describe('inject helpers', () => {
function createContainer() {
return new Container();
}
describe('args(index)', () => {
it('resolves InjectionToken args before reaching @inject(args(...))', () => {
const ValueToken = new SingleToken<string>('value');
class Service {
constructor(@inject(args(0)) public value: string) {}
}
const ServiceToken = new SingleToken<Service>('Service');
const container = createContainer()
.addRegistration(R.fromValue('injected').bindTo(ValueToken))
.addRegistration(R.fromClass(Service).bindTo(ServiceToken));
const instance = ServiceToken.args(ValueToken).resolve(container);
expect(instance.value).toBe('injected');
});
it('returns undefined for out-of-bounds index', () => {
@register(appendArgs('only'))
class Service {
constructor(@inject(args(5)) public value: unknown) {}
}
const container = createContainer().addRegistration(R.fromClass(Service));
expect(container.resolve<Service>('Service').value).toBeUndefined();
});
});
describe('argsFn', () => {
it('receives an empty array when no args are provided', () => {
class Service {
constructor(@inject(argsFn((...a) => a.length)) public count: number) {}
}
const container = createContainer().addRegistration(R.fromClass(Service));
expect(container.resolve<Service>('Service').count).toBe(0);
});
it('can transform args into a complex object', () => {
@register(appendArgsFn(() => ['x', 'y']))
class Service {
constructor(@inject(argsFn((a, b) => ({ first: a, second: b }))) public data: unknown) {}
}
const container = createContainer().addRegistration(R.fromClass(Service));
expect(container.resolve<Service>('Service').data).toEqual({ first: 'x', second: 'y' });
});
});
});
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 { bindTo, Container, type IContainer, register, Registration as R, SimpleInjector } from 'ts-ioc-container';
@register(bindTo('HandlerCreateUser'))
class CreateUserHandler {
handle(username: string): string {
return `User ${username} created`;
}
}
describe('SimpleInjector', function () {
it('should inject container to allow dynamic resolution', function () {
@register(bindTo('Dispatcher'))
class CommandDispatcher {
constructor(private container: IContainer) {}
dispatch(type: string, payload: string): string {
const handler = this.container.resolve<CreateUserHandler>(`Handler${type}`);
return handler.handle(payload);
}
}
const container = new Container({ injector: new SimpleInjector() })
.addRegistration(R.fromClass(CommandDispatcher))
.addRegistration(R.fromClass(CreateUserHandler));
const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
expect(dispatcher.dispatch('CreateUser', 'alice')).toBe('User alice created');
});
it('should pass additional arguments alongside the container', function () {
class WidgetFactory {
constructor(
private container: IContainer,
private theme: string,
) {}
createWidget(name: string): string {
return `Widget ${name} with ${this.theme} theme (container: ${!!this.container})`;
}
}
const container = new Container({ injector: new SimpleInjector() }).addRegistration(R.fromClass(WidgetFactory));
const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (container: true)');
});
});
import { bindTo, Container, type IContainer, register, Registration as R, SimpleInjector } from 'ts-ioc-container';
@register(bindTo('HandlerCreateUser'))
class CreateUserHandler {
handle(username: string): string {
return `User ${username} created`;
}
}
describe('SimpleInjector', function () {
it('should inject container to allow dynamic resolution', function () {
@register(bindTo('Dispatcher'))
class CommandDispatcher {
constructor(private container: IContainer) {}
dispatch(type: string, payload: string): string {
const handler = this.container.resolve<CreateUserHandler>(`Handler${type}`);
return handler.handle(payload);
}
}
const container = new Container({ injector: new SimpleInjector() })
.addRegistration(R.fromClass(CommandDispatcher))
.addRegistration(R.fromClass(CreateUserHandler));
const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
expect(dispatcher.dispatch('CreateUser', 'alice')).toBe('User alice created');
});
it('should pass additional arguments alongside the container', function () {
class WidgetFactory {
constructor(
private container: IContainer,
private theme: string,
) {}
createWidget(name: string): string {
return `Widget ${name} with ${this.theme} theme (container: ${!!this.container})`;
}
}
const container = new Container({ injector: new SimpleInjector() }).addRegistration(R.fromClass(WidgetFactory));
const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (container: 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.
ProxyInjector resolves dependencies from property names. Renaming, minification, or unclear alias naming can change resolution behavior.
Reserved keywords and conventions
args— accessingdeps.argsreturns the rawargs[]array passed at resolve time, allowing runtime arguments to be forwarded directly to the constructoraliasconvention — any property whose name contains"alias"(case-insensitive, e.g.loggersAlias) is resolved viaresolveByAliasinstead ofresolve, returning an array of all matching registrations
import { bindTo, Container, ProxyInjector, register, Registration as R } from 'ts-ioc-container';
describe('ProxyInjector', function () {
it('should inject dependencies as a props object', function () {
@register(bindTo('logger'))
class Logger {
log(msg: string) {
return `Logged: ${msg}`;
}
}
class UserController {
private logger: Logger;
private prefix: string;
constructor({ logger, prefix }: { logger: Logger; prefix: string }) {
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))
.addRegistration(R.fromValue('USER:').bindToKey('prefix'))
.addRegistration(R.fromClass(UserController));
expect(container.resolve<UserController>('UserController').createUser('bob')).toBe('Logged: USER: bob');
});
it('should expose runtime args through the reserved "args" property', function () {
class ReportGenerator {
format: string;
constructor({ args }: { args: string[] }) {
this.format = args[0];
}
generate(): string {
return `Report in ${this.format}`;
}
}
const container = new Container({ injector: new ProxyInjector() }).addRegistration(R.fromClass(ReportGenerator));
const generator = container.resolve<ReportGenerator>('ReportGenerator', { args: ['PDF'] });
expect(generator.generate()).toBe('Report in PDF');
});
});
import { bindTo, Container, ProxyInjector, register, Registration as R } from 'ts-ioc-container';
describe('ProxyInjector', function () {
it('should inject dependencies as a props object', function () {
@register(bindTo('logger'))
class Logger {
log(msg: string) {
return `Logged: ${msg}`;
}
}
class UserController {
private logger: Logger;
private prefix: string;
constructor({ logger, prefix }: { logger: Logger; prefix: string }) {
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))
.addRegistration(R.fromValue('USER:').bindToKey('prefix'))
.addRegistration(R.fromClass(UserController));
expect(container.resolve<UserController>('UserController').createUser('bob')).toBe('Logged: USER: bob');
});
it('should expose runtime args through the reserved "args" property', function () {
class ReportGenerator {
format: string;
constructor({ args }: { args: string[] }) {
this.format = args[0];
}
generate(): string {
return `Report in ${this.format}`;
}
}
const container = new Container({ injector: new ProxyInjector() }).addRegistration(R.fromClass(ReportGenerator));
const generator = container.resolve<ReportGenerator>('ReportGenerator', { args: ['PDF'] });
expect(generator.generate()).toBe('Report in PDF');
});
});
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