DEV Community

Cover image for My DI container ✨ type-chef-di
Krisztián Maurer
Krisztián Maurer

Posted on

My DI container ✨ type-chef-di

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.

Image description

GitHub logo OpenZer0 / type-chef-di

General-purpose dependency injection framework (IoC)

DALL·E 2022-11-12 23 28 30 - Pizza with code bacground, cyberpunk style (1) (1)

type-chef-di npm version NPM Downloads

SonarCloud

Bugs Code Smells Duplicated Lines (%) Lines of Code Maintainability Rating Reliability Rating Security Rating Technical Debt Vulnerabilities

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"
}
Enter fullscreen mode Exit fullscreen mode

Install the npm package:

npm install type-chef-di
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 */


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Maurer Krisztián

Thank you for reading, ❤️ tell me your opinion in the comment section. 🧐

Top comments (0)