Auto-generated service factories
Imagine you have a class which you need to instantiate possibly multiple times with some arguments passed manually at the call site and others injected by the DI container. For example:
export class UserNotificationChannel {
constructor(
// we want the DI to inject this:
private readonly transport: NotificationTransport,
// but we need to pass this manually:
private readonly username: string,
) {}
}
Some people would just inject NotificationTransport
wherever they need to
create new instances of UserNotificationChannel
and then create those
instances manually, but it's probably easy to see how that could become a chore
if UserNotificationChannel
has multiple dependencies. You could write
a factory service manually, e.g. like this:
export class UserNotificationChannelFactory {
constructor(
// let's be extra lazy and ask for a _lazy accessor_ instead of the resolved instance:
private readonly getTransport: () => NotificationTransport,
) {}
create(username: string): UserNotificationChannel {
return new UserNotificationChannel(this.getTransport(), username);
}
}
... but that still means you have to manage the dependencies yourself at some point. Instead, you can just declare the interface for the factory service with just the arguments you want to pass manually, and DICC will generate the implementation for you and inject it where appropriate:
export interface UserNotificationChannelFactory {
create(username: string): UserNotificationChannel;
}
Some notes on how it works:
- Both the factory interface and the service class need to be registered as
services, either implicitly by exporting them from a resource file, or
explicitly using a
satisfies
expression. - The arguments of the factory method or callback will be mapped to the target service's arguments by name during compilation. This means that the factory's arguments' positions and order can be arbitrary - they don't have to be in the same order as the target service's arguments.
- The generated factory will attempt to resolve the target service's
dependencies as lazily as possible; but if the
create()
method doesn't return a Promise and one or more of the target service's dependencies ends up being async, the factory service itself will be made async and the async dependencies will be resolved eagerly when creating an instance of the factory class, so that thecreate()
method can stay synchronous.
Auto-generated factories can also be generated from abstract classes: if the
compiler encounters an abstract class with a single abstract create()
method,
it will create a factory service by extending the class and implementing the create()
method using the same mechanism as for an interface. Such a factory
class can therefore serve other purposes than just creating an instance of the
target service: it can e.g. carry some metadata about the target service, so
that you can inject a list of factories and then determine at runtime which
target service you want to instantiate based on the metadata. This can be useful
e.g. for GraphQL class resolvers, where each resolver could have a corresponding
factory class which knows which operation the resolver belongs to, so that a
root resolver can examine the factory classes and instantiate the appropriate
resolver for an incoming GraphQL operation.
Auto-implemented accessor services
A special case of auto-implemented factories are accessor services, which
are interfaces (or abstract classes) with a single (abstract) get()
method
with no arguments. They behave exactly the same as auto-implemented factory
services, with the core distinction being that the target service isn't
unregistered from the container, and therefore its lifecycle can be managed
using the scope
option as usual, instead of effectively making it private
as auto-factories do.
Since auto-implemented factories and accessors can be derived from abstract classes (and therefore can carry additional data available before accessing the target service), you can use them for similar purposes as you would use service tags in other DI implementations, while retaining strict typing.