Skip to content
Buy me a coffee

Diagnostics

Four methods on ServiceCollection answer “what does this service pull in?” and “where are the cycles?”. They read the registrations only: nothing is constructed, so you can call them in a test, a CLI command or a startup log.

Method Returns
getDependencyTree(token) A DependencyTreeNode tree
visualizeDependencyTree(token) The same tree as text
getCircularDependencies() Every cycle, as CircularDependency[]
visualizeCircularDependencies() The cycles as text

Edit the registrations and watch the graph. Cycles come from getCircularDependencies(); captive dependencies (a singleton reaching a scoped service) are the ones validateScopes would reject.

6registrations
0cycles
0captive
0missing
ConfigSILoggerSIDbSIUserRepositorySCUserServiceSCUserControllerTR
SI singletonSC scopedTR transient cycle captive dependencyClick a service to resolve it for real.
No cycles, nothing missing, no captive dependencies.
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');
const
const Config: InjectionToken<{
url: string;
}>
Config
=
createToken<{
url: string;
}>(name: string): InjectionToken<{
url: string;
}>

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
<{
url: string
url
: string }>('Config');
class
class ConsoleLogger
ConsoleLogger
implements
interface Logger
Logger
{
ConsoleLogger.log(msg: string): void
log
(
msg: string
msg
: string) {}
}
class
class Database
Database
{
constructor(readonly
Database.config: {
url: string;
}
config
: {
url: string
url
: string }, readonly
Database.logger: Logger
logger
:
interface Logger
Logger
) {}
}
class
class UserRepository
UserRepository
{
constructor(readonly
UserRepository.db: Database
db
:
class Database
Database
) {}
}
class
class Mailer
Mailer
{
constructor(readonly
Mailer.logger: Logger
logger
:
interface Logger
Logger
) {}
}
class
class UserService
UserService
{
constructor(readonly
UserService.repo: UserRepository
repo
:
class UserRepository
UserRepository
, readonly
UserService.mailer: Mailer
mailer
:
class Mailer
Mailer
, readonly
UserService.logger: Logger
logger
:
interface Logger
Logger
) {}
}
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.addValue<{
url: string;
}>(token: Token<{
url: string;
}>, value: NoInfer<{
url: string;
}>): ServiceCollection

Registers an existing value (config, constants, an instance you built). Any value works, null and undefined included. Never disposed by the container.

addValue
(
const Config: InjectionToken<{
url: string;
}>
Config
, {
url: string
url
: 'postgres://localhost/app' });
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 ConsoleLogger
ConsoleLogger
);
const services: ServiceCollection
services
.
ServiceCollection.addSingleton<Database, [config: {
url: string;
}, logger: Logger]>(token: Newable<Database, [config: {
url: string;
}, logger: Logger]>, dependencies: readonly [config: Token<{
url: string;
}>, logger: Token<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 Database
Database
, [
const Config: InjectionToken<{
url: string;
}>
Config
,
const Logger: InjectionToken<Logger>
Logger
]);
const services: ServiceCollection
services
.
ServiceCollection.addScoped<UserRepository, [db: Database]>(token: Newable<UserRepository, [db: Database]>, dependencies: readonly [db: Token<Database>]): ServiceCollection (+4 overloads)

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

addSingleton

.

addScoped
(
class UserRepository
UserRepository
, [
class Database
Database
]);
const services: ServiceCollection
services
.
ServiceCollection.addTransient<Mailer, [logger: Logger]>(token: Newable<Mailer, [logger: Logger]>, dependencies: readonly [logger: Token<Logger>]): ServiceCollection (+4 overloads)

Registers a transient service: a new instance on every resolution. Same forms as

addSingleton

.

addTransient
(
class Mailer
Mailer
, [
const Logger: InjectionToken<Logger>
Logger
]);
const services: ServiceCollection
services
.
ServiceCollection.addScoped<UserService, [repo: UserRepository, mailer: Mailer, logger: Logger]>(token: Newable<UserService, [repo: UserRepository, mailer: Mailer, logger: Logger]>, dependencies: readonly [repo: Token<UserRepository>, mailer: Token<Mailer>, logger: Token<Logger>]): ServiceCollection (+4 overloads)

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

addSingleton

.

addScoped
(
class UserService
UserService
, [
class UserRepository
UserRepository
,
class Mailer
Mailer
,
const Logger: InjectionToken<Logger>
Logger
]);
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
(
const services: ServiceCollection
services
.
ServiceCollection.visualizeDependencyTree(token: Token): string

The dependency tree of token as text.

visualizeDependencyTree
(
class UserService
UserService
));

prints:

└── UserService [SCOPED]
├── UserRepository [SCOPED]
│ └── Database [SINGLETON]
│ ├── Config [SINGLETON]
│ └── Logger [SINGLETON]
├── Mailer [TRANSIENT]
│ └── Logger [SINGLETON]
└── Logger [SINGLETON]

getDependencyTree returns the same thing as data. Each node has token, name, lifetime, depth and dependencies:

{
"name": "Mailer",
"lifetime": "TRANSIENT",
"depth": 0,
"dependencies": [{ "name": "Logger", "lifetime": "SINGLETON", "depth": 1, "dependencies": [] }]
}

Two extra lifetime values show problems:

  • NOT_REGISTERED: a dependency has no plain registration (└── Nope [NOT_REGISTERED]).
  • CIRCULAR: the dependency is already on the path. The node has isCircular: true and circularPath, and the text form marks it: └── A [CIRCULAR] ⚠️ CIRCULAR.

Values show as [SINGLETON]. Factories show as leaves: their dependencies are inside a function the container can’t inspect.

class
class A
A
{
constructor(readonly
A.b: B
b
:
class B
B
) {}
}
class
class B
B
{
constructor(readonly
B.c: C
c
:
class C
C
) {}
}
class
class C
C
{
constructor(readonly
C.a: A
a
:
class A
A
) {}
}
class
class Self
Self
{
constructor(readonly
Self.self: Self
self
:
class Self
Self
) {}
}
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<A, [b: B]>(token: Newable<A, [b: B]>, dependencies: readonly [b: Token<B>]): 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 A
A
, [
class B
B
]).
ServiceCollection.addSingleton<B, [c: C]>(token: Newable<B, [c: C]>, dependencies: readonly [c: Token<C>]): 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 B
B
, [
class C
C
]).
ServiceCollection.addSingleton<C, [a: A]>(token: Newable<C, [a: A]>, dependencies: readonly [a: Token<A>]): 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 C
C
, [
class A
A
]);
const services: ServiceCollection
services
.
ServiceCollection.addSingleton<Self, [self: Self]>(token: Newable<Self, [self: Self]>, dependencies: readonly [self: Token<Self>]): 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 Self
Self
, [
class Self
Self
]);
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
(
const services: ServiceCollection
services
.
ServiceCollection.visualizeCircularDependencies(): string

Every dependency cycle as text.

visualizeCircularDependencies
());
const
const cycles: CircularDependency[]
cycles
=
const services: ServiceCollection
services
.
ServiceCollection.getCircularDependencies(): CircularDependency[]

Every dependency cycle among plain registrations.

getCircularDependencies
();
const cycles: CircularDependency[]
cycles
.
Array<CircularDependency>.map<string[]>(callbackfn: (value: CircularDependency, index: number, array: CircularDependency[]) => string[], thisArg?: any): string[][]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.

map
((
c: CircularDependency
c
) =>
c: CircularDependency
c
.
CircularDependency.tokens: {
token: Token;
name: string;
}[]
tokens
.
Array<{ token: Token; name: string; }>.map<string>(callbackfn: (value: {
token: Token;
name: string;
}, index: number, array: {
token: Token;
name: string;
}[]) => string, thisArg?: any): string[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.

map
((
t: {
token: Token;
name: string;
}
t
) =>
t: {
token: Token;
name: string;
}
t
.
name: string
name
)); // [['A', 'B', 'C', 'A'], ['Self', 'Self']]

prints:

Found 2 circular dependencies:
Circular Dependency 1:
A → B → C → A
Circular Dependency 2:
Self → Self

With no cycles it prints No circular dependencies found.

A cycle is not an error: the container resolves it with a proxy (see Circular dependencies). The report tells you where a constructor must not touch its partner, and where a refactor might be worth it.

Put the checks in a test so a bad registration fails CI instead of production:

function it(name: string, fn: () => void): void
it
('wires the app', () => {
const
const services: ServiceCollection
services
=
function addAppServices(services: ServiceCollection): ServiceCollection
addAppServices
(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
());
function expect<() => ServiceProvider>(value: () => ServiceProvider): {
toEqual(expected: unknown): void;
not: {
toThrow(): void;
};
}
expect
(() =>
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
({
BuildOptions.validateOnBuild?: boolean | undefined

Check every registration up front (missing dependencies, captive scoped services) and throw a ValidationError.

validateOnBuild
: true,
BuildOptions.validateScopes?: boolean | undefined

Throw a ScopeError when a scoped service is resolved from the root provider or captured by a singleton. Recommended in development.

validateScopes
: true })).
not: {
toThrow(): void;
}
not
.
function toThrow(): void
toThrow
();
function expect<CircularDependency[]>(value: CircularDependency[]): {
toEqual(expected: unknown): void;
not: {
toThrow(): void;
};
}
expect
(
const services: ServiceCollection
services
.
ServiceCollection.getCircularDependencies(): CircularDependency[]

Every dependency cycle among plain registrations.

getCircularDependencies
()).
function toEqual(expected: unknown): void
toEqual
([]);
});

validateOnBuild reports missing dependencies and captive scoped services; see Validation.