Typescript dependency injection for humans!
Reason
I wasn't happy with any DI container found for both Typescript and Javascript. Each was missing some required feature: one had construction injection autowiring but didn't have property injection and ability to pass ordinary constructor parameters when instantiating, other had an ability to register or pass constructor params but didn't have typescript features, etc...
Features
This DI container supports:
- Constructor injection
- Property injection
- Callable injection
- Service locator pattern (register and resolve primitive, object, class or callable by string definition)
- Property and constructor dependencies autowiring
- Simple API, just 3 methods and few decorators to go
- Optional injection (from version 1.2)
- Injecting container as factory (from 1.3 version)
- Auto-Creating object factories! (from 1.3 version)
Todo
- Lazy injection
Installation
npm install huject --save
Install typescript definition
tsd link # will update your tsd.d.ts file
or directly include
///
API
This library is intended to use only with typescript 1.5+ and --emitDecoratorMetadata flag enabled. Do not use it with just Javascript
Initialization
To use the the library you need to create new Container object. Do it in one place, probably in application bootstrap file
;
container.register() method
/** * Register class definition * @param classDefinition Class definition * @param constructorArguments Optional array of constructor arguments. They will be passed to constructor when object will be instantiated */registerclassDefinition: Function, constructorArguments?: Array<any>: Definition;/** * Bind class to another class (interface) * @param interfaceDefinition An interface to bind to * @param implementationDefinition Class definition * @param constructorArguments Optional array of constructor arguments */registerinterfaceDefinition: Function, implementationDefinition: Function, constructorArguments?: Array<any>: Definition;/** * Bind pre-created object to class definition. The object will be used when defined class is instantiated * @param classDefinition Class definition * @param object Object */registerclassDefinition: Function, object: Object: Definition;/** * Bind class definition to string definition. Object could be later instantiated by resolve('symbol'); * @param symbolDefinition String * @param classDefinition Class definition * @param constructorArguments Optional array of constructor arguments */registersymbolDefinition: string, classDefinition: Function, constructorArguments?: Array<any>: Definition;/** * Bind object to string definition * @param symbolDefinition String * @param Object Object */registersymbolDefinition: string, Object: any: Definition;
Each function returns Definition object, which has following signature:
It may be used to override default factory method used when instantiating objects. By default all definitions have FACTORY method
Examples
// Just register class in containercontainer.registerMyClass; // Register constructor arguments with class. Useful when instantiating external library objectscontainer.registerMyInterfaceImplementation, ; // Register class to interface. Constructor arguments from previous registration will be passed when resolving MyInterface as wellcontainer.registerMyInterface, MyInterfaceImplementation; // Register interface to implementation with constructor arguments. Arguments will overwrite previous arguments registration for MyInterfaceImplementationcontainer.registerMyInterface, MyInterfaceImplementation, ; // register interface (or class) with precreated object;container.registerMyServiceInterface, myService; // Assign class to string definition (service locator pattern)container.registerMyDBWrapper, ;container.register'db', MyDBWrapper; // Assign class with constructor arguments to string definition// same as previous declarationcontainer.register'db', MyDBWrapper, ; // Assign any value to string definitioncontainer.register'secretkey', 'qwerrty12345';container.register'secretflag', true;container.register'secreteoptions', ;
container.registerCallable() method
Register callable with class or string definition. The main difference with just register() is that the container doesn't try to instantiate callable via new and returns callable value as resolve value
/** * Bind callable function to class definition. Instead creating new object the function result will be used instead * @param classDefinition Class definition * @param callable Callable */registerCallableclassDefinition: Function, callable:Object|Function: Definition;/** * Bind callable function to string definition. Instead creating new object the function result will be used instead * @param symbolDefinition String definition * @param callable Callable */registerCallablesymbolDefinition: string, callable:Object|Function: Definition;
Examples
// Register implementation to interfacecontainer.registerCallableMyServiceInterface,;// Assign class to string definitioncontainer.registerCallable'db',
container.resolve() method
Resolves definition. It will resolve any registered dependencies for instance too
/** * Resolve (instantiate) object from container. Will resolve all wired dependencies if they were specified by decorators * @param definition Class definition * @param method Factory method. Used to override definition method only for this instantiation */resolvedefinition: Function, method?: FactoryMethod: any;/** * Resolve * @param definition Class definition * @param method Factory method. Used to override definition method only for this instantiation */resolvedefinition: string, method?: FactoryMethod: any;
Examples
;;
FactoryMethod enum
By default resolve() resolves new instance each time as called. By setting FactoryMethod either in register() or resolve() you can override this behavior
/** * Used to specify instantiate method */
Examples
; container.registerMyClass.asFactoryMethod.FACTORY;;; impl1 === impl2; // false container.registerMyClass.asFactoryMethod.SINGLETON;;; impl1 === impl2; // true // Register MyClass as implementation for MyInterface with Singleton factorycontainer.registerMyInterface, MyClass.asFactoryMethod.SINGLETON; container.registerMyClass.asFactoryMethod.OBJECT;; // Original constructor functiontypeof impl === 'function'; //true;
Decorators
You can specify dependencies by using decorators:
// Use at top of class property. Resolves by using factory method in definition or default (FACTORY) one// Use at top of class property. Same as @Inject but specifies factory method to resolve// Use at top of class property. Resolves by string definition. Optional pass the factory method// Use at top of class definition. Takes dependencies from constructor arguments// Use before constructor argument. Override factory method for single argument// Use before constructor argument. Resolves argument by string definition. Optionally takes the factory method// Use before class property/constructor argument with either @Inject or @ConstructorInject. Specified optional (non-strict) resolution// If dependency wasn't found then leave original value (for property injection) or pass null (for constructor injection)// Used for creating auto factories (See below)
Note: @Inject() and @Inject are not same
Important: You can combine both property and constructor style injection, but do not use ordinary constructor arguments (without @ConstructorInject('literal') when using constructor injection.
That will not work!
But that will:
container.registerTest, ;
Starting from version 1.1 you can use string literals for constructor injection too
container.register'token', 'qwerty12345';container.register'seed', Math.random;
Starting from version 1.2 you can specify @Optional dependencies. That means if dependency wasn't found then don't throw an error and pass null or leave default value instead:
;
This is very useful for property injection and default configuration:
; container.register'classToken', 'mytoken';
Here classParam1 will be replaced with 'classToken' but port will contain original value
Examples:
;
Here the service1 and service2 are being resolved by constructor injection and service3 and service4 are resolved by property injection. You must have a public property to do property injection.
Another a slight complex example:
The @Inject(FactoryMethod) syntax is used to override factory method for inject property. These objects will be equal:
public service5: FiveService;public service6: FiveService;
but these will be not:
public service5: FiveService;public service6: FiveService;
Also the service classes should be registered in container first. If any constructor params or implementation bindings were bound to these service, they will be applied automatically.
; ; container.registerOneService; container.registerSecondService, ;
You can change this behavior by setting container.setAllowUnregisteredResolving(true) so you don't need to do simple container.register(class) registration:
;;
but you need to have a reference to constructor functions anyway, so i'd recommend avoid to depend on services directly and use interfaces (or class-like analogs)
Factories
I'm pleased to introduce to you new feature starting from version 1.3: Auto-Factory and ContainerFactoryInterface
ContainerFactoryInterface
You can now inject ContainerFactoryInterface into your service/controller and create object from container dynamically, by request:
;;
No need to pre-register ContainerFactoryInterface (unless you want to redefine it). Also objects created by this method will always have FACTORY scope (and it will return new object at each call). The object will be resolved by container so it will get all benefits from dependencies autowiring. You can also pass constructor arguments to make()
Interface is simple:
Auto-Factories
By using decorators and typehints you can tell container to create factory for you:
// class name doesn't matter, don't forget to specify @Factory for both methods and class! // You can extend factories // Just inject factory to controller. No pre-registration needed
The return type annotation is required. Also it should be only constructor function. For others types it will throw an error. The return type will be resolved by container, so autowiring is possible (probably better to use property injection for that). You can also pass constructor arguments - just define them in factory and pass when calling factory method.
No need to pre-register neither factory or classes used to create instances by factories in container.
Note: It'a bad practice generally to inject something into business models.
Typescript interfaces and implementation->interface binding
In typescript the interfaces are not a real objects in javascript realtime. I'd suggest you initially were going to write something like it:
container.registerServiceInterface, MyService;container.registerMyController; ; //error
but you can't. There is no enough runtime information for interfaces so ServiceInterface will be just empty Object and resolve lookup will fail.
Here you can have only one way to workaround this problem:
Use class or (abstract class) as interface
You can write class instead interface:
// Just use class or abstract class keywords container.registerServiceInterface, MyService; //Okcontainer.registerMyController; ; //OK
Nothing wrong here since interface is a shape, but class is a shape too. One problem you need to watch for you 'classed' interfaces and avoid creation these interfaces at runtime:
// allow to resolve unregistered definitionscontainer.setAllowUnregisteredResolvingtrue;container.registerMyController; // forgot to bind ServiceInterface to MyService here ; // Ok, but controller.service will have an empty ServiceInterface object instead correctly MyService object
That's why i explicitly enabled strong container registration. Without container.setAllowUnregisteredResolving(true) the
;
would give you 'Undefined ServiceInterface error';
Note: Ordinary or abstract class doesn't matter from runtime perspective. As of version 1.6.0-beta typescript compiler doesn't emit any runtime checks to avoid creation abstract classes at runtime. That could be changed later though.
Note: Any abstract methods will be omitted when compiling to JS. I'd suggest you to use empty function body {} and avoid use abstract method(), if you're using abstract classes as interfaces but the choice is up to you. That doesn't impact any container functionality but impacts testing:
// in test.ts ;;controller.test; // Error
The compiler will omit method1() from compiled JS file if method was declared as abstract and your stub will not have correct method
// in test.ts ;;controller.test; // OK since there was method1() emitted by compiler for service interface prototypemyMock.method1.should.have.been.called; // OK
This may looks weird though, so it's up to you which method to use. As i said it didn't affect any container functionality but might be useful for creating testing mocks/stubs.
Example
In example/ directory you can see completely working DI example. To build you need to run grunt first
npm install -g grunt-clinpm installgruntnode example/main.js
Tests
To run tests type:
grunt test