A powerful and lightweight inversion of control container for JavaScript and Node.js apps powered by TypeScript.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
InversifyJS supports sophisticated conditional binding resolution, allowing you to bind multiple implementations to the same service identifier and resolve them based on context, names, tags, or custom conditions. This enables flexible dependency injection scenarios and context-aware service selection.
The primary interface for configuring conditional resolution rules.
interface BindWhenFluentSyntax<T> {
when(constraint: (request: ResolutionContext) => boolean): BindOnFluentSyntax<T>;
whenTargetNamed(name: string): BindOnFluentSyntax<T>;
whenTargetTagged(tag: string, value: any): BindOnFluentSyntax<T>;
whenInjectedInto(parent: Newable<any> | string): BindOnFluentSyntax<T>;
whenParentNamed(name: string): BindOnFluentSyntax<T>;
whenParentTagged(tag: string, value: any): BindOnFluentSyntax<T>;
whenAnyAncestorIs(ancestor: Newable<any> | string): BindOnFluentSyntax<T>;
whenNoAncestorIs(ancestor: Newable<any> | string): BindOnFluentSyntax<T>;
whenAnyAncestorNamed(name: string): BindOnFluentSyntax<T>;
whenNoAncestorNamed(name: string): BindOnFluentSyntax<T>;
whenAnyAncestorTagged(tag: string, value: any): BindOnFluentSyntax<T>;
whenNoAncestorTagged(tag: string, value: any): BindOnFluentSyntax<T>;
whenAnyAncestorMatches(constraint: (request: ResolutionContext) => boolean): BindOnFluentSyntax<T>;
whenNoAncestorMatches(constraint: (request: ResolutionContext) => boolean): BindOnFluentSyntax<T>;
}Use named bindings to distinguish between multiple implementations of the same interface.
interface ILogger {
log(message: string): void;
}
@injectable()
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(`[CONSOLE] ${message}`);
}
}
@injectable()
class FileLogger implements ILogger {
log(message: string) {
// Write to file
console.log(`[FILE] ${message}`);
}
}
// Bind with names
container.bind<ILogger>("Logger").to(ConsoleLogger).whenTargetNamed("console");
container.bind<ILogger>("Logger").to(FileLogger).whenTargetNamed("file");
// Usage with @named decorator
@injectable()
class UserService {
constructor(
@inject("Logger") @named("console") private consoleLogger: ILogger,
@inject("Logger") @named("file") private fileLogger: ILogger
) {}
processUser(user: User) {
this.consoleLogger.log("Processing user...");
this.fileLogger.log(`User processed: ${user.id}`);
}
}Resolve based on the name of the parent service.
@injectable()
class DatabaseLogger implements ILogger {
log(message: string) {
// Log to database
}
}
@injectable()
class WebLogger implements ILogger {
log(message: string) {
// Log to web service
}
}
// Bind based on parent names
container.bind<ILogger>("Logger").to(DatabaseLogger).whenParentNamed("database");
container.bind<ILogger>("Logger").to(WebLogger).whenParentNamed("web");
// Parent services with names
container.bind<IDatabaseService>("DatabaseService").to(DatabaseService).whenTargetNamed("database");
container.bind<IWebService>("WebService").to(WebService).whenTargetNamed("web");Use tagged bindings for more complex conditional resolution based on key-value metadata.
interface IPaymentProcessor {
process(amount: number): Promise<PaymentResult>;
}
@injectable()
class CreditCardProcessor implements IPaymentProcessor {
async process(amount: number) {
// Credit card processing logic
return { success: true, transactionId: "cc_123" };
}
}
@injectable()
class PayPalProcessor implements IPaymentProcessor {
async process(amount: number) {
// PayPal processing logic
return { success: true, transactionId: "pp_456" };
}
}
@injectable()
class CryptoProcessor implements IPaymentProcessor {
async process(amount: number) {
// Cryptocurrency processing logic
return { success: true, transactionId: "crypto_789" };
}
}
// Bind with tags
container.bind<IPaymentProcessor>("PaymentProcessor")
.to(CreditCardProcessor)
.whenTargetTagged("method", "creditcard");
container.bind<IPaymentProcessor>("PaymentProcessor")
.to(PayPalProcessor)
.whenTargetTagged("method", "paypal");
container.bind<IPaymentProcessor>("PaymentProcessor")
.to(CryptoProcessor)
.whenTargetTagged("method", "crypto");
// Usage with @tagged decorator
@injectable()
class CheckoutService {
constructor(
@inject("PaymentProcessor") @tagged("method", "creditcard")
private creditCardProcessor: IPaymentProcessor,
@inject("PaymentProcessor") @tagged("method", "paypal")
private paypalProcessor: IPaymentProcessor,
@inject("PaymentProcessor") @tagged("method", "crypto")
private cryptoProcessor: IPaymentProcessor
) {}
async processPayment(method: string, amount: number) {
switch (method) {
case "creditcard":
return this.creditCardProcessor.process(amount);
case "paypal":
return this.paypalProcessor.process(amount);
case "crypto":
return this.cryptoProcessor.process(amount);
default:
throw new Error(`Unsupported payment method: ${method}`);
}
}
}// Bind with multiple tag conditions
container.bind<ILogger>("Logger")
.to(DatabaseLogger)
.whenTargetTagged("destination", "database")
.whenTargetTagged("level", "error");
container.bind<ILogger>("Logger")
.to(ConsoleLogger)
.whenTargetTagged("destination", "console")
.whenTargetTagged("level", "debug");
// Usage
@injectable()
class ErrorHandler {
constructor(
@inject("Logger")
@tagged("destination", "database")
@tagged("level", "error")
private errorLogger: ILogger
) {}
}Resolve based on the class receiving the injection.
interface IRepository<T> {
save(entity: T): Promise<void>;
findById(id: string): Promise<T | null>;
}
@injectable()
class UserRepository implements IRepository<User> {
async save(user: User) { /* User-specific logic */ }
async findById(id: string) { /* User-specific logic */ return null; }
}
@injectable()
class ProductRepository implements IRepository<Product> {
async save(product: Product) { /* Product-specific logic */ }
async findById(id: string) { /* Product-specific logic */ return null; }
}
// Bind based on injection target
container.bind<IRepository<any>>("Repository")
.to(UserRepository)
.whenInjectedInto(UserService);
container.bind<IRepository<any>>("Repository")
.to(ProductRepository)
.whenInjectedInto(ProductService);
@injectable()
class UserService {
constructor(
@inject("Repository") private userRepository: IRepository<User>
) {}
}
@injectable()
class ProductService {
constructor(
@inject("Repository") private productRepository: IRepository<Product>
) {}
}interface IConfigProvider {
getConfig(): any;
}
@injectable()
class TestConfigProvider implements IConfigProvider {
getConfig() {
return { env: "test", debug: true };
}
}
@injectable()
class ProdConfigProvider implements IConfigProvider {
getConfig() {
return { env: "production", debug: false };
}
}
// Bind based on ancestor class
container.bind<IConfigProvider>("Config")
.to(TestConfigProvider)
.whenAnyAncestorIs(TestService);
container.bind<IConfigProvider>("Config")
.to(ProdConfigProvider)
.whenNoAncestorIs(TestService);
@injectable()
class TestService {
constructor(@inject("Database") private db: IDatabase) {}
}
@injectable()
class DatabaseService {
constructor(@inject("Config") private config: IConfigProvider) {}
// Will get TestConfigProvider when created by TestService
// Will get ProdConfigProvider otherwise
}// Bind based on ancestor naming
container.bind<ILogger>("Logger")
.to(VerboseLogger)
.whenAnyAncestorNamed("debug");
container.bind<ILogger>("Logger")
.to(QuietLogger)
.whenNoAncestorNamed("debug");
// Bind based on ancestor tagging
container.bind<ICache>("Cache")
.to(RedisCache)
.whenAnyAncestorTagged("performance", "high");
container.bind<ICache>("Cache")
.to(MemoryCache)
.whenNoAncestorTagged("performance", "high");// Environment-based binding
container.bind<IEmailService>("EmailService")
.to(SmtpEmailService)
.when((request) => {
return process.env.NODE_ENV === "production";
});
container.bind<IEmailService>("EmailService")
.to(MockEmailService)
.when((request) => {
return process.env.NODE_ENV !== "production";
});
// Time-based binding
container.bind<IGreetingService>("GreetingService")
.to(MorningGreetingService)
.when(() => {
const hour = new Date().getHours();
return hour >= 6 && hour < 12;
});
container.bind<IGreetingService>("GreetingService")
.to(EveningGreetingService)
.when(() => {
const hour = new Date().getHours();
return hour >= 18 || hour < 6;
});
// Request context-based binding
container.bind<IAuthService>("AuthService")
.to(AdminAuthService)
.when((request) => {
// Check if any ancestor service has admin role
let current = request.currentRequest;
while (current) {
if (current.target?.hasTag("role", "admin")) {
return true;
}
current = current.parentRequest;
}
return false;
});interface ResolutionContext {
container: Container;
currentRequest: Request;
plan: Plan;
addPlan(plan: Plan): void;
}
interface Request {
serviceIdentifier: ServiceIdentifier;
parentRequest: Request | null;
target: Target | null;
childRequests: Request[];
bindings: Binding<any>[];
requestScope: Map<any, any> | null;
addChildRequest(serviceIdentifier: ServiceIdentifier, bindings: Binding<any>[], target: Target): Request;
}
interface Target {
serviceIdentifier: ServiceIdentifier;
name: TaggedType<string>;
tags: TaggedType<any>[];
hasTag(key: string, value?: any): boolean;
isArray(): boolean;
matchesArray(name: string): boolean;
matchesNamedConstraint(constraint: string): boolean;
matchesTaggedConstraint(key: string, value: any): boolean;
}interface ITenantService {
getTenantData(): any;
}
@injectable()
class EnterpriseTenantuService implements ITenantService {
getTenantData() { return { type: "enterprise", features: ["advanced"] }; }
}
@injectable()
class BasicTenantService implements ITenantService {
getTenantData() { return { type: "basic", features: ["standard"] }; }
}
// Complex tenant-based binding
container.bind<ITenantService>("TenantService")
.to(EnterpriseTenantService)
.when((request) => {
// Check if request comes from enterprise context
let current = request.currentRequest;
while (current) {
if (current.target?.hasTag("tenant", "enterprise")) {
return true;
}
current = current.parentRequest;
}
return false;
});
container.bind<ITenantService>("TenantService")
.to(BasicTenantService)
.when(() => true); // Default fallback
@injectable()
class EnterpriseController {
constructor(
@inject("TenantService") @tagged("tenant", "enterprise")
private tenantService: ITenantService
) {}
}