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)%').
