Skip to content
Buy me a coffee

Service management

A ServiceCollection is a mutable list of registrations until you call buildServiceProvider(). These methods let libraries register defaults that applications can override, and let tests swap pieces out.

Method Does
tryAddSingleton / tryAddScoped / tryAddTransient Register only if the token has no plain registration yet
remove(token) Remove every registration of the token, keyed ones included
replace(token, impl, deps?) Swap the plain registrations for one new one, keeping the lifetime
has(token, key?) Is it registered in this collection?
provider.isService(token, key?) Is it registered in this provider? (false once disposed)

tryAdd* takes the same forms as add* and does nothing when the token already has a plain registration. Use it in code that sets up defaults, so whoever registers first wins:

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 Logger
Logger
{
Logger.log(msg: string): void
log
(
msg: string
msg
: string): void;
}
const
const Logger: InjectionToken<Logger>
Logger
=
createToken<Logger>(name: string): InjectionToken<Logger>

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 Logger
Logger
>('Logger');
class
class ConsoleLogger
ConsoleLogger
implements
interface Logger
Logger
{
ConsoleLogger.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 JsonLogger
JsonLogger
implements
interface Logger
Logger
{
JsonLogger.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
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@param ― value A JavaScript value, usually an object or array, to be converted.

@param ― replacer A function that transforms the results.

@param ― space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws ― {TypeError} If a circular reference or a BigInt value is found.

stringify
({
msg: string
msg
}));
}
}
// A library's setup function: only fills gaps.
function
function addMyLibrary(services: ServiceCollection): ServiceCollection
addMyLibrary
(
services: ServiceCollection
services
:
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
) {
services: ServiceCollection
services
.
ServiceCollection.tryAddSingleton<Logger>(token: Token<Logger>, implementation: Newable<NoInfer<Logger>, []>): ServiceCollection (+4 overloads)

Like

addSingleton

, but does nothing if token is already registered.

tryAddSingleton
(
const Logger: InjectionToken<Logger>
Logger
,
class ConsoleLogger
ConsoleLogger
);
return
services: ServiceCollection
services
;
}
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: Token<Logger>, implementation: Newable<NoInfer<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
(
const Logger: InjectionToken<Logger>
Logger
,
class JsonLogger
JsonLogger
); // the app's choice
function addMyLibrary(services: ServiceCollection): ServiceCollection
addMyLibrary
(
const services: ServiceCollection
services
); // keeps JsonLogger

Compare with add*, which appends: the last registration would win for getService, and getServices would return both. Keyed registrations don’t count: tryAddSingleton(Logger, ...) still registers when only keyed Loggers exist.

replace drops the plain registrations of a token and adds one new registration with the lifetime of the last one (singleton if there was none). Dependencies are type-checked like add*:

class
class FixedClock
FixedClock
extends
class Clock
Clock
{
override
FixedClock.now(): Date
now
() {
return new
var Date: DateConstructor
new (value: number | string | Date) => Date (+3 overloads)
Date
('2026-01-01T00:00:00Z');
}
}
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.addScoped<Clock>(token: Newable<Clock, []>): ServiceCollection (+4 overloads)

Registers a scoped service: one instance per scope. Same forms as

addSingleton

.

addScoped
(
class Clock
Clock
);
const services: ServiceCollection
services
.
ServiceCollection.replace<Clock>(token: Token<Clock>, implementation: Newable<NoInfer<Clock>, []>): ServiceCollection (+2 overloads)

Replaces the plain registrations of token, keeping the lifetime of the last one (singleton if there was none).

replace
(
class Clock
Clock
,
class FixedClock
FixedClock
); // still scoped
const services: ServiceCollection
services
.
ServiceCollection.replace<Clock>(token: Token<Clock>, factory: ServiceFactory<NoInfer<Clock>>): ServiceCollection (+2 overloads)

Replaces the plain registrations of token, keeping the lifetime of the last one (singleton if there was none).

replace
(
class Clock
Clock
, () => new
constructor FixedClock(): FixedClock
FixedClock
()); // a factory works too

Keyed registrations of the token are left alone. To replace a keyed one, register the same token and key again.

remove(token) deletes every registration of the token: all plain registrations and all keyed ones. It returns the collection, so calls chain. (removeAll from @ylcnfrht/ioc is gone; remove does the same.)

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<Cache>(token: Newable<Cache, []>): 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 Cache
Cache
).
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.remove<Cache>(token: Token<Cache>): ServiceCollection

Removes every registration of token, keyed ones included.

remove
(
class Cache
Cache
);
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'); // false

services.has(token) checks plain registrations, services.has(token, key) a keyed one. On a built provider, provider.isService(token, key?) answers the same question without resolving anything, and returns false after the provider (or scope) is disposed.

if (
const provider: ServiceProvider
provider
.
ServiceProvider.isService<{
inc(name: string): void;
}>(token: Token<{
inc(name: string): void;
}>, key?: ServiceKey): boolean

True if token (or token with key) is registered. False once disposed.

isService
(
const Metrics: InjectionToken<{
inc(name: string): void;
}>
Metrics
)) {
const
const metrics: {
inc(name: string): void;
}
metrics
= await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<{
inc(name: string): void;
}>(token: Token<{
inc(name: string): void;
}>): Promise<{
inc(name: string): void;
}>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
const Metrics: InjectionToken<{
inc(name: string): void;
}>
Metrics
);
const metrics: {
inc(name: string): void;
}
metrics
.
function inc(name: string): void
inc
('started');
}

buildServiceProvider() copies the registrations. Adding, removing or replacing afterwards affects the next provider you build, never one that already exists:

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 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 services: ServiceCollection
services
.
ServiceCollection.addSingleton<Mailer>(token: Newable<Mailer, []>): 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 Mailer
Mailer
);
const provider: ServiceProvider
provider
.
ServiceProvider.isService<Mailer>(token: Token<Mailer>, key?: ServiceKey): boolean

True if token (or token with key) is registered. False once disposed.

isService
(
class Mailer
Mailer
); // false: registered after the build
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
().
ServiceProvider.isService<Mailer>(token: Token<Mailer>, key?: ServiceKey): boolean

True if token (or token with key) is registered. False once disposed.

isService
(
class Mailer
Mailer
); // true