Skip to content
Buy me a coffee

Factories

A factory is a function that builds the instance. Use one when a constructor call is not enough: the instance comes from another library, it needs a value computed at runtime, or it must finish async setup (open a connection, load a file) before anyone uses it. The .NET equivalent is AddSingleton(sp => ...).

Pass a function where an implementation would go. It works with every lifetime and with keyed registrations.

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 '@nodelibraries/ioc';
interface
interface Config
Config
{
Config.dbUrl: string
dbUrl
: string;
Config.poolSize: number
poolSize
: number;
}
const
const Config: InjectionToken<Config>
Config
=
createToken<Config>(name: string): InjectionToken<Config>

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 Config
Config
>('Config');
class
class Database
Database
{
constructor(readonly
Database.url: string
url
: string, readonly
Database.poolSize: number
poolSize
: number) {}
}
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<Config>(token: Token<Config>, value: NoInfer<Config>): 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<Config>
Config
, {
Config.dbUrl: string
dbUrl
: 'postgres://localhost/app',
Config.poolSize: number
poolSize
: 10 });
const services: ServiceCollection
services
.
ServiceCollection.addSingleton<Database>(token: Token<Database>, factory: ServiceFactory<NoInfer<Database>>): 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
, async (
provider: ServiceProvider
provider
) => {
const
const config: Config
config
= await
provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<Config>(token: Token<Config>): Promise<Config>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
const Config: InjectionToken<Config>
Config
);
return new
constructor Database(url: string, poolSize: number): Database
Database
(
const config: Config
config
.
Config.dbUrl: string
dbUrl
,
const config: Config
config
.
Config.poolSize: number
poolSize
);
});
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 db: Database
db
= await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<Database>(token: Token<Database>): Promise<Database>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class Database
Database
);

The signature is (provider: ServiceProvider, key?: ServiceKey) => T | Promise<T>. The factory runs once per singleton, once per scope for scoped services, and on every resolution for transients, exactly like a class.

Resolution is always async, so a factory may return a promise. Everyone who resolves the service waits for it, and concurrent resolutions of a singleton or scoped service share the same pending promise: the factory runs once.

class
class Db
Db
{
constructor(private readonly
Db.conn: {
query(sql: string): Promise<unknown[]>;
close(): Promise<void>;
}
conn
:
type Awaited<T> = T extends null | undefined ? T : T extends object & {
then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : T

Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to never. This emulates the behavior of await.

Awaited
<
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

Obtain the return type of a function type

ReturnType
<typeof
function connect(url: string): Promise<{
query(sql: string): Promise<unknown[]>;
close(): Promise<void>;
}>
connect
>>) {}
static async
Db.open(url: string): Promise<Db>
open
(
url: string
url
: string) {
return new
constructor Db(conn: Awaited<ReturnType<typeof connect>>): Db
Db
(await
function connect(url: string): Promise<{
query(sql: string): Promise<unknown[]>;
close(): Promise<void>;
}>
connect
(
url: string
url
));
}
Db.query(sql: string): Promise<unknown[]>
query
(
sql: string
sql
: string) {
return this.
Db.conn: {
query(sql: string): Promise<unknown[]>;
close(): Promise<void>;
}
conn
.
function query(sql: string): Promise<unknown[]>
query
(
sql: string
sql
);
}
async
Db.onDestroy(): Promise<void>
onDestroy
() {
await this.
Db.conn: {
query(sql: string): Promise<unknown[]>;
close(): Promise<void>;
}
conn
.
function close(): Promise<void>
close
();
}
}
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<Db>(token: Token<Db>, factory: ServiceFactory<NoInfer<Db>>): 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 Db
Db
, () =>
class Db
Db
.
Db.open(url: string): Promise<Db>
open
('postgres://localhost/app'));
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 a: Db
a
,
const b: Db
b
] = await
var Promise: PromiseConstructor

Represents the completion of an asynchronous operation

Promise
.
PromiseConstructor.all<[Promise<Db>, Promise<Db>]>(values: [Promise<Db>, Promise<Db>]): Promise<[Db, Db]> (+1 overload)

Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when any Promise is rejected.

@param ― values An array of Promises.

@returns ― A new Promise.

all
([
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<Db>(token: Token<Db>): Promise<Db>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class Db
Db
),
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredService<Db>(token: Token<Db>): Promise<Db>

Resolves token, throwing ServiceNotFoundError if it is not registered.

getRequiredService
(
class Db
Db
)]);
// a === b: one connection, opened once

The alternative is onInit() on a class registration (see Lifecycle hooks). Prefer a factory when the instance can’t exist before setup finishes; prefer onInit when the class is fine to construct and only needs warming up.

The provider argument is not the root provider. It is a view of the provider doing the resolution (the root or a scope) that also carries the current resolution chain. Two things follow:

  • Scoped services resolved inside a factory come from the right scope.
  • Cycle detection and scope validation see through the factory. A singleton factory that resolves a scoped service fails with a ScopeError under validateScopes, with the full path in the message:
ScopeError: Scoped service 'Req' cannot be injected into singleton 'Cache' (Cache → Req): the singleton would keep one scope's instance forever.

Resolve through the provider you are given, not through a provider captured from outside: only the given one carries the chain, so only it lets cycle detection and scope validation see what the factory resolves. Once the factory returns, the view stops carrying the chain and behaves like the provider it came from, so keeping it in the instance for later, lazy resolutions is safe.

A keyed factory receives the key as the second argument, so one function can serve several keys:

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 '@nodelibraries/ioc';
interface
interface Cache
Cache
{
readonly
Cache.region: string
region
: string;
}
const
const Cache: InjectionToken<Cache>
Cache
=
createToken<Cache>(name: string): InjectionToken<Cache>

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 Cache
Cache
>('Cache');
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 makeCache: (_: unknown, key?: string | symbol) => Cache
makeCache
= (
_: unknown
_
: unknown,
key: string | symbol | undefined
key
?: string | symbol):
interface Cache
Cache
=> ({
Cache.region: string
region
:
var String: StringConstructor
(value?: any) => string

Allows manipulation and formatting of text strings and determination and location of substrings within strings.

String
(
key: string | symbol | undefined
key
) });
const services: ServiceCollection
services
.
ServiceCollection.addKeyedSingleton<Cache>(token: Token<Cache>, factory: ServiceFactory<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
(
const Cache: InjectionToken<Cache>
Cache
,
const makeCache: (_: unknown, key?: string | symbol) => Cache
makeCache
, 'eu');
const services: ServiceCollection
services
.
ServiceCollection.addKeyedSingleton<Cache>(token: Token<Cache>, factory: ServiceFactory<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
(
const Cache: InjectionToken<Cache>
Cache
,
const makeCache: (_: unknown, key?: string | symbol) => Cache
makeCache
, 'us');
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 eu: Cache
eu
= await
const provider: ServiceProvider
provider
.
ServiceProvider.getRequiredKeyedService<Cache>(token: Token<Cache>, key: ServiceKey): Promise<Cache>

Resolves the keyed registration of token for key, throwing ServiceNotFoundError if there is none.

getRequiredKeyedService
(
const Cache: InjectionToken<Cache>
Cache
, 'eu'); // { region: 'eu' }

See Keyed services.

The container decides by syntax:

You pass Treated as
class Foo {} Implementation: called with new, dependencies injected, onInit called
() => ..., function () {...}, async () => ... Factory: called with (provider, key)

Only class syntax counts as an implementation. A constructor function written as function Foo() { this.x = 1 } (or a class transpiled to ES5) passed as the implementation is treated as a factory and called without new. Use class syntax, or wrap it: (p) => new Foo().

Factories have no dependency array: validateOnBuild can’t see what a factory will resolve, and diagnostics show a factory registration as a leaf.