Skip to main content

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) {}
}