Keyed services
Keyed services answer “which one?” at resolution time: a Cache for 'eu' and one for 'us', a PaymentGateway per
provider, a Notifier per channel. They mirror .NET 8’s AddKeyedSingleton / GetRequiredKeyedService.
Register and resolve
Section titled “Register and resolve”addKeyedSingleton, addKeyedScoped and addKeyedTransient take the token, a class or factory, the key (a string or
symbol) and, for classes with constructor parameters, the dependency tokens:
import { class ServiceCollection
Collects service registrations, then builds a
ServiceProvider
.
Modeled on .NET's IServiceCollection.
ServiceCollection, function createToken<T>(name: string): InjectionToken<T>
Creates a typed token.
createToken } from '@ylcnfrht/ioc';
interface interface Notifier
Notifier { Notifier.send(to: string, text: string): Promise<void>
send(to: string
to: string, text: string
text: string): interface Promise<T>
Represents the completion of an asynchronous operation
Promise<void>;}const const Notifier: InjectionToken<Notifier>
Notifier = createToken<Notifier>(name: string): InjectionToken<Notifier>
Creates a typed token.
createToken<interface Notifier
Notifier>('Notifier');
class class Logger
Logger { Logger.log(msg: string): void
log(msg: string
msg: string) { var console: Console
The console module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
- A global
console instance configured to write to process.stdout and
process.stderr. The global console can be used without importing the node:console module.
Warning: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O for
more information.
Example using the global console:
console.log('hello world');// Prints: hello world, to stdoutconsole.log('hello %s', 'world');// Prints: hello world, to stdoutconsole.error(new Error('Whoops, something bad happened'));// Prints error message and stack trace to stderr:// Error: Whoops, something bad happened// at [eval]:5:15// at Script.runInThisContext (node:vm:132:18)// at Object.runInThisContext (node:vm:309:38)// at node:internal/process/execution:77:19// at [eval]-wrapper:6:22// at evalScript (node:internal/process/execution:76:60)// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';console.warn(`Danger ${name}! Danger!`);// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
const out = getStreamSomehow();const err = getStreamSomehow();const myConsole = new console.Console(out, err);
myConsole.log('hello world');// Prints: hello world, to outmyConsole.log('hello %s', 'world');// Prints: hello world, to outmyConsole.error(new Error('Whoops, something bad happened'));// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';myConsole.warn(`Danger ${name}! Danger!`);// Prints: Danger Will Robinson! Danger!, to err
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to stdout with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()).
const count = 5;console.log('count: %d', count);// Prints: count: 5, to stdoutconsole.log('count:', count);// Prints: count: 5, to stdout
See util.format() for more information.
log(msg: string
msg); }}class class EmailNotifier
EmailNotifier implements interface Notifier
Notifier { constructor(private readonly EmailNotifier.logger: Logger
logger: class Logger
Logger) {} async EmailNotifier.send(to: string, text: string): Promise<void>
send(to: string
to: string, text: string
text: string) { this.EmailNotifier.logger: Logger
logger.Logger.log(msg: string): void
log(`email to ${to: string
to}: ${text: string
text}`); }}class class SmsNotifier
SmsNotifier implements interface Notifier
Notifier { async SmsNotifier.send(to: string, text: string): Promise<void>
send(to: string
to: string, text: string
text: string) {}}
const const services: ServiceCollection
services = new new ServiceCollection(): ServiceCollection
Collects service registrations, then builds a
ServiceProvider
.
Modeled on .NET's IServiceCollection.
ServiceCollection();const services: ServiceCollection
services.ServiceCollection.addSingleton<Logger>(token: Newable<Logger, []>): ServiceCollection (+4 overloads)
Registers a singleton: one instance for the root provider and all its scopes.
addSingleton(Class) / addSingleton(Class, [deps]): the class is its own token.
addSingleton(token, Implementation, [deps]): deps are checked against the constructor's parameters.
addSingleton(token, (provider) => instance): a factory, may be async.
addSingleton(class Logger
Logger);const services: ServiceCollection
services.ServiceCollection.addKeyedSingleton<Notifier, [logger: Logger]>(token: Token<Notifier>, implementation: Newable<NoInfer<Notifier>, [logger: Logger]>, key: ServiceKey, dependencies: readonly [logger: Token<Logger>]): ServiceCollection (+2 overloads)
Registers a keyed singleton, resolved with getKeyedService(token, key).
Keyed registrations are separate from plain ones: getService(token) never returns them.
addKeyedSingleton(const Notifier: InjectionToken<Notifier>
Notifier, class EmailNotifier
EmailNotifier, 'email', [class Logger
Logger]);const services: ServiceCollection
services.ServiceCollection.addKeyedTransient<Notifier>(token: Token<Notifier>, implementation: Newable<NoInfer<Notifier>, []>, key: ServiceKey): ServiceCollection (+2 overloads)
Registers a keyed transient service. See
addKeyedSingleton
.
addKeyedTransient(const Notifier: InjectionToken<Notifier>
Notifier, class SmsNotifier
SmsNotifier, 'sms');
const const provider: ServiceProvider
provider = const services: ServiceCollection
services.ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider
Builds the provider. Registrations are copied, so changing this collection afterwards
does not affect providers already built.
buildServiceProvider();const email = await const provider: ServiceProvider
provider.ServiceProvider.getRequiredKeyedService<Notifier>(token: Token<Notifier>, key: ServiceKey): Promise<Notifier>
Resolves the keyed registration of token for key, throwing ServiceNotFoundError if there is none.
getRequiredKeyedService(const Notifier: InjectionToken<Notifier>
Notifier, 'email');const email: Notifier
const const push: Notifier | undefined
push = await const provider: ServiceProvider
provider.ServiceProvider.getKeyedService<Notifier>(token: Token<Notifier>, key: ServiceKey): Promise<Notifier | undefined>
Resolves the keyed registration of token for key, or undefined.
getKeyedService(const Notifier: InjectionToken<Notifier>
Notifier, 'push'); // undefinedgetKeyedService(token, key)returnsundefinedwhen nothing is registered for that key.getRequiredKeyedService(token, key)throws aServiceNotFoundErrorwithtokenandkeyset:No service registered for 'Notifier' with key 'push'.
Dependencies of keyed classes are checked like any other registration: the count at registration time
(RegistrationError), the types at compile time.
One registration per key
Section titled “One registration per key”Registering the same token and key again replaces the earlier one; there is no “all registrations for a key”. If you need a list, use multiple implementations instead.
Keyed and plain registrations are separate
Section titled “Keyed and plain registrations are separate”A keyed registration never answers a plain lookup, and a plain one never answers a keyed lookup:
const const services: ServiceCollection
services = new new ServiceCollection(): ServiceCollection
Collects service registrations, then builds a
ServiceProvider
.
Modeled on .NET's IServiceCollection.
ServiceCollection();const services: ServiceCollection
services.ServiceCollection.addKeyedSingleton<Cache>(token: Token<Cache>, implementation: Newable<NoInfer<Cache>, []>, key: ServiceKey): ServiceCollection (+2 overloads)
Registers a keyed singleton, resolved with getKeyedService(token, key).
Keyed registrations are separate from plain ones: getService(token) never returns them.
addKeyedSingleton(class Cache
Cache, class Cache
Cache, 'eu');
const services: ServiceCollection
services.ServiceCollection.has<Cache>(token: Token<Cache>, key?: ServiceKey): boolean
True if token (or token with key) is registered.
has(class Cache
Cache); // falseconst services: ServiceCollection
services.ServiceCollection.has<Cache>(token: Token<Cache>, key?: ServiceKey): boolean
True if token (or token with key) is registered.
has(class Cache
Cache, 'eu'); // true
const const provider: ServiceProvider
provider = const services: ServiceCollection
services.ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider
Builds the provider. Registrations are copied, so changing this collection afterwards
does not affect providers already built.
buildServiceProvider();await const provider: ServiceProvider
provider.ServiceProvider.getService<Cache>(token: Token<Cache>): Promise<Cache | undefined>
Resolves token, or returns undefined if it is not registered. With several registrations, the last one wins.
getService(class Cache
Cache); // undefinedawait const provider: ServiceProvider
provider.ServiceProvider.getKeyedService<Cache>(token: Token<Cache>, key: ServiceKey): Promise<Cache | undefined>
Resolves the keyed registration of token for key, or undefined.
getKeyedService(class Cache
Cache, 'eu'); // a Cacheconst provider: ServiceProvider
provider.ServiceProvider.isService<Cache>(token: Token<Cache>, key?: ServiceKey): boolean
True if token (or token with key) is registered. False once disposed.
isService(class Cache
Cache, 'eu'); // trueRegister a plain default next to the keyed ones if callers should get something without a key.
Factories get the key
Section titled “Factories get the key”A keyed factory receives (provider, key), so one function can build every variant:
import { class ServiceCollection
Collects service registrations, then builds a
ServiceProvider
.
Modeled on .NET's IServiceCollection.
ServiceCollection, function createToken<T>(name: string): InjectionToken<T>
Creates a typed token.
createToken, type type ServiceKey = string | symbol
Key of a keyed service.
ServiceKey } from '@ylcnfrht/ioc';
interface interface Gateway
Gateway { readonly Gateway.name: string
name: string;}const const Gateway: InjectionToken<Gateway>
Gateway = createToken<Gateway>(name: string): InjectionToken<Gateway>
Creates a typed token.
createToken<interface Gateway
Gateway>('Gateway');
const const services: ServiceCollection
services = new new ServiceCollection(): ServiceCollection
Collects service registrations, then builds a
ServiceProvider
.
Modeled on .NET's IServiceCollection.
ServiceCollection();const const gateway: (_: unknown, key?: ServiceKey) => Gateway
gateway = (_: unknown
_: unknown, key: ServiceKey | undefined
key?: type ServiceKey = string | symbol
Key of a keyed service.
ServiceKey): interface Gateway
Gateway => ({ Gateway.name: string
name: var String: StringConstructor(value?: any) => string
Allows manipulation and formatting of text strings and determination and location of substrings within strings.
String(key: ServiceKey | undefined
key) });for (const const key: string
key of ['stripe', 'adyen', 'paypal']) const services: ServiceCollection
services.ServiceCollection.addKeyedScoped<Gateway>(token: Token<Gateway>, factory: ServiceFactory<NoInfer<Gateway>>, key: ServiceKey): ServiceCollection (+2 overloads)
Registers a keyed scoped service. See
addKeyedSingleton
.
addKeyedScoped(const Gateway: InjectionToken<Gateway>
Gateway, const gateway: (_: unknown, key?: ServiceKey) => Gateway
gateway, const key: string
key);
const const provider: ServiceProvider
provider = const services: ServiceCollection
services.ServiceCollection.buildServiceProvider(options?: BuildOptions): ServiceProvider
Builds the provider. Registrations are copied, so changing this collection afterwards
does not affect providers already built.
buildServiceProvider();await using await using scope: ServiceProvider
scope = const provider: ServiceProvider
provider.ServiceProvider.createScope(): ServiceProvider
Creates a scope: scoped services resolve once per scope. Dispose it when done (or use await using).
createScope();const const pick: (country: string) => "stripe" | "adyen"
pick = (country: string
country: string) => (country: string
country === 'NL' ? 'adyen' : 'stripe');const const gw: Gateway
gw = await await using scope: ServiceProvider
scope.ServiceProvider.getRequiredKeyedService<Gateway>(token: Token<Gateway>, key: ServiceKey): Promise<Gateway>
Resolves the keyed registration of token for key, throwing ServiceNotFoundError if there is none.
getRequiredKeyedService(const Gateway: InjectionToken<Gateway>
Gateway, const pick: (country: string) => "stripe" | "adyen"
pick('NL')); // { name: 'adyen' }Lifetimes
Section titled “Lifetimes”Keyed services follow the same lifetime rules as plain ones: a keyed singleton is shared by every scope, a keyed scoped
service is one instance per scope and key, a keyed transient is new on every call. remove(token) removes the keyed
registrations of that token as well; see Service management.