Skip to content
Buy me a coffee

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.

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.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

ServiceCollection
,
function createToken<T>(name: string): InjectionToken<T>

Creates a typed token.

@example

interface Logger { log(msg: string): void }
const Logger = createToken<Logger>('Logger');
services.addSingleton(Logger, ConsoleLogger);
const logger = await provider.getRequiredService(Logger); // typed as Logger

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.

@example

interface Logger { log(msg: string): void }
const Logger = createToken<Logger>('Logger');
services.addSingleton(Logger, ConsoleLogger);
const logger = await provider.getRequiredService(Logger); // typed as Logger

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 stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.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 out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.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

@see ― source

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 stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since ― v0.1.100

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.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

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'); // undefined
  • getKeyedService(token, key) returns undefined when nothing is registered for that key.
  • getRequiredKeyedService(token, key) throws a ServiceNotFoundError with token and key set: 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.

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.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

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
); // false
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
, '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
); // undefined
await
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 Cache
const 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'); // true

Register a plain default next to the keyed ones if callers should get something without a 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.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

ServiceCollection
,
function createToken<T>(name: string): InjectionToken<T>

Creates a typed token.

@example

interface Logger { log(msg: string): void }
const Logger = createToken<Logger>('Logger');
services.addSingleton(Logger, ConsoleLogger);
const logger = await provider.getRequiredService(Logger); // typed as Logger

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.

@example

interface Logger { log(msg: string): void }
const Logger = createToken<Logger>('Logger');
services.addSingleton(Logger, ConsoleLogger);
const logger = await provider.getRequiredService(Logger); // typed as Logger

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.

@example

const services = new ServiceCollection();
services.addSingleton(Logger, ConsoleLogger);
services.addScoped(UserService, [Logger]);
const provider = services.buildServiceProvider({ validateScopes: true });

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

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.