Features
Component Scanner
To automatically register all decorated components in a
container instance the method enableComponentScanner()
need
to be called on the instance.
Please note that when using
enableComponentScanner()
all components have to be imported. Make sure that your bundler (such as webpack) did not optimize theimport
statement away.
Lifecycle methods
To hook into component creation and destruction there are two
decorators available. @initialize
and @destroy
.
They are call on the respective lifecycle event.
import { component, initialize, destroy } from 'tsdi';
@component
export class User {
private timer: NodeJS.Timer;
@initialize
public init(): void {
this.timer = setInterval(() => {
// do someting ...
}, 100);
}
@destroy
public destroy(): void {
clearInterval(this.timer);
}
}
Name based injection (hints)
Instead of using the type of the component (the type of the class) as identifier for injection it is also possible to specify the name as a custom string as depicted in the following example:
import { TSDI, Component, Inject } from 'tsdi';
@Component()
class A {}
@Component()
class B extends A {}
@Component({name: 'Bar'})
class C extends A {}
@Component({name: 'Foo'})
class D extends A {
@Inject({name: 'Bar'})
private a: A;
}
const tsdi: TSDI = new TSDI();
tsdi.enableComponentScanner();
const a: A = tsdi.get(A, 'Foo');
This can be particularly useful if you have certain circular dependencies which can not be expressed without name based injection.
Property value injection (configuration)
In cases in which it is necessary to simply inject a single atomic value, as for example a config value, it is possible
to define a property using tsdi.addProperty(...)
. it can then be injected normally using name based injection.
import { TSDI, Component, Inject } from 'tsdi';
@Component()
class A {
@Inject({name: 'config-key'})
public some: string;
}
const tsdi: TSDI = new TSDI();
tsdi.addProperty('config-key', 'config-value');
tsdi.register(A);
console.log(tsdi.get(A).some); // 'config-value'
You should not use property value injection to inject complex structures, objects or instances of classes. Consider
using a @Factory()
instead.
Constructor parameter injection
import { TSDI, Component, Inject } from 'tsdi';
@Component()
class A {}
@Component()
class B {}
@Component()
class C {
constructor(@Inject() a: A, @Inject() b: B) {}
}
const tsdi: TSDI = new TSDI();
tsdi.enableComponentScanner();
tsdi.get(C);
Singletons vs. Instances
Sometimes it can be useful to inject a new instance of a Component everytime it is injected. In order to achieve this
you can configure the component to not be singleton { singleton: false }
and a new instance will be created everytime
it is retrieved or injected.
import { TSDI, Component } from 'tsdi';
@Component({ singleton: false })
class A {}
@Component()
class B {}
const tsdi: TSDI = new TSDI();
tsdi.enableComponentScanner();
const a0: A = tsdi.get(A);
const a1: A = tsdi.get(A);
// a0 !== a1
const b0: B = tsdi.get(B);
const b1: B = tsdi.get(B);
// b0 === b1
Factories
In cases in which you need to be able to inject dependencies which are not just classes but objects (interfaces) or
simply not maintained by you it is possible to define a @Factory()
which creates these dependencies and makes them
injectable:
import { TSDI, Component, Factory } from 'tsdi';
class A {}
@Component()
class B {
@Factory()
public createA(): A {
return new A();
}
}
const tsdi: TSDI = new TSDI();
tsdi.enableComponentScanner();
tsdi.get(A);
Please note that the return-type of the factory needs to be deductable by the
Reflection
api and if it is not you will need to use name based injection.
Externals
Sometimes (for example when dealing with React) it is necessary to inject dependencies into classes which are not
@Components()
themself and which should not be augmented by TSDI. As TSDI overrides the constructor()
of all
classes which use TSDI it is necessary to mark these classes (for example every React component) using the
@External()
decorator.
import { TSDI, Component, External } from 'tsdi';
@Component
class A {}
@Component
class B {}
@External()
class C {
@Inject()
public a: A;
public b: B;
constructor(@Inject() b: B) {
this.b = b;
}
@Initialize()
public init() {
}
}
const tsdi: TSDI = new TSDI();
tsdi.enableComponentScanner();
const c = new C();
console.log(c.a);
Lazy injection
You can mark individual injections as { lazy: true }
, which will lead to the injected @Component()
s being
created only when they are first touched.
import { TSDI, Component, Inject } from 'tsdi';
@Component()
class A {
@Inject({lazy: true})
public some: Dependency;
}
const tsdi: TSDI = new TSDI();
tsdi.register(A);
const a = tsdi.get(A); // <-- at this point a.some is still undefined (not created and not injected)
console.log(a.some); // <-- at this point some is created and return (on first property access)
Lifecycle listeners
import { TSDI, Component, Inject } from 'tsdi';
@component
class A {
}
const tsdi: TSDI = new TSDI();
tsdi.register(A);
tsdi.addLifecycleListener({
onCreate(component: any): void {
console.log(component); // <-- this line is executed the first time a component is created
}
onDestroy(component: any): void {
console.log(component); // <-- this line is executed a component is destroyed (e.g. container close)
}
});
const a = tsdi.get(A);
tsdi.close();
Eager components
You can mark an individual @Component()
as { eager: true }
which will make sure that this component
is instanced as soon as it is discovered by TSDI.
import { TSDI, Component, Inject } from 'tsdi';
@component({ eager: true })
class A {}
const tsdi: TSDI = new TSDI();
tsdi.register(A); // <-- here the class A is instantiated
Debug logging
To inspect which component is created when and injected where one can enable debug logging by either
set the environment variable DEBUG
(node) or a localStorage key (browser) debug
to tsdi
.
Automocks
import { TSDI, component, inject, initialize } from 'tsdi';
@component
class Foo {
public foo(): void {
}
}
@component
class Bar {
public bar(): string {
return 'bar';
}
}
@component
class Baz {
@inject
public foo: Foo;
@inject
public bar: Bar;
@initialize
protected init(): void {
this.foo.foo();
console.log(this.bar.bar()); // <-- logs 'bar' since this.bar is not mocked
}
}
// This means: create mocks for all inject but 'Bar'
tsdi.enableAutomock(Bar);
tsdi.get(Baz);
Scopes
Scopes could be seen as lifecycle bounds for a managed dependency. By that it is meant that components with a defined scope are only as long as the scope is entered/valid.
import { TSDI, component, destroy } from 'tsdi';
@component({scope: 'some-scope'})
class Foo {
@destroy
private close(): void {
// free resources...
}
}
tsdi.get(Foo); // <-- will throw since the scope was not entered
tsdi.getScope('some-scope').enter();
tsdi.get(Foo); // <-- will return a new Foo
tsdi.getScope('some-scope').leave();
// Foo is destructed
tsdi.getScope('some-scope').enter();
tsdi.get(Foo); // <-- will return a new Foo
Whenever a scope is left, the lifecycle callbacks are executed. In the above
example the close
method is invoked.
Currently it is valid to inject scoped components into unscoped components which will lead to stale dependencies, since TSDI does not clear out injected dependencies as components are destructed.
Dynamic Injections
The dynamic setting on an injected dependency marks it as dependency to
be reevaluated on every access.
This means it is a dependency which could be come and go every moment and prior
to access an application should check the availability.
This also means it could be dynamic dependencies which could could be injected
in more static ones.
import { TSDI, component, inject } from 'tsdi';
@component({scope: 'some-scope'})
class Foo {
public foo(): void {
// do something
}
}
@component
class Bar {
@inject({dynamic: true})
private foo: Foo;
public bar(): void {
this.foo.foo();
}
}
const bar = tsdi.get(Bar); // <-- bar is constructed without a foo
bar.bar() // <-- this will throw, since foo is not available
tsdi.getScope('some-scope').enter();
bar.bar() // <-- this will be okay, since foo is available here
tsdi.getScope('some-scope').leave();
bar.bar() // <-- this will throw, since foo is not available
tsdi.getScope('some-scope').enter();
bar.bar() // <-- this will be okay, since a new foo is available here
StrictPropertyInitialization
The new --strictPropertyInitialization
in TypeScript 2.7 could be used with TSDI by
using the definite assignment assertion modifiers.
import { component, inject } from 'tsdi';
@component
class Foo {
}
@component
class Bar {
@inject
private foo!: Foo; // note the ! here. It will supress the initialization error
}
Async Dependencies
Components can have @initialize
methods which are typed async
or delcare Promise
as return type.
note: It is not sufficient to just return a Promise
since then the typescript compiler may
not detect the async nature of the method.
When injected into another component the depending component's @initialize
method will be called
after the dependencies initializer has resolved. This is for example usefull when injecting a
something like a database connecti#on which needs asynchroneous setup code:
import { component, inject } from 'tsdi';
@component
class DatabaseConnection {
public connection?:
@initialize
private async initialize() {
this.connection = await connectToDatabase();
}
}
@component
class RestApi {
@inject private db!: DatabaseConnection;
@initialize
private initialize() {
// This initializer will be called after the database was injected.
console.log(this.db.connection.query('SELECT * FROM user'));
}
}
This does not work with dynamic injections and will throw an error. Please note that async injections can not be lazy and will not be lazy by default.