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.
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.
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()classSayService{publicgetString(){return"pizza";}}@Injectable()classSayService2{publicgetString(){return"coffee";}}@Injectable()classClient{constructor(privatereadonlysayService:SayService,privatereadonlysayService2:SayService2){}publicsay(){return`I like ${this.sayService.getString()} and ${this.sayService2.getString()}`;}}@Injectable({instantiation:"singleton"})classService{constructor(privatereadonlyclient:Client){}publiccheck(){return`client says: ${this.client.say()}`;}}asyncfunctionrun(){constcontainer=newContainer({enableAutoCreate:true});constservice=awaitcontainer.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.
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.
classService{constructor(@Inject("serviceStr")privatereadonlyvalue:string){}publicsay(){return`${this.value}`;}}classClient{constructor(@Inject("clientStr")privatereadonlyvalue:string,@Inject("service")privatereadonlyservice:Service// or @Inject<IService>(Service)){}publicsay(){return`I like ${this.value} and ${this.service.say()}`;}}asyncfunctionrun(){constcontainer=newContainer();container.register("clientStr","coffee").asConstant();container.register("serviceStr","pizza").asConstant();container.register("service",Service).asPrototype();container.register("client",Client).asSingleton();constservice=awaitcontainer.resolve<Client>("client");// new Service('pizza');constservice2=awaitcontainer.resolveByType<Client>(Client);// new Client('coffee', new Service('pizza'));console.log(service.say());// client says: I like pizza and coffeeconsole.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
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.
exportclassMeasureWrapperimplementsIMethodWrapper{constructor(){// DI will resolve dependencies (type & key injection)}asyncrun(next:Function,params:any[]){// run code beforeconststart=newDate().getTime();// call original fnconstres=awaitnext()// (params automatically added)//run code afterconstend=newDate().getTime();consttime=end-start;console.log(`Execution time: ${time} ms`)// return fn resultreturnres;}}classTest{@MeasureWrapper(MeasureWrapper)// or use registerd string keyfoo(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
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. 🧐