I would like to learn more about how frameworks and DI containers work, so I wrote one myself from scratch. In this article I will show you how it works.
OpenZer0 / type-chef-di
General-purpose dependency injection framework (IoC)
Type-chef-di is a general purpose dependency injection framework, focused on simplicity and extendability.
Documentation: https://zer0-2.gitbook.io/type-chef-di
Setup:
tsconfig.json:
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "es6"
}
Install the npm package:
npm install type-chef-di
https://www.npmjs.com/package/type-chef-di
One of the feature that may be interesting for you is the Type resolution. essentially, you can resolve types without registering them to the DI. The DI container will try to resolve by looking the constructor param types recuresively.
import { Container, Injectable } from "type-chef-di";
@Injectable()
class SayService {
public getString() {
return "pizza";
}
}
@Injectable()
class SayService2 {
public getString() {
return "coffee";
}
}
@Injectable()
class Client {
constructor(private readonly sayService: SayService,
private readonly sayService2: SayService2) {
}
public say()
…https://www.npmjs.com/package/type-chef-di
Type-chef-di is a general purpose dependency injection framework. I tried to focus on simplicity and extendability.
One of the feature that may be interesting for you is the Type resolution. essentially, you can resolve types without registering them to the DI. The DI container will try to resolve by looking the constructor param types recuresively.
import { Container, Injectable } from "type-chef-di";
@Injectable()
class SayService {
public getString() {
return "pizza";
}
}
@Injectable()
class SayService2 {
public getString() {
return "coffee";
}
}
@Injectable()
class Client {
constructor(private readonly sayService: SayService,
private readonly sayService2: SayService2) {
}
public say() {
return `I like ${this.sayService.getString()} and ${this.sayService2.getString()}`;
}
}
@Injectable({instantiation: "singleton"})
class Service {
constructor(private readonly client: Client) {
}
public check() {
return `client says: ${this.client.say()}`;
}
}
async function run() {
const container = new Container({enableAutoCreate: true});
const service = await container.resolveByType<Service>(Service); // new Service(new Client(new SayService(), new SayService2()));
console.log(service.check()); // client says: I like pizza and coffee
}
run();
You can choose the instantiation mode: singleton / new instance.
but if you want to use interfaces, you can do so with this automatic resolution just use the @Inject decorator with the type.
constructor(@Inject<IOptions>(OptionClass) options: IOptions,
@Inject<IOptions>(OptionClass2) options2: IOptions) {}
Registration process can be manual or automatic.
Manually eg container.register("key", value), .registerTypes([Service, FoodFactory])
then you can inject the registered key into the constructor with the Inject('key') decorator.
class Service {
constructor(@Inject("serviceStr") private readonly value: string) {
}
public say() {
return `${this.value}`;
}
}
class Client {
constructor(
@Inject("clientStr") private readonly value: string,
@Inject("service") private readonly service: Service // or @Inject<IService>(Service)
) {
}
public say() {
return `I like ${this.value} and ${this.service.say()}`;
}
}
async function run() {
const container = new Container();
container.register("clientStr", "coffee").asConstant();
container.register("serviceStr", "pizza").asConstant();
container.register("service", Service).asPrototype();
container.register("client", Client).asSingleton();
const service = await container.resolve<Client>("client"); // new Service('pizza');
const service2 = await container.resolveByType<Client>(Client); // new Client('coffee', new Service('pizza'));
console.log(service.say()); // client says: I like pizza and coffee
console.log(service2.say()); // client says: I like pizza and coffee
}
run();
If you want more control over the injection process you can use the token injection. This lets you inject the value that you registered.
The DI can't resolve automatically the primitive types / interfaces: eg. string, number, interfaces... You must specify the value and use the @Inject decorator for that
constructor(service: Service,
@Inject('options') options: IOptions)
constructor(service: Service,
@Inject<IOptions>(OptionClass) options: IOptions,
@Inject<IOptions>(OptionClass2) options2: IOptions)
Explanation:
service: Service
: if {enableAutoCreate: true} you don't have to do anything it will register and resolve automatically. if false you need to register before resolution eg container.registerByType(Service) but you can inject it with @Inject if you want.
@Inject('options') options: IOptions
- this cannot be resolved automatically because this is just a general interface (IOptions), you need to specify (by registering) a token eg 'option' and inject via @Inject("key")
@Inject(OptionClass) options: IOptions, @Inject<IOptions>(OptionClass2) options2: IOptions)
- You can directly specify the class that you want to inject, this way you don't need to register the OptionClass (the generic will check the passed type correctness)
If the key is not registered, the resolution process will fail.
You can check the container after you finished the configuration:
container.done()
This will try to resolve all the registered keys, and types.
After instatniation you can also run Initializers eg. MethodWrapper, RunBefore, InitMethod erc. or you can easily create your own.
export class MeasureWrapper implements IMethodWrapper {
constructor() { // DI will resolve dependencies (type & key injection)
}
async run(next: Function, params: any[]) {
// run code before
const start = new Date().getTime();
// call original fn
const res = await next() // (params automatically added)
//run code after
const end = new Date().getTime();
const time = end - start;
console.log(`Execution time: ${time} ms`)
// return fn result
return res;
}
}
class Test {
@MeasureWrapper(MeasureWrapper) // or use registerd string key
foo(p1:string, p2: string){
console.log("original fn: ", p1, p2)
// ...
}
}
/* After Test.foo is called
it will log the `Execution time: ${time} ms` because of the @MeasureWrapper */
There are a few more features:
@RunBefore(key: string | Type<IRunBefore>) // run before method call
@RunAfter(key: string | Type<IRunAfter>) // run after method call
@AddTags(tags) // resolve tagged classes
@InitMethod() // run init fuction after instantiation
@InjectProperty<T>(key: string | Type<T>) // @Inject just for class props
I tried to keep the article short, if you are interested, check the documentation https://zer0-2.gitbook.io/type-chef-di/
There are still things to improve and document, you can help,
if you would like to improve the documentation, click on the "edit on GitHub" button and make a pull request.
Thank you for reading, ❤️ tell me your opinion in the comment section. 🧐
Top comments (0)