fuse-ts is a dependency injection library for
TypeScript. It allows you to use a @fused
decorator on your TypeScript classes, so that when an instance
of that class is constructed, it will receive instances of the types the class depends on.
import {fuse, fused} from 'fuse-ts';
class Service {
public someMethod(): void { }
}
class ServiceImplementation implements Service {
public someMethod(): void { }
}
@fused
class ServiceConsumer {
constructor(public service?: Service) { }
}
// let fuse know that all Service dependencies are
// to be instances of ServiceImplementation
fuse(Service).to(ServiceImplementation);
var serviceConsumer: ServiceConsumer = new ServiceConsumer();
// this will be true
var isServiceImplementation: boolean = serviceConsumer.service instanceof ServiceImplementation;
As you can see in the example, the ServiceConsumer
constructor has one
optional argument of the type Service
. Upon creating an instance by using
new ServiceConsumer()
, fuse-ts will for each constructor argument look for a
type that has been "fused" to the type of that argument, inject an instance in
the constructor. So, in the example, a ServiceImplementation
instance is
injected in the ServiceConsumer
constructor. This is made possible by the
@fused
decorator on the ServiceConsumer
class.
To use fuse-ts in your project, make sure you use the --experimentalDecorators
and --emitDecoratorMetadata
flags when running tsc
, or use the following in your tsconfig.json file:
{
...
"experimentalDecorators": true,
"emitDecoratorMetadata": true
...
}
When you decorate a TypeScript class with @fused
, the class's constructor is wrapped by a function
that resolves values for the original constructor's arguments, based on their type. For convenience, it
is preferable to make all the constructor's arguments you want injected optional, so you can call the
constructor without TypeScript compiler complaints. Like this:
@fused
class Component {
constructor(a?: A, b?: B) {
// stuff
}
}
var component = new Component();
The following won't compile:
@fused
class Component {
constructor(a: A, b: B) {
// stuff
}
}
// Fails with "Supplied parameters do not match any signature of call target."
var component = new Component();
If there are arguments you always want to pass to the constructor (ie. never injected by fuse-ts), make them required:
@fused
class Component {
constructor(value: number, a?: A, b?: B) {
// stuff
}
}
var component = new Component(1);
To make a type injectable, it must be registered with the library. This is done by calling
fuse(BaseType).to(InjectedType)
. See the following example:
class BaseType {
public someMethod(): void { }
}
class InjectedType implements BaseType {
public someMethod(): void {
// stuff
}
}
// register with fuse-ts
fuse(BaseType).to(InjectedType);
You probably noticed that InjectedType
implements a class instead of a TypeScript interface. Since
interfaces are a design-time feature of TypeScript, they are not available in the transpiled JavaScript at
run time. Because of that and the way fuse-ts works (for now), interfaces are unsuitable as a type to
resolve to. So the following won't work:
interface Foo {
fooIt(): void;
}
class InjectedFoo implements Foo {
public fooIt(): void {
// stuff
}
}
// register with fuse-ts
fuse(Foo).to(InjectedFoo); // Compilation fails with "Cannot find name 'Foo'"
When you use fuse(A).to(B)
, each injected instance of A
will resolve to a new instance of B
. This is called a transient dependency. This is the default behaviour of fuse-ts. To make this explicit in your code, you can use the asTransient()
method on the result of fuse(A).to(B)
.
An example:
class Service {
public serviceMethod(): void {}
}
class SingletonServiceImplementation implements Service {
public serviceMethod(): void {
// stuff
}
}
fuse(Service).to(SingletonServiceImplementation).asTransient();
@fused
class ServiceConsumer {
constructor(public service?: Service) { }
}
var firstInstance = new ServiceConsumer();
var secondInstance = new ServiceConsumer();
// The following will be true
firstInstance.service !== secondInstance.service;
Sometimes you want a service to be one and the same instance across your whole project, in other words, you want it to be a singleton. By default, each time fuse-ts injects a dependency, an new instance of that dependency is created. To make fuse-ts inject a singleton, you use the following:
class Service {
public serviceMethod(): void {}
}
class SingletonServiceImplementation implements Service {
public serviceMethod(): void {
// stuff
}
}
fuse(Service).to(SingletonServiceImplementation).asSingleton();
Now, each time a class that depends on Service
is instantiated, it will receive the same
SingletonServiceImplementation
instance:
@fused
class ServiceConsumer {
constructor(public service?: Service) { }
}
var firstInstance = new ServiceConsumer();
var secondInstance = new ServiceConsumer();
// The following will be true
firstInstance.service === secondInstance.service;
This was created as part of a first personal venture into TypeScript, and inspired by the .NET Ninject DI library. Comments and complaints are welcome.