- Notifications
You must be signed in to change notification settings - Fork68
A Lightweight annotation-based dependency injection container for typescript.
License
thiagobustamante/typescript-ioc
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
This is a lightweight annotation-based dependency injection container for typescript.
It can be used on browser, on react native or on node.js server code.
The documentation for the previous version can be foundhere
Table of Contents
- IoC Container for Typescript
This library only works with typescript. Ensure it is installed:
npm install typescript -g
To install typescript-ioc:
npm install typescript-ioc
Typescript-ioc requires the following TypeScript compilation options in your tsconfig.json file:
{"compilerOptions":{"experimentalDecorators":true,"emitDecoratorMetadata":true,"target":"es6"// or anything newer like esnext}}
import{Inject}from"typescript-ioc";classPersonDAO{ @InjectrestProxy:PersonRestProxy;}
That's it. You can just call now:
letpersonDAO:PersonDAO=newPersonDAO();
And the dependencies will be resolved.
You can also inject constructor parameters, like:
classPersonService{privatepersonDAO:PersonDAO;constructor( @InjectpersonDAO:PersonDAO){this.personDAO=personDAO;}}
and then, if you make an injection to this class, like...
classPersonController{ @InjectprivatepersonService:PersonService;}
The container will create an instance of PersonService that receives the PersonDAO from the container on its constructor.But you can still call:
letpersonService:PersonService=newPersonService(myPersonDAO);
And pass your own instance of PersonDAO to PersonService.
Note that any type with a constructor can be injected.
classPersonController{ @InjectprivatepersonService:PersonService; @InjectcreationTime:Date;}
You don't have to do anything special to work with sub-types.
abstractclassBaseDAO{ @InjectcreationTime:Date;}classPersonDAOextendsBaseDAO{ @InjectprivatepersonRestProxy:PersonRestProxy;}classProgrammerDAOextendsPersonDAO{ @InjectprivateprogrammerRestProxy:PersonRestProxy;}
The above example will work as expected.
You can use scopes to manage your instances. We have three pre defined scopes (Scope.Singleton,Scope.Request andScope.Local), but you can define your own custom Scope.
Allow just one instance for each type bound to this scope.
@SingletonclassPersonService{ @InjectprivatepersonDAO:PersonDAO;}classPersonController{ @InjectprivatepersonService:PersonService; @InjectcreationTime:Date;}
So, we can create a lot of PersonController instances, but all of them will share the same singleton instance of PersonService
letcontroller1:PersonController=newPersonController();letcontroller2:PersonController=newPersonController();
Types bound to this scope will share instances between the same build context. When you callContainer.get, a new build context is created and every container resolution performed will share this context.
For example:
@InRequestScopeclassRequestScopeClass{}classFirstClass{ @Injectpublica:RequestScopeClass;}classSecondClass{ @Injectpublica:RequestScopeClass; @Injectpublicb:FirstClass;}
In that example, we can expect:
constsecondClass=Container.get(SecondClass);expect(secondClass.a).toEqual(secondClass.b.a);
The container will create a new instance every time it will be asked to retrieve objects for types bound to the Local scope.
The Local scope is the default scope. So you don't need to configure nothing to work with a Local scope. However if you have a Type bound to other scope and want to change it to the Local scope, you can use theScope.Local property:
@SingletonclassMyType{}Container.bind(MyType).scope(Scope.Local);
To define a new scope, you just have to extend the Scope abstract class:
classMyScopeextendsScope{resolve(factory:ObjectFactory,source:Function,context:BuildContext){console.log('created by my custom scope.')returnfactory(context);}}@Scoped(newMyScope())classPersonService{ @InjectprivatepersonDAO:PersonDAO;}
Factories can be used to create the instances inside the IoC Container.
constpersonFactory:ObjectFactory=()=>newPersonService(); @Factory(personFactory)classPersonService{ @InjectprivatepersonDAO:PersonDAO;}
The Factory method will receive theBuildContext as parameter. So, if you need to retrieve another instance from the container to perform the factory instantiation, you can ask it to the BuildContext. For example:
constpersonFactory:ObjectFactory=(context)=>newPersonService(context.resolve(PersonDAO)); @Factory(personFactory)classPersonService{constructor(privatepersonDAO:PersonDAO){}}
The @OnlyInstantiableByContainer annotation transforms the annotated class, changing its constructor. So, it will only be able to create new instances for the decorated class through to the IoC Container.
It is usefull, for example, to avoid direct instantiation of Singletons.
@Singleton @OnlyInstantiableByContainerclassPersonService{ @InjectprivatepersonDAO:PersonDAO;}
If anybody try to invoke:
newPersonService();
Will prodeuce a TypeError.
You can also bind types directly to Container resolution.
// it will override any annotation configurationContainer.bind(PersonDAO).to(ProgrammerDAO).scope(Scope.Local);// that will make any injection to Date to return// the same instance, created when the first call is executed.Container.bind(Date).to(Date).scope(Scope.Singleton);// it will ask the IoC Container to retrieve the instance.letpersonDAO=Container.get(PersonDAO);
classPersonDAO{ @InjectprivatepersonRestProxy:PersonRestProxy;}Container.bind(PersonDAO);letpersonDAO:PersonDAO=Container.get(PersonDAO);// orletotherPersonDAO:PersonDAO=newPersonDAO();// personDAO.personRestProxy is defined. It was resolved by Container.
@OnlyInstantiableByContainer@SingletonclassPersonDAO{}letp:PersonDAO=newPersonDAO();// throws a TypeError. classes decorated with@OnlyInstantiableByContainer can not be instantiated directlyconstpersonFactory:ObjectFactory=()=>newPersonDAO();Container.bind(PersonDAO).factory(personFactory);//Works OKletpersonDAO=Container.get(PersonDAO);// Works OK
It is possible to bind constants to the Container. It is useful for configurations, for example.
interfaceConfig{dependencyURL:string;port:number;}Container.bindName('config').to({dependencyURL:'http://localhost:8080',port:1234});
And then you can use the@InjectValue decorator exactly as you use@Inject to inject instances.
classMyService{constructor(@InjectValue('config')publicconfig:Config){}}
It is possible to inject an internal property from a constant, like:
classMyService{constructor(@InjectValue('config.dependencyURL')privateurl:string, @InjectValue('myConfig.otherProperty.item[0].otherURL')privateotherURL:string){}}
And also to mix constants and other container injections, like:
classMyService{constructor(@InjectValue('config.dependencyURL')privateurl:string, @InjectValue('myConfig.otherProperty.item[0].otherURL')privateotherURL:string, @InjectprivatemyRepository:MyRepository){}}
Value Injections can be used direclty in class properties:
classMyService{ @InjectValue('config.dependencyURL')privateurl:string; @InjectValue('myConfig.otherProperty.item[0].otherURL')privateotherURL:string; @InjectprivatemyRepository:MyRepository;}
Or read directly from the Container:
consturl:string=Container.getValue('config.dependencyURL');
It is possible to bind an internal property of a constant, like:
Container.bindName('config.dependencyURL').to('http://anewURL.com');
It is possible to create specific namespaces with custom configurations and then tell container to use these namespaces.
For example:
Container.bindName('config.dependencyURL').to('http://myURL.com');constnamespace=Container.namespace('test');Container.bindName('config.dependencyURL').to('http://anewURL.com');
Only if the namespace'test' is active, the'config.dependencyURL' will resolve to'http://anewURL.com'.
To use the default namespace, just callContainer.namespace(null).
If you want to remove a namespace, just callnamespace.remove()
constnamespace=Container.namespace('test');namespace.remove();
It is not possible to remove the default namespace.
An alias called'environment' is defined for the namespace method:
Container.namespace('test');Container.environment('test');// both commands are equivalents
Take a look athere for more examples of namespaces usage.
You can use snapshot for testing or where you need to temporarily override a binding.
describe('Test Service with Mocks',()=>{constsnapshot:Snapshot;before(function(){// Store the IoC configurationsnapshot=Container.snapshot();// Change the IoC configuration to a mock service.Container.bind(IService).to(MockService);});after(function(){// Put the IoC configuration back for IService, so other tests can run.snapshot.restore();});it('Should do a test',()=>{// Do some test});});
You can put all manual container configurations in an external file and then use the '''Container.configure''' method to import them.
For example, you can create theioc.config.ts file:
import{MyType,MyTypeImpl,MyType2,MyType2Factory}from'./my-types';import{Scope}from'typescript-ioc';import*asyamlfrom'js-yaml';import*asfsfrom'fs';constconfig=yaml.safeLoad(fs.readFileSync('service-config.yml','utf8'));exportdefault[{bind:MyType,to:MyTypeImpl},{bind:MyType2,factory:MyType2Factory,withParams:[Date],scope:Scope.Singleton},{bindName:'config',to:config}];
And then import the configurations using:
import{Container}from"typescript-ioc";importconfigfrom'./ioc.config';Container.configure(config);
You need to load the configurations only once, but before you try to use the objects that depends on these files.
You can create configurations for specific namespaces, like:
import{MyRepository,MyTestRepository}from'./my-types';import*asyamlfrom'js-yaml';import*asfsfrom'fs';constconfig=yaml.safeLoad(fs.readFileSync('service.config.yml','utf8'));constconfigTest=yaml.safeLoad(fs.readFileSync('service.config-test.yml','utf8'));constconfigProd=yaml.safeLoad(fs.readFileSync('service.config-prod.yml','utf8'));exportdefault[{bindName:'config',to:config},{namespace:{test:[{bindName:'config',to:configTest},{bind:MyRepository,to:MyTestRepository},],production:[{bindName:'config',to:configProd}]}}];
Typescript interfaces only exist at development time, to ensure type checking. When compiled, they do not generate runtime code.This ensures good performance, but also means that is not possible to use interfaces as the type of a property being injected. There is no runtime information that could allow any reflection on interface type. Take a look atmicrosoft/TypeScript#3628 for more information about this.
So, this is not supported:
interfacePersonDAO{get(id:string):Person;}classProgrammerDAOimplementsPersonDAO{ @InjectprivateprogrammerRestProxy:PersonRestProxy;get(id:string):Person{// get the person and return it...}}Container.bind(PersonDAO).to(ProgrammerDAO);// NOT SUPPORTEDclassPersonService{ @Inject// NOT SUPPORTEDprivatepersonDAO:PersonDAO;}
However there is no reason for panic. Typescript classes are much more than classes. It could have the same behavior that interfaces on other languages.
So it is possible to define an abstract class and then implement it as we do with interfaces:
abstractclassPersonDAO{abstractget(id:string):Person;}classProgrammerDAOimplementsPersonDAO{ @InjectprivateprogrammerRestProxy:PersonRestProxy;get(id:string):Person{// get the person and return it...}}Container.bind(PersonDAO).to(ProgrammerDAO);// It worksclassPersonService{ @Inject// It worksprivatepersonDAO:PersonDAO;}
The abstract class in this example has exactly the same semantic that the typescript interface on the previous example. The only difference is that it generates type information into the runtime code, making possible to implement some reflection on it.
Some examples of using the container for tests:
describe('My Test',()=>{letmyService:MyService;beforeAll(()=>{classMockRepositoryimplementsAuthenticationRepository{asyncgetAccessToken(){return'my test token';}}Container.bind(AuthenticationRepository).to(MockRepository)myService=Container.get(MyService);});//...});
or you can configure all your mocks togheter in a mocks.config.ts
classMockRepositoryimplementsAuthenticationRepository{asyncgetAccessToken(){return'my test token';}}classOtherMockRepositoryimplementsOtherRepository{asyncdoSomething(){return'done';}}exportdefault[{bind:AuthenticationRepository,to:MockRepository},{bind:OtherRepository,to:OtherMockRepository}];
and then in your test files:
importmocksConfigfrom'./mocks.config.ts';describe('My Test',()=>{letmyService:MyService;beforeAll(()=>{Container.config(mocksConfig);myService=Container.get(MyService);});//...});
or if you want to use the configurations and restore the container after the test:
importmocksConfigfrom'./mocks.config.ts';describe('My Test',()=>{letmyService:MyService;letsnaphot:Snaphot;beforeAll(()=>{snapshot=Container.snapshot();Container.config(mocksConfig);myService=Container.get(MyService);});afterAll(()=>{snapshot.restore();});//...});
Define configurations on a file, likeioc.config.ts:
import{MyRepository,MyTestRepository}from'./my-types';import*asyamlfrom'js-yaml';import*asfsfrom'fs';constconfig=yaml.safeLoad(fs.readFileSync('service.config.yml','utf8'));constconfigTest=yaml.safeLoad(fs.readFileSync('service.config-test.yml','utf8'));constconfigProd=yaml.safeLoad(fs.readFileSync('service.config-prod.yml','utf8'));exportdefault[{bindName:'config',to:config},{env:{test:[{bindName:'config',to:configTest},{bind:MyRepository,to:MyTestRepository},],production:[{bindName:'config',to:configProd}]}}];
And then import the configurations using:
import{Container}from"typescript-ioc";importconfigfrom'./ioc.config';Container.configure(config);// Then activate the environment calling the containerContainer.environment(process.env.NODE_ENV);
It was tested with browserify and webpack, but it should work with any other similar tool.
Starting from version 2, this library only works in browsers that supports javascript ES6 '''class'''. If you need to support old ES5 browsers, please use the version 1.2.6 of this library
- Circular injections are not supported
Some breaking changes:
This library does not support old ES5 code anymore. So, you need to change the target compilation of your code toes6 (or anything else newer, like es2016, es2020, esnext etc)
Yourtsconfig.json needs to include at least:
{"compilerOptions": {"experimentalDecorators":true,"emitDecoratorMetadata":true,"target":"es6" }}This decision was taken to help to solve a lot of bugs with react native and browser environments.
If you need to support es5 code, you can keep using the 1.2.6 version
A lot of confusion with@AutoWired motivated us to rename it to@OnlyInstantiableByContainer. It is a big name, but it says exactly what that decorator does. It is completely optional (The container will always work in the same way when instrumenting the types), but it transforms the decorated constructor to avoid that anybody create new instances calling direct a new expression.
So you need to change all references to@AutoWired to@OnlyInstantiableByContainer.
We changed the name of the interfaceProvider toObjectFactory and also change the definition of this type to be a simple function signature.
So, now we have:
// previous versionconstprovider={get:()=>newMyType()};// new versionconstfactory=()=>newMyType();
Following the same design, whe renamed the@Provideddecorator to@Factory.
// previous version@Provided({get:()=>newMyType()})classMyType{}// new version@Factory(()=>newMyType())classMyType{}
The@Provides decorator was removed because it could cause a lot of problems, once it was used in the class that would provide an implementation, that usually was always defined in different files. That forced us to had things likeContainerConfig.addSource() to scan folders for files. It caused problems in react native, in browser and in some environments like lambda functions.
We redesigned a new way to load container configurations that does not need to scan folders anymore, removing the problems and improving the performance. Take a look atContainer.configure method for a better option for the old@Provides.
We had a minor change in the Snapshot handling. We don't have anymore the public methodContainer.restore(type). A safer way to work with snapshots was implemented. Now theContainer.snapshot method returns a snapshot object. That object has arestore() method.
The new way:
constsnapshot=Container.snapshot();snapshot.restore();
About
A Lightweight annotation-based dependency injection container for typescript.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors9
Uh oh!
There was an error while loading.Please reload this page.
