CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-inversify

A powerful and lightweight inversion of control container for JavaScript and Node.js apps powered by TypeScript.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

conditional.mddocs/

Conditional Resolution

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.

Conditional Binding Syntax

BindWhenFluentSyntax

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>;
}

Named Bindings

Use named bindings to distinguish between multiple implementations of the same interface.

Target Named Binding

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}`);
  }
}

Parent Named Binding

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");

Tagged Bindings

Use tagged bindings for more complex conditional resolution based on key-value metadata.

Target Tagged Binding

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}`);
    }
  }
}

Multiple Tag Conditions

// 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
  ) {}
}

Injection Context Binding

Injected Into Binding

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>
  ) {}
}

Ancestor-Based Binding

Any Ancestor Conditions

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
}

Ancestor Named/Tagged Conditions

// 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");

Custom Conditional Logic

Custom Constraint Functions

// 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;
  });

Resolution Context

ResolutionContext Interface

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;
}

Complex Conditional Examples

Multi-Tenant Application

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
  ) {}
}

Best Practices

  1. Use specific conditions: Make conditional logic as specific as possible
  2. Order matters: More specific bindings should be registered first
  3. Provide fallbacks: Always have a default binding when using conditions
  4. Keep constraints simple: Complex logic can impact performance
  5. Test conditional bindings: Verify all conditional paths work correctly
  6. Document complex conditions: Make conditional logic clear for maintainers
  7. Use named/tagged over custom: Prefer declarative over imperative conditions
  8. Consider performance: Complex constraint functions are evaluated at resolution time

docs

binding.md

conditional.md

container.md

decorators.md

index.md

lifecycle.md

modules.md

tile.json