Skip to content
Buy me a coffee

Multiple implementations

Registering the same token more than once doesn’t overwrite anything: every registration is kept, in order. This is how plugins, middleware chains, validators and event handlers are usually wired, and it matches .NET’s IEnumerable<T> injection.

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 Validator
Validator
{
Validator.validate(order: {
total: number;
items: number;
}): string | undefined
validate
(
order: {
total: number;
items: number;
}
order
: {
total: number
total
: number;
items: number
items
: number }): string | undefined;
}
const
const Validator: InjectionToken<Validator>
Validator
=
createToken<Validator>(name: string): InjectionToken<Validator>

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 Validator
Validator
>('Validator');
class
class PositiveTotal
PositiveTotal
implements
interface Validator
Validator
{
PositiveTotal.validate(o: {
total: number;
}): "total must be positive" | undefined
validate
(
o: {
total: number;
}
o
: {
total: number
total
: number }) {
return
o: {
total: number;
}
o
.
total: number
total
> 0 ?
var undefined
undefined
: 'total must be positive';
}
}
class
class NotEmpty
NotEmpty
implements
interface Validator
Validator
{
NotEmpty.validate(o: {
items: number;
}): "order is empty" | undefined
validate
(
o: {
items: number;
}
o
: {
items: number
items
: number }) {
return
o: {
items: number;
}
o
.
items: number
items
> 0 ?
var undefined
undefined
: 'order is empty';
}
}
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<Validator>(token: Token<Validator>, implementation: Newable<NoInfer<Validator>, []>): 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 Validator: InjectionToken<Validator>
Validator
,
class PositiveTotal
PositiveTotal
);
const services: ServiceCollection
services
.
ServiceCollection.addSingleton<Validator>(token: Token<Validator>, implementation: Newable<NoInfer<Validator>, []>): 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 Validator: InjectionToken<Validator>
Validator
,
class NotEmpty
NotEmpty
);
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
const all: Validator[]
all
= await
const provider: ServiceProvider
provider
.
ServiceProvider.getServices<Validator>(token: Token<Validator>): Promise<Validator[]>

Resolves every registration of token, in registration order. Empty if none.

getServices
(
const Validator: InjectionToken<Validator>
Validator
); // [PositiveTotal, NotEmpty], registration order
const
const last: Validator
last
= await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<Validator>(token: Token<Validator>): Promise<Validator>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
const Validator: InjectionToken<Validator>
Validator
); // NotEmpty: the last registration wins
const
const errors: (string | undefined)[]
errors
=
const all: Validator[]
all
.
Array<Validator>.map<string | undefined>(callbackfn: (value: Validator, index: number, array: Validator[]) => string | undefined, thisArg?: any): (string | undefined)[]

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
((
v: Validator
v
) =>
v: Validator
v
.
Validator.validate(order: {
total: number;
items: number;
}): string | undefined
validate
({
total: number
total
: 0,
items: number
items
: 0 })).
Array<string | undefined>.filter(predicate: (value: string | undefined, index: number, array: (string | undefined)[]) => unknown, thisArg?: any): (string | undefined)[] (+1 overload)

Returns the elements of an array that meet the condition specified in a callback function.

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

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

filter
(
var Boolean: BooleanConstructor
Boolean
);
  • getServices(token) resolves every plain registration in registration order. It returns [] when there is none.
  • getService / getRequiredService (and dependency injection through [deps]) use the last registration. That makes “register a default, let the app register its own later” work without removing anything.
  • Keyed registrations are never part of getServices; see Keyed services.

Two registrations are two services, even when they use the same class and lifetime. Two singleton registrations give two singletons; two scoped registrations give two instances per scope:

class
class Handler
Handler
{}
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<unknown>(token: Token<unknown>, implementation: Newable<unknown, []>): ServiceCollection (+4 overloads)

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

addSingleton

.

addScoped
('handlers',
class Handler
Handler
);
const services: ServiceCollection
services
.
ServiceCollection.addScoped<unknown>(token: Token<unknown>, implementation: Newable<unknown, []>): ServiceCollection (+4 overloads)

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

addSingleton

.

addScoped
('handlers',
class Handler
Handler
);
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 a: unknown
a
,
const b: unknown
b
] = await
await using scope: ServiceProvider
scope
.
ServiceProvider.getServices<unknown>(token: Token<unknown>): Promise<unknown[]>

Resolves every registration of token, in registration order. Empty if none.

getServices
('handlers');
const a: unknown
a
===
const b: unknown
b
; // false: one instance per registration
(await
await using scope: ServiceProvider
scope
.
ServiceProvider.getService<unknown>(token: Token<unknown>): Promise<unknown>

Resolves token, or returns undefined if it is not registered. With several registrations, the last one wins.

getService
('handlers')) ===
const b: unknown
b
; // true: the last registration's scoped instance

The lifetime still applies per registration: calling getServices twice in the same scope returns the same scoped instances, and in another scope, new ones.

Dependency arrays inject one service per token. To give a class every implementation, use a factory:

class
class PluginHost
PluginHost
{
constructor(private readonly
PluginHost.plugins: Plugin[]
plugins
:
interface Plugin
Plugin
[]) {}
async
PluginHost.startAll(): Promise<void>
startAll
() {
for (const
const p: Plugin
p
of this.
PluginHost.plugins: Plugin[]
plugins
) await
const p: Plugin
p
.
Plugin.start(): Promise<void>
start
();
}
}
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<Plugin>(token: Token<Plugin>, factory: ServiceFactory<NoInfer<Plugin>>): 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 Plugin: InjectionToken<Plugin>
Plugin
, () => ({
Plugin.name: string
name
: 'metrics',
Plugin.start(): Promise<void>
start
: async () => {} }));
const services: ServiceCollection
services
.
ServiceCollection.addSingleton<Plugin>(token: Token<Plugin>, factory: ServiceFactory<NoInfer<Plugin>>): 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 Plugin: InjectionToken<Plugin>
Plugin
, () => ({
Plugin.name: string
name
: 'audit',
Plugin.start(): Promise<void>
start
: async () => {} }));
const services: ServiceCollection
services
.
ServiceCollection.addSingleton<PluginHost>(token: Token<PluginHost>, factory: ServiceFactory<NoInfer<PluginHost>>): 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 PluginHost
PluginHost
, async (
p: ServiceProvider
p
) => new
constructor PluginHost(plugins: Plugin[]): PluginHost
PluginHost
(await
p: ServiceProvider
p
.
ServiceProvider.getServices<Plugin>(token: Token<Plugin>): Promise<Plugin[]>

Resolves every registration of token, in registration order. Empty if none.

getServices
(
const Plugin: InjectionToken<Plugin>
Plugin
)));