NewNode Dependency Injection v4 is now available.Ver en npm ->

Documentation

Keyed Services

Keyed Services

Group multiple implementations under a key and resolve the one you need.

Keyed services let you register multiple implementations of the same interface under a named group, then retrieve a specific one by key — or inject the whole group as a Map — without writing any factory boilerplate.

Registering keyed services

Programmatic API

import { ContainerBuilder, KeyedReference, KeyedGroupReference } from 'node-dependency-injection'
import StripePayment from './payments/StripePayment'
import PaypalPayment from './payments/PaypalPayment'

const container = new ContainerBuilder()

// Register implementations under the 'payment' group
container.registerKeyed('payment', 'stripe', StripePayment).setDefault(true)
container.registerKeyed('payment', 'paypal', PaypalPayment)

await container.compile()

// Retrieve by key
const stripe  = container.getKeyed('payment', 'stripe')
// Retrieve the default (the one marked with .setDefault(true))
const def     = container.getKeyed('payment')
// Get all implementations as a Map<key, instance>
const all     = container.getKeyedGroup('payment')

YAML configuration

services:
  payment.stripe:
    class: 'payments/StripePayment'
    keyed:
      group: payment
      key: stripe
      default: true

  payment.paypal:
    class: 'payments/PaypalPayment'
    keyed:
      group: payment
      key: paypal

Injecting keyed services

Injecting a specific key

Use KeyedReference (or @keyed(group, key) in YAML) to inject one specific implementation:

container.register('checkout', CheckoutService)
  .addArgument(new KeyedReference('payment', 'stripe'))
checkout:
  class: 'CheckoutService'
  arguments: ['@keyed(payment, stripe)']

Injecting the whole group

Use KeyedGroupReference (or @keyed_group(group) in YAML) to receive a Map<string, T> with all registered implementations:

container.register('payment.router', PaymentRouter)
  .addArgument(new KeyedGroupReference('payment'))
payment.router:
  class: 'PaymentRouter'
  arguments: ['@keyed_group(payment)']

Keyed defaults and cardinality

Keyed groups are plural collections, so a group does not require a default:

  • Zero defaults: valid. Explicit keyed lookups and full-group lookups work.
  • One default: valid. getKeyed('payment') resolves that implementation.
  • Multiple defaults: invalid because the singular lookup would be ambiguous.

The same rules apply to YAML default: true and the programmatic .setDefault(true) API. Validation reports multiple defaults as keyed_group_multiple_defaults. A group with no default is not a validation error, although getKeyed('payment') without a key still requires exactly one default and therefore throws when no default is asserted.

Autowire integration

When using Autowire you can inject keyed services into typed TypeScript constructor parameters by registering a bind whose name matches the parameter name exactly.

// TypeScript services
export default class CheckoutService {
  constructor(private readonly payment: IPaymentService) {}
}

export default class PaymentRouter {
  constructor(private readonly payments: Map<string, IPaymentService>) {}
}
container.registerKeyed('payment', 'stripe', StripePaymentService)
container.registerKeyed('payment', 'paypal', PaypalPaymentService)

// Bind name must match the constructor parameter name
container.addBind('payment',  new KeyedReference('payment', 'stripe'))
container.addBind('payments', new KeyedGroupReference('payment'))

const autowire = new Autowire(container)
await autowire.process()
await container.compile()

// container.get(CheckoutService).payment  → StripePaymentService instance
// container.get(PaymentRouter).payments   → Map { 'stripe' => …, 'paypal' => … }

Note: Named binds take priority over type-based resolution, so the same mechanism also works for scalars: container.addBind('apiKey', '%env(API_KEY)%').

Related guides