Injector
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 '../../lib';
class Logger {
name = 'Logger';
}
class App {
constructor(@inject('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().addRegistration(R.fromClass(Logger).bindToKey('ILogger'));
const app = container.resolve(App);
expect(app.getLoggerName()).toBe('Logger');
});
});
import { Container, inject, Registration as R } from '../../lib';
class Logger {
name = 'Logger';
}
class App {
constructor(@inject('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().addRegistration(R.fromClass(Logger).bindToKey('ILogger'));
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 '../../lib';
describe('SimpleInjector', function () {
it('should pass container as first parameter', function () {
class App {
constructor(public container: IContainer) {}
}
const container = new Container({ injector: new SimpleInjector() }).addRegistration(
R.fromClass(App).bindToKey('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({ injector: new SimpleInjector() }).addRegistration(
R.fromClass(App).bindToKey('App'),
);
const app = container.resolve<App>('App', { args: ['Hello world'] });
expect(app.greeting).toBe('Hello world');
});
});
import { Container, type IContainer, Registration as R, SimpleInjector } from '../../lib';
describe('SimpleInjector', function () {
it('should pass container as first parameter', function () {
class App {
constructor(public container: IContainer) {}
}
const container = new Container({ injector: new SimpleInjector() }).addRegistration(
R.fromClass(App).bindToKey('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({ injector: new SimpleInjector() }).addRegistration(
R.fromClass(App).bindToKey('App'),
);
const app = container.resolve<App>('App', { args: ['Hello world'] });
expect(app.greeting).toBe('Hello world');
});
});
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 { args, Container, ProxyInjector, Registration as R } from '../../lib';
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({ injector: new ProxyInjector() }).addRegistration(
R.fromClass(Logger).bindToKey('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({ injector: new ProxyInjector() })
.addRegistration(R.fromClass(App).bindToKey('App').pipe(args({ greetingTemplate })))
.addRegistration(R.fromClass(Logger).bindToKey('logger'));
const app = container.resolve<App>('App', { args: [{ name: `world` }] });
expect(app.greeting).toBe('Hello world');
});
it('should resolve array dependencies when property name contains "array"', function () {
class Logger {}
class Service {}
class App {
loggers: Logger[];
service: Service;
constructor({ loggersArray, service }: { loggersArray: Logger[]; service: Service }) {
this.loggers = loggersArray;
this.service = service;
}
}
// Mock container's resolveByAlias to return an array with a Logger instance
const mockLogger = new Logger();
const mockContainer = new Container({ injector: new ProxyInjector() });
mockContainer.resolveByAlias = jest.fn().mockImplementation((key) => {
// Always return the mock array for simplicity
return [mockLogger];
});
mockContainer.addRegistration(R.fromClass(Service).bindToKey('service'));
const app = mockContainer.resolve(App);
expect(app.loggers).toBeInstanceOf(Array);
expect(app.loggers.length).toBe(1);
expect(app.loggers[0]).toBe(mockLogger);
expect(app.service).toBeInstanceOf(Service);
// Verify that resolveByAlias was called with the correct key
expect(mockContainer.resolveByAlias).toHaveBeenCalledWith('loggersArray');
});
});
import { args, Container, ProxyInjector, Registration as R } from '../../lib';
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({ injector: new ProxyInjector() }).addRegistration(
R.fromClass(Logger).bindToKey('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({ injector: new ProxyInjector() })
.addRegistration(R.fromClass(App).bindToKey('App').pipe(args({ greetingTemplate })))
.addRegistration(R.fromClass(Logger).bindToKey('logger'));
const app = container.resolve<App>('App', { args: [{ name: `world` }] });
expect(app.greeting).toBe('Hello world');
});
it('should resolve array dependencies when property name contains "array"', function () {
class Logger {}
class Service {}
class App {
loggers: Logger[];
service: Service;
constructor({ loggersArray, service }: { loggersArray: Logger[]; service: Service }) {
this.loggers = loggersArray;
this.service = service;
}
}
// Mock container's resolveByAlias to return an array with a Logger instance
const mockLogger = new Logger();
const mockContainer = new Container({ injector: new ProxyInjector() });
mockContainer.resolveByAlias = jest.fn().mockImplementation((key) => {
// Always return the mock array for simplicity
return [mockLogger];
});
mockContainer.addRegistration(R.fromClass(Service).bindToKey('service'));
const app = mockContainer.resolve(App);
expect(app.loggers).toBeInstanceOf(Array);
expect(app.loggers.length).toBe(1);
expect(app.loggers[0]).toBe(mockLogger);
expect(app.service).toBeInstanceOf(Service);
// Verify that resolveByAlias was called with the correct key
expect(mockContainer.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 } from '../../lib';
class CustomInjector extends Injector {
createInstance<T>(container: IContainer, target: constructor<T>, { args = [] }: ProviderOptions = {}): T {
return new App(args[0] as string) as T;
}
}
class App {
constructor(public version: string) {}
}
describe('Custom Injector', function () {
it('should use custom injector for dependency injection', function () {
const container = new Container({ injector: new CustomInjector() });
const app = container.resolve<App>(App, { args: ['1.0.0'] });
expect(app.version).toBe('1.0.0');
});
});
import { type constructor, Container, type IContainer, Injector, ProviderOptions } from '../../lib';
class CustomInjector extends Injector {
createInstance<T>(container: IContainer, target: constructor<T>, { args = [] }: ProviderOptions = {}): T {
return new App(args[0] as string) as T;
}
}
class App {
constructor(public version: string) {}
}
describe('Custom Injector', function () {
it('should use custom injector for dependency injection', function () {
const container = new Container({ injector: new CustomInjector() });
const app = container.resolve<App>(App, { args: ['1.0.0'] });
expect(app.version).toBe('1.0.0');
});
});
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