@travetto/di
Version:
Dependency registration/management and injection support.
322 lines (246 loc) • 11.3 kB
Markdown
<!-- This file was generated by /doc and should not be modified directly -->
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/di/DOC.tsx and execute "npx trv doc" to rebuild -->
# Dependency Injection
## Dependency registration/management and injection support.
**Install: /di**
```bash
npm install /di
# or
yarn add /di
```
[Dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) is a framework primitive. When used in conjunction with automatic file scanning, it provides for handling of application dependency wiring. Due to the nature of [Typescript](https://typescriptlang.org) and type erasure of interfaces, dependency injection only supports `class`es as a type signifier. The primary goal of dependency injection is to allow for separation of concerns of object creation and it's usage.
## Declaration
The [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L16) and [@InjectableFactory](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L48) decorators provide the registration of dependencies. Dependency declaration revolves around exposing `class`es and subtypes thereof to provide necessary functionality. Additionally, the framework will utilize dependencies to satisfy contracts with various implementation.
**Code: Example Injectable**
```typescript
import { Injectable } from '/di';
()
class CustomService {
async coolOperation() {
// Do work!
}
}
```
When declaring a dependency, you can also provide a token to allow for multiple instances of the dependency to be defined. This can be used in many situations:
**Code: Example Injectable with multiple targets**
```typescript
import { Injectable, Inject } from '/di';
()
class CustomService {
async coolOperation() {
// do work!
}
}
const Custom2Symbol = Symbol.for('di-custom2');
({ target: CustomService, qualifier: Custom2Symbol })
class CustomService2 extends CustomService {
override async coolOperation() {
await super.coolOperation();
// Do some additional work
}
}
class Consumer {
(Custom2Symbol) // Pull in specific service
service: CustomService;
}
```
As you can see, the `target` field is also set, which indicates to the dependency registration process what `class` the injectable is compatible with. Additionally, when using `abstract` classes, the parent `class` is always considered as a valid candidate type.
**Code: Example Injectable with target via abstract class**
```typescript
import { Injectable } from '/di';
abstract class BaseService {
abstract work(): Promise<void>;
}
()
class SpecificService extends BaseService {
async work() {
// Do some additional work
}
}
```
In this scenario, `SpecificService` is a valid candidate for `BaseService` due to the abstract inheritance. Sometimes, you may want to provide a slight variation to a dependency without extending a class. To this end, the [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L48) decorator denotes a `static` class method that produces an [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L16).
**Code: Example InjectableFactory**
```typescript
import { InjectableFactory } from '/di';
// Not injectable by default
class CoolService {
}
class Config {
()
static initService() {
return new CoolService();
}
}
```
Given the `static` method `initService`, the function will be provided as a valid candidate for `CoolService`. Instead of calling the constructor of the type directly, this function will work as a factory for producing the injectable.
**Code: Example Conditional Dependency**
```typescript
import { Runtime } from '/runtime';
import { Inject, Injectable } from '/di';
({ enabled: Runtime.production })
class ProductionLogger {
async log() {
console.log('This will only run in production');
}
}
()
class RuntimeService {
()
logger?: ProductionLogger;
action(): void {
// Only injected when available, in production
this.logger?.log();
// Do work
}
}
```
In this example, the enabled flag is specified in relationship to the deployment environment. When coupled with optional properties, and optional chaining, allows for seamless inclusion of optional dependencies at runtime.
**Note**: Other modules are able to provide aliases to [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L16) that also provide additional functionality. For example, the [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support") module @Config or the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module @Controller decorator registers the associated class as an injectable element.
## Injection
Once all of your necessary dependencies are defined, now is the time to provide those [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L16) instances to your code. There are three primary methods for injection:
The [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L16) decorator, which denotes a desire to inject a value directly. These will be set post construction.
**Code: Example Injectable with dependencies as Inject fields**
```typescript
import { Injectable, Inject } from '/di';
import type { DependentService } from './dependency.ts';
()
class CustomService {
()
private dependentService: DependentService;
async coolOperation() {
await this.dependentService.doWork();
}
}
```
The [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L16) constructor params, which will be provided as the instance is being constructed.
**Code: Example Injectable with dependencies in constructor**
```typescript
import { Injectable } from '/di';
import type { DependentService } from './dependency.ts';
()
class CustomService {
dependentService: DependentService;
constructor(service: DependentService) {
this.dependentService = service;
}
async coolOperation() {
await this.dependentService.doWork();
}
}
```
Via [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L48) params, which are comparable to constructor params
**Code: Example InjectableFactory with parameters as dependencies**
```typescript
import { InjectableFactory } from '/di';
import { type DependentService, CustomService } from './dependency.ts';
class Config {
()
static initService(dependentService: DependentService) {
return new CustomService(dependentService);
}
}
```
### Multiple Candidates for the Same Type
If you are building modules for others to consume, often times it is possible to end up with multiple implementations for the same class.
**Code: Example Multiple Candidate Types**
```typescript
import { Injectable, Inject } from '/di';
export abstract class Contract {
}
()
class SimpleContract extends Contract { }
()
export class ComplexContract extends Contract { }
()
class ContractConsumer {
// Will default to SimpleContract if nothing else registered
()
contract: Contract;
}
```
By default, if there is only one candidate without qualification, then that candidate will be used. If multiple candidates are found, then the injection system will bail. To overcome this the end user will need to specify which candidate type should be considered `primary`:
**Code: Example Multiple Candidate Types**
```typescript
import { InjectableFactory } from '/di';
import type { Contract, ComplexContract } from './injectable-multiple-default.ts';
class Config {
// Complex will be marked as the available Contract
({ primary: true })
static getContract(complex: ComplexContract): Contract {
return complex;
}
}
```
## Non-Framework Dependencies
The module is built around the framework's management of class registration, and being able to decorate the code with [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L16) decorators. There may also be a desire to leverage external code and pull it into the dependency injection framework. This could easily be achieved using a wrapper class that is owned by the framework.
It is also possible to directly reference external types, and they will be converted into unique symbols. These symbols cannot be used manually, but can be leveraged using [](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L16) decorators.
**Code: Example External Dependencies**
```typescript
import { EventEmitter } from 'node:events';
import type { Writable } from 'node:stream';
import { Inject, Injectable, InjectableFactory } from '/di';
import { asFull } from '/runtime';
class Source {
()
static emitter(): EventEmitter {
return new EventEmitter();
}
(Symbol.for('custom-1'))
static writable(): Writable {
return asFull({});
}
(Symbol.for('custom-2'))
static writableAlt(): Writable {
return asFull({});
}
}
()
class Service {
()
emitter: EventEmitter;
(Symbol.for('custom-2'))
writable: Writable;
}
```
## Manual Invocation
Some times you will need to lookup a dependency dynamically, or you want to control the injection process at a more granular level. To achieve that you will need to directly access the [DependencyRegistryIndex](https://github.com/travetto/travetto/tree/main/module/di/src/registry/registry-index.ts#L19). The registry allows for requesting a dependency by class reference:
**Code: Example of Manual Lookup**
```typescript
import { Injectable, DependencyRegistryIndex } from '/di';
()
class Complex { }
class ManualLookup {
async invoke() {
const complex = await DependencyRegistryIndex.getInstance(Complex);
return complex;
}
}
```
Additionally, support for interfaces (over class inheritance) is provided, but requires binding the interface to a concrete class as the interface does not exist at runtime.
**Code: Example Interface Injection**
```typescript
import { DependencyRegistryIndex, Inject, Injectable, InjectableFactory } from '/di';
import { toConcrete } from '/runtime';
/**
* @concrete
*/
export interface ServiceContract {
deleteUser(userId: string): Promise<void>;
}
class MyCustomService implements ServiceContract {
async deleteUser(userId: string): Promise<void> {
// Do something
}
}
()
class SpecificService {
()
service: ServiceContract;
}
class ManualInvocationOfInterface {
()
static getCustomService(): Promise<ServiceContract> {
return DependencyRegistryIndex.getInstance(toConcrete<ServiceContract>());
}
}
```