@jacekpietal/dependency-injection
    TypeScript icon, indicating that this package has built-in type declarations

    1.4.0 • Public • Published

    TypeScript dependency injection library

    Build Status

    This TypeScript library allows you to easily declare and resolve dependencies, injecting them in your classes attributes, using eye-candy TypeScript annotations.

    (Shortcut to "Getting started" section)

    Usage example

    import { Injectable, Inject } from '@jacekpietal/dependency-injection'
    
    @Injectable
    class TestService {
      foo: string = 'bar'
    }
    
    class TestComponent {
      @Inject(TestService) service: TestService
    
      baz: string
    
      constructor() {
        this.baz = this.service.foo // 'bar'
      }
    }

    Requirements

    • TypeScript compiler 1.5 or higher
    • EcmaScript5-compliant engine (nodejs versions >= .0.10 will do fine)

    Who is it for?

    This library is primarily aimed at framework developers but any programmer that want clean and concise code will surely enjoy it too! It's a great way to reduce redundant boilerplate code.

    Side-note about the terminology: The official term of the @Something syntax in TypeScript is "decorator", but I might inadvertently call it "annotation" quite frequently.

    Features

    • Powerful. Resolves dependencies by prototype and/or name.
    • Concise. Using TypeScript annotations will be a real pleasure for your eyes. I promise.
    • Expressive. By declaring multiple contexts, you have fine control of the resolution process.
    • Safe. The solver automatically detects ambiguous contexts and prevent unexpected behaviors.
    • Forgiving. Even if you forget an annotation (eg. @DirectLoad), the framework will warn you and find a way around to make things work.

    umh... dependency injection?

    Dependency injection allows you to reduce coupling by dynamically setting ("injecting") variables where they need to be.

    For example, let's say I'm writing some controllers and I want to be able to send emails from them. I can write an EmailService class and provide dynamically its instance to any controller that requests it. Dependency injection will allow me to have a clean and unified syntax for both requesting and providing the EmailService (see below for an example).

    Getting started

    First you have to install the library:

    $ yarn add @jacekpietal/dependency-injection -D

    Then import the library in your TypeScript code using:

    import DI from '@jacekpietal/dependency-injection'

    Manual context resolution

    Then, you can declare a dependency using the following annotation:

    class MyClass {
      @DI.Injection(MyDependency)
      public dep: MyDependency
    }

    Here, you are declaring that instances of MyClass needs an instance of MyDependency to work properly.

    To provide an instance of the dependency to an instance of MyClass, you have to create a dependency context. A context is a way to explicitly define which values are available during the resolution.

    All you have to do is add all the values that participates in the context and run the resolution. To create the context and resolve the dependencies:

    // Instantiate everything that has to
    const dep = new MyDependency()
    const instance = new MyClass()
    const context = new DI.Context()
    
    // Provide the values to the context
    context.addValue(dep)
    context.addValue(instance)
    
    // Resolve all the dependencies
    context.resolve()

    The dependency matching is performed here on the prototypes, but it can also be performed on names.

    Named dependencies

    You have the ability to give names to dependencies to avoid collisions. You have to use another annotation, NamedInjection:

    class MyClass {
      @DI.NamedInjection("some_name", MyDependency)
      private attr: MyDependency
    }

    Then, you can add the values to the context by specifying their name:

    context.addNamedValue(new MyDependency(), "some_name")
    // or an equivalent syntax:
    context.addValue(new MyDependency(), "some_name")
    class MyClass {
      @DI.NamedInjection("my dep", MyDependency)
      public dep: MyDependency
    }
    
    // [...] later in the code:
    context.addValue(dep, "my dep")
    context.addValue(instance, "an instance")

    Inheritance

    Of course, the resolution support inheritance in the dependencies.

    Example

    class Dep2 extends MyDependency {
      // empty class
    }

    Dep2 instances will be successfully matched as a MyDependency during the resolution.

    Injecting primitives

    You can inject primitive types by name the same way you do with class instances. The only thing you have to do is adding them to the context:

    context.addValue(1, "attr1")        // number
    context.addValue("message", "attr2")    // string
    context.addValue(true, "attr3")      // boolean
    context.addValue(function() {        // function
      console.log("Hello, I was injected !")
    }, "attr4")

    Note: As primitive types do not have a prototype, there is currently no way of directly specifying its type in the annotation. I'm currently working on a solution using strings parameters (like this: @DI.NamedInjection("attr1", "number")) but this is experimental.

    Automatic injection for singletons

    If you have some singletons classes, you may want to expose them at various places in your code. This library allows you to automatically instantiate and inject singleton without having anything to do except annotating your class!

    First, declare your singleton:

    @DI.Injectable
    class MyInjectable {
      public singletonMethod(): void {
        console.log("Hello!")
      }
    }

    Then, request it:

    class MyClass {
      @DI.Inject(MyInjectable)
      public attr: MyInjectable
    }

    And that's it! The singleton is available on every instance of MyClass:

    const a = new MyClass()
    a.attr.singletonMethod()  // prints "Hello!" in the console

    Strict resolution

    By default, when you call context.resolve(), if a dependency is not found in the context, nothing happens and the class attribute is undefined (or whatever default value you provided). You may want to ensure that all the dependencies were met. To do so, you can use context.resolveStrict() or context.resolve(true). The injection system will throw an exception if something's missing.

    Ambiguous context

    It might happen that when resolving a context, you get an error saying that the context is ambiguous. It means that there are many possible values for a single injection request, and the injection system can't guess which one has to be used.

    There are two probable causes of ambiguous context error :

    • the context contains multiple instances of the same class (or of some inherited classes) that are not named
    • the context contains multiple instances of the same class with the same name

    Self-injection & same-name injection requests

    The injection system will prevent an instance from injecting into itself ("why ?"). The benefit of this is that it will allow you to have two instances with the same name and same type in the same context, to make them cross-inject into one another,

    class SelfInjectingClass {
      @DI.NamedInjection("a_friend", SelfInjectingClass)
      public dep: SelfInjectingClass
    }
    
    const self1 = new SelfInjectingClass()
    const self2 = new SelfInjectingClass()
    
    context.addValue(self1, "a_friend")
    context.addValue(self2, "a_friend")
    context.resolve()   // no error! :)

    Note. It is also possible to use an non-named injection annotation in the class declaration:

    @DI.Injection(SelfInjectingClass)
    public dep: SelfInjectingClass

    todo list

    • named dependencies
    • strict context resolution (optional)
    • unit tests
    • fix log4js dependency blocking webpack usage
    • singleton dependency magic injection
    • primitive injection by type declaration ("number"/"string"/"boolean")
    • context extension : be able to "copy" a context, and add values into this "child" context, without reaffecting
    • values from the parent context. Example: server context -> match context -> player context
    • Documentation : example of Annotation wrapping for framework developpers.
    • register user-created singletons

    Install

    npm i @jacekpietal/dependency-injection

    DownloadsWeekly Downloads

    7

    Version

    1.4.0

    License

    none

    Unpacked Size

    89 kB

    Total Files

    71

    Last publish

    Collaborators

    • jacekpietal