"siringa" (italian): "syringe", a tool for administering injections.
SIRINGA is an extremely simple dependency injection framework focusing on type correctness and asynchronous injections, without relying on decorators and/or decorator metadata.
- Quick Start
- One-Time Injection
- Name-based Injection
- Promises Injection
- Using Factories
- Using Instances
- Child Injectors
- API Reference
- Copyright Notice
- License
At its core, SIRINGA tries to make dependency injection as easy as possible.
Classes can be injected their dependencies by specifying a static $inject
property (a tuple of injection keys):
// A class with no dependencies, and an implicit zero-args constructor
class Foo {
foo() { /* ... your code... */ }
}
// Here "Bar" needs an instance of "Foo" to be injected
class Bar {
// The _tuple_ of required injections, this must be declared `as const`
// and the resolved types must match the constructor's arguments
static $inject = [ Foo ] as const
// Called by the injector with the correct dependencies
constructor(private _foo: Foo) {}
// Your code
bar() {
this._foo.bar()
}
}
// Create a new injector
const injector = new Injector()
.bind(Foo) // Bind "Foo" (no dependencies required)
.bind(Bar) // Bind "Bar" (requires "Foo")
// Get the singleton instance of "Bar"
const bar = await injector.get(Bar)
// Get the singleton instance of "Foo" (injection magically happens!)
const foo = await injector.get(Foo)
We can use an Injector
also to perform one-time injections (the instance
injected won't be managed by the Injector
itself).
Using the same Foo
and Bar
classes from above:
// Create a new injector and bind Foo
const injector = new Injector().bind(Foo)
// Create a new "Bar" instance injected with its required "Foo"
const bar = await injector.inject(Bar)
// Will return the same instance given to "Bar"'s constructor
const foo = await injector.get(Foo)
// The code below will miserably fail, as "Bar" is not managed by the injector
// const bar2 = await injector.get(Bar)
Injection keys must not necessarily be classes, they can also be string
s:
class Foo {
foo() { /* ... your code... */ }
}
class Bar {
// The string `foo` is used to identify the binding
static $inject = [ 'foo' ] as const
// Called by the injector with the correct dependencies
constructor(private _foo: Foo) {}
// Your code
bar() {
this._foo.bar()
}
}
// Create a new injector
const injector = new Injector()
.bind('foo', Foo) // Bind "Foo" to the string 'foo'
.bind('bar', Bar) // Bind "Bar" to the string 'bar'
// Get the singleton instance of "Bar"
const bar = await injector.get('bar')
// Get the singleton instance of "Foo"
const foo = await injector.get('foo')
Sometimes it might be necessary to be injected a Promise
of a component,
rather than a component itself.
For example, in an Lambda Function we might want to asynchronously use other services (retrieve secrets, connect to databases, ...) while starting to process a request.
In this case we can request to be injected a (yet unresolved) Promise
of a
bound component:
import { promise } from 'siringa'
class Foo {
foo() { /* ... your code... */ }
}
class Bar {
// The `promise(...)` function declares the injection of a `Promise`
static $inject = [ promise(Foo) ] as const
// Called by the injector with the correct `Promise` for dependencies
constructor(private _foo: Promise<Foo>) {}
// Your code
bar() {
await (this._foo).bar()
}
}
The injector's create(...)
method allows to use a factory pattern to
create instances to be injected:
class Foo {
constructor(public _value: number)
}
class Bar {
// The string `foo` is used to identify the binding
static $inject = [ Foo ] as const
// Called by the injector with the correct dependencies
constructor(private _foo: Foo) {}
// Your code
bar() {
console.log(this._foo._value)
}
}
// Create a new injector
const injector = new Injector()
.bind(Foo, (injections) => {
// The "injections" object can be used to resolve dependencies
// in the injector itself using "get(...)" or "inject(...)"
return new Foo(12345)
})
// Inject "Bar" with "Foo"
const bar = await injector.inject(Bar)
// This will print "12345"
bar.bar()
As in the example above, factory methods will be given an Injections
instance
which can be used to get instances, inject new objects, or create sub-injectors.
See the reference for Injections
below.
Similar to factories (above) the injector's use(...)
method allows to use
pre-baked instances as dependencies for other objects:
class Foo {
constructor(public _value: number)
}
class Bar {
// The string `foo` is used to identify the binding
static $inject = [ Foo ] as const
// Called by the injector with the correct dependencies
constructor(private _foo: Foo) {}
// Your code
bar() {
console.log(this._foo._value)
}
}
// Create a new injector
const injector = new Injector()
.use(Foo, new Foo(12345))
// Inject "Bar" with "Foo"
const bar = await injector.inject(Bar)
// This will print "12345"
bar.bar()
In some cases it is useful to create child injectors.
A child injector inherits all the bindings from its parent, but any extra binding declared in it won't affect its parent.
Also, a child injector can redeclare a binding without affecting its parent.
For example (using silly strings to simplify the inner workings!)
const parent = new Injector()
.use('foo', 'parent foo')
const child = parent.injector()
.create('parentFoo', async (injections) => {
const s = await injections.get('foo') // this is the parent's value!!!
return `${s} from a child`
}
.use('foo', 'child foo')
await parent.get('foo') // this will return "parent foo"
await child.get('foo') // this will return "child foo" (overrides parent)
await child.get('parentFoo') // this will return "parent foo from a child"
// await parent.get('parentFoo') // fails, as parent doesn't define "parentFoo"
Quick-and-dirty reference for our types (for details, everything should be annotated with JSDoc).
-
injector.bind(component: Constructor): Injector
Bind the specified constructor (a class) to the injector -
injector.bind(component: Constructor, implementation: Constructor): Injector
Bind the specified component (a class) to the injector, and use the specified implementation (another class extending the component) to create instances -
injector.bind(name: string, implementation: Constructor): Injector
Bind the specified name (a _string) to the injector, and use the specified implementation (a class ) to create instances
-
injector.create(component: Constructor, factory: Factory): Injector
Use the specified factory (a function) to create instances for the specified component (a class) -
injector.create(name: string, factory: Factory): Injector
Use the specified factory (a function) to create instances for the specified name (a string)
-
injector.use(component: Constructor, instance: any): Injector
Bind the specified instance to a component (a class). -
injector.create(name: string, instance: any): Injector
Bind the specified instance to a name (a string).
-
injections.get(component: Constructor): Instance
Returns the instance bound to the specified constructor (a class) -
injections.get(name: string): Instance
Returns the instance bound to the specified name (a string) -
injections.inject(injectable: Constructor): Instance
Create a new instance of the given injectable (a class) injecting all its required dependencies -
injections.make(factory: Factory): ReturnType<Factory>
Simple utility method to invoke the factory with the correctInjections
and return its result. This can be used to alleviate issues when top-level await is not available.
injections.injector(): Injector
Create a sub-injector (child) of the current one
-
injections.get(component: Constructor): Instance
Returns the instance bound to the specified constructor (a class) -
injections.get(name: string): Instance
Returns the instance bound to the specified name (a string) -
injections.inject(injectable: Constructor): Instance
Create a new instance of the given injectable (a class) injecting all its required dependencies -
injections.injector(): Injector
Create a sub-injector (child) of the current one