Usage
Creating an Injection Container
An InjectionContainer
is an encapsulated DI environment where Injectables can be stored and retrieved.
Each container is initialized with it's @Injectable
class instances. Objects registered
to one container cannot be accessed by classes in another container.
const container = new InjectionContainer();
tip
A container will automatically initialize (instantiate all classes) when created unless the isManualInit
option is specified when creating the instance.
Defining an injectable
Classes
Use the @Injectable
annotation to let the framework know that you intend for this class to be handled
by the injection context. This means that it can be injected into other classes and that any injectable
classes provided as newable arguments are automatically injected.
@Injectable()
class ArnyService {
public getQuote(): string {
const quotes = [
"Hasta la vista, baby!",
"If it bleeds, we can kill it.",
"Come with me if you want to live.",
];
return getRandomItem(quotes);
}
}
Injectable objects
Some use cases may require manually registering objects or classes in the DI container.
This can be achieved using the register
method. An InjectableItem
is returned upon
successfully registering an injectable, including a unique token that is used to as it's
unique identifier when injecting.
const container = new InjectionContainer({
isManualInit: true,
});
const config: Config = {
isFeatureEnabled: true,
};
const { token: configToken } = container.register(config).successOrThrow();
warning
If you intend to register injectables manually, make sure to manually initialize your container, otherwise they may not be available in classes during instantiation.
Injecting dependencies
🌾 Field Injection
The ArnyService
class from the above example can be injected into the ArnyQuoteApp
class using field injection as per the snippet below. Manually registered injectables
can also be injected by supplying their tokens.
@Injectable
export class ArnyQuoteApp {
@Autowire(ArnyService)
private service: ArnyService;
@Autowire(configToken)
private config: ConfigObject;
public tryGetQuote(): string | void {
if (this.config.isFeatureEnabled) {
return this.service.getQuote();
}
}
}
🔨 Constructor Injection
The example above can also be achieved using constructor injection, which is the only injection type which allows access to the injectables inside the class constructor.
@Injectable
export class ArnyQuoteApp {
private readonly isEnabled: boolean;
constructor(
@Autowire(configToken)
private config: ConfigObject,
@Autowire(ArnyService)
private service: ArnyService
) {
this.isEnabled = config.isFeatureEnabled;
}
public tryGetQuote(): string | void {
if (this.isEnabled) {
return this.service.getQuote();
}
}
}
tip
The @Autowire
decorator isn't strictly required for classes that have been registered with
the @Injectable
decorator. The injection framework is smart enough to infer what should be
injected by adding the class as a parameter.
Resolving a class
Use the resolve
method on an InjectionContainer
to get an instantiated version of your
injectable class.
const container = new InjectionContainer();
const app = container.resolve(ArnyQuoteApp);
app.tryGetQuote().then((quote) => console.log(quote));
Environment variables
Environment variables can be injected into class members using the @Env
annotation.
The framework will use the type
of the class member
to infer how to parse the value.
Supported types are: string
, boolean
, object
, number
.
Mapping functions can be optionally provided that take the string value as an argument and
return the mapped value.
process.env.CFG_STR = "test";
process.env.CFG_NUM = "123";
process.env.CFG_BOOL = "true";
process.env.CFG_OBJ = '{"myObj": "hello"}';
type MyObj = { myObj: string };
function mapObj(val: string): MyObj {
const obj = JSON.parse(val);
obj.newVal = 123;
return obj;
}
export class App {
@Env("CFG_STR")
private myString: string;
@Env("CFG_NUM")
private myNumber: number;
@Env("CFG_BOOL")
private myBool: boolean;
@Env("CFG_OBJ")
private myObj: MyObj;
@Env<MyObj>("CFG_OBJ", mapObj)
private myMappedObj: MyObj;
newable() {
console.log(this.myString);
// "test"
console.log(this.myNumber);
// 123
console.log(this.myBool);
// true
console.log(this.myObj);
// {
// myObj: "hello"
// }
console.log(this.myMappedObj);
// {
// myObj: "hello",
// newVal: 123
// }
}
}
Circular dependencies
In order to resolve dependencies, the framework will traverse the dependency tree until it finds a class it can instantiate. If circular dependencies are detected (like in the scenario below) the framework won't be able to traverse the tree correctly; this will result in an error.
@Injectable
export class ServiceOne {
constructor(private service: ServiceTwo) {}
}
@Injectable
export class ServiceTwo {
constructor(private service: ServiceOne) {}
}
In most cases, circular dependencies are indicative of a flaw in application design. The intention of the above code be accomplished by creating a higher order class that contains both dependencies instead.
@Injectable
export class HigherOrderService {
constructor(private service: ServiceOne, private serviceTwo: ServiceTwo) {}
}