Typescript dependency injection for humans!
Reason
I wasn't happy with any found DI container both for 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 to go
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, perhaps in application bootstrap file
import {Container} from 'huject'
let container = new Container();
container.register() method
register(classDefinition: Function, constructorArguments?: Array<any>): Definition;
register(interfaceDefinition: Function, implementationDefinition: Function, constructorArguments?: Array<any>): Definition;
register(classDefinition: Function, object: Object): Definition;
register(symbolDefinition: string, classDefinition: Function, constructorArguments?: Array<any>): Definition;
register(symbolDefinition: string, Object: any): Definition;
Each function returns Definition object, which has following signature:
interface Definition {
as(method: FactoryMethod): Definition;
}
It may be used to override default factory method used when instantiating objects. By default all definitions have FACTORY method
Examples
container.register(MyClass);
container.register(MyInterfaceImplementation, ['param1', 'value1']);
container.register(MyInterface, MyInterfaceImplementation);
container.register(MyInterface, MyInterfaceImplementation, ['accesskey', 'accesstoken']);
let myService = new MyService();
container.register(MyServiceInterface, myService);
container.register(MyDBWrapper, ['host','username','password']);
container.register('db', MyDBWrapper);
container.register('db', MyDBWrapper, ['host','username','password']);
container.register('secretkey', 'qwerrty12345');
container.register('secretflag', true);
container.register('secreteoptions', { opt1: 'val1'});
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
registerCallable(classDefinition: Function, callable: () => Object|Function): Definition;
registerCallable(symbolDefinition: string, callable: () => Object|Function): Definition;
Examples
container.registerCallable(MyServiceInterface, () => {
return new MyServiceImplementation();
});
container.registerCallable('db', () => {
return new DBWrapper(container.get('host'), container.get('username'), ...);
);
container.resolve() method
Resolves definition. It will resolve any registered dependencies for instance too
resolve(definition: Function, method?: FactoryMethod): any;
resolve(definition: string, method?: FactoryMethod): any;
Examples
let implementation = container.resolve(MyInterfaceOrClass);
let implementation = container.resolve('db');
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
export const enum FactoryMethod {
SINGLETON,
FACTORY,
OBJECT
}
Examples
import {FactoryMethod} from 'huject';
container.register(MyClass).as(FactoryMethod.FACTORY);
let impl1 = container.resolve(MyClass);
let impl2 = container.resolve(MyClass);
(impl1 === impl2);
container.register(MyClass).as(FactoryMethod.SINGLETON);
let impl1 = container.resolve(MyClass);
let impl2 = container.resolve(MyClass);
(impl1 === impl2);
container.register(MyInterface, MyClass).as(FactoryMethod.SINGLETON);
container.register(MyClass).as(FactoryMethod.OBJECT);
let impl = container.resolve(MyClass);
(typeof impl === 'function');
let obj = new impl();
Decorators
You can specify dependencies by using decorators:
@Inject
@Inject(method: FactoryMethod);
@Inject(literal: string, method?: FactoryMethod)
@ConstructorInject
@ConstructorInject(method: FactoryMethod)
@ConstructorInject(literal: string, method?: FactoryMethod)
@Optional
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!
@ConstructorInject
class Test {
public constructor(service: MyService, param1: string, param2: number) {
....
}
}
But that will:
container.register(Test, ['string1',10]);
class Test {
@Inject
public service: MyService;
public constructor(param1: string, param2: number) {
...
}
}
Starting from version 1.1 you can use string literals for constructor injection too
container.register('token', 'qwerty12345');
container.register('seed', Math.random());
@ConstructorInject
class Test {
public constructor(service: MyService, @ConstructorInject('token') param1: string, @ConstructorInject('seed') param2: number) {
....
}
}
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:
import {Optional} from 'huject';
@ConstructorInject
class Test {
public constructor(
service: MyService,
@Optional @ConstructorInject('token') param1: string,
@Optional @ConstructorInject('seed') param2: number)
{
if (param1 !== null) {
....
}
....
}
}
This is very useful for property injection and default configuration:
import {Optional, Inject} from 'huject';
container.register('classToken', 'mytoken');
class Test {
@Optional
@Inject('classToken')
public classParam1: string = "default string";
@Optional
@Inject('servicePort')
public port: number = 80;
}
Here classParam1 will be replaced with 'classToken' but port will contain original value
Examples:
import {Inject, ConstructorInject, FactoryMethod} from 'huject';
@ConstructorInject
class TestController1 {
private service1: OneService;
private service2: SecondService;
@Inject
public service3: ThirdService;
@Inject(FactoryMethod.SINGLETON)
public service4; QuatroService;
public constructor(service1: OneService, service2: SecondService) {
this.service1 = service1;
this.service2 = service2;
}
}
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:
@ConstructorInject
class TestController2 {
private service1: OneService;
private service2: SecondService;
private secret: string;
@Inject(FactoryMethod.SINGLETON)
public service3: ThirdService
@Inject('db', FactoryMethod.SINGLETON)
public db: DbWrapper;
@Inject('controllerToken')
public controllerToken: string;
public constructor(
service1: OneService,
@ConstructorInject(FactoryMethod.SINGLETON) service2: SecondService,
@ConstructorInject('secretkey') secret: string
) {
this.service1 = service1;
this.service2 = service2;
this.secret = secret;
}
}
The @Inject(FactoryMethod) syntax is used to override factory method for inject property. These objects will be equal:
@Inject(FactoryMethod.SINGLETON)
public service5: FiveService;
@Inject(FactoryMethod.SINGLETON)
public service6: FiveService;
but these will be not:
@Inject(FactoryMethod.FACTORY)
public service5: FiveService;
@Inject(FactoryMethod.FACTORY)
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.
import {OneService} from 'FirstService';
import {SecondService} from 'SecondService';
container.register(OneService);
container.register(SecondService, ['param1', 'param2', true]);
You can change this behavior by setting container.setAllowUnregisteredResolving(true) so you don't need to do simple container.register(class) registration:
import {OneService} from 'FirstService';
import {SecondService} from 'SecondService';
class Controller {
....
@Inject
public service1: OneService;
@Inject
public service2: SecondService;
...
}
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)
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:
interface ServiceInterface {
public method1(): void;
public method2(num: number): string;
}
class MyService implements ServiceInterface {
public method1(): void {
...
}
public method2(num: number): string {
...
}
}
class MyController {
@Inject
public service: ServiceInterface
}
container.register(ServiceInterface, MyService);
container.register(MyController);
let controller = container.resolve(MyController);
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:
class ServiceInterface {
public method1(): void {};
public method2(num: number): string {};
}
class MyService implements ServiceInterface {
public method1(): void {
...
}
public method2(num: number): string {
...
}
}
class MyController {
@Inject
public service: ServiceInterface
}
container.register(ServiceInterface, MyService);
container.register(MyController);
let controller = container.resolve(MyController);
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:
class ServiceInterface {
public method1(): void {};
public method2(num: number): string {};
}
class MyService implements ServiceInterface {
public method1(): void {
...
}
public method2(num: number): string {
...
}
}
class MyController {
@Inject
public service: ServiceInterface
}
container.setAllowUnregisteredResolving(true);
container.register(MyController);
let controller = container.resolve(MyController);
That's why i explicitly enabled strong container registration. Without container.setAllowUnregisteredResolving(true) the
let controller = container.resolve(MyController);
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 omit 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:
abstract class ServiceInterface {
public abstract method1(): void;
}
@ConstructorInject
class Controller {
private service: ServiceInterface;
public constructor(service: ServiceInterface) {
this.service = service;
}
public test(): void {
this.service.method1();
}
}
let myMock = sinon.createStubInstance(ServiceInterface);
let controller = new Controller(myMock);
controller.test();
The compiler will omit method1() from compiled JS file if method was declared as abstract and your stub will not have correct method
abstract class ServiceInterface {
public method1(): void {};
}
@ConstructorInject
class Controller {
private service: ServiceInterface;
public constructor(service: ServiceInterface) {
this.service = service;
}
public test(): void {
this.service.method1();
}
}
let myMock = sinon.createStubInstance(ServiceInterface);
let controller = new Controller(myMock);
controller.test();
myMock.method1.should.have.been.called;
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-cli
npm install
grunt
node example/main.js
Tests
To run tests type:
grunt test