siringa
v0.4.4
Published
Simple type-checking injection library for Typescript
Readme
SIRINGA
"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
Quick Start
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)One-Time Injection
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)Name-based Injection
Injection keys must not necessarily be classes, they can also be strings:
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')Promises Injection
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()
}
}Using Factories
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.
Using Instances
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()Child Injectors
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"API Reference
Quick-and-dirty reference for our types (for details, everything should be annotated with JSDoc).
class Injector
Binding
injector.bind(component: Constructor): Injector
Bind the specified constructor (a class) to the injectorinjector.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 instancesinjector.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
Factories
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)
Instances
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
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 dependenciesinjections.make(factory: Factory): ReturnType<Factory>
Simple utility method to invoke the factory with the correctInjectionsand return its result. This can be used to alleviate issues when top-level await is not available.
Sub Injectors
injections.injector(): Injector
Create a sub-injector (child) of the current one
interface Injections
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 dependenciesinjections.injector(): Injector
Create a sub-injector (child) of the current one
