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 => ...).
Register a factory
Section titled “Register a factory”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.
ServiceCollection, function createToken<T>(name: string): InjectionToken<T>
Creates a typed token.
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.
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.
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.
Async initialization
Section titled “Async initialization”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.
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.
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 onceThe 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 a factory receives
Section titled “The provider a factory receives”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
ScopeErrorundervalidateScopes, 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.
Factories for keyed services
Section titled “Factories for keyed services”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.
ServiceCollection, function createToken<T>(name: string): InjectionToken<T>
Creates a typed token.
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.
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.
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.
Function or class?
Section titled “Function or class?”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.