Yet another cheap and simple dependency injection library for TypeScript.
Just like you'd expect:
npm install --save inclined-plane
Define a perfectly normal interface. You probably want to export it.
export interface Logger {
debug(message: string): void;
}
Create the InjectableType
version with the same name:
import {injectableType} from 'inclined-plane';
export interface Logger {
debug(message: string): void;
}
export const Logger = injectableType<Logger>('Logger');
Yes, the syntax there is a little redundant because we need both compile- and run-time type information.
The type identifier can now be used to produce decorators:
Decorator | Target | Example | Intent |
---|---|---|---|
.implementation |
Class | @Logger.implementation |
The decorated class is an implementation of the type. |
.accessor |
Method | class LoggerBuilder { |
The decorated instance method can be called to get instances of the type. |
.supplier |
Method | class LoggerBuilder { |
The decorated static method can be called to get instances of the type. |
.required |
Param | constructor( |
A value of that type for the parameter is required. |
.optional |
Param | (same as above) | A value of that type should be provided, or undefined if not available. |
.inject |
Property | class Whatever { |
A value of that type should be injected into that property. |
Given a normal service/bean/component which you don't need to export, identify it by decorating it:
import {Logger} from './path/to/Logger';
@Logger.implementation
class ConsoleLogger implements Logger {
debug(message: string) {
console.log(message);
}
}
Or decorate an instance method:
class LoggerBuilder {
@Logger.accessor
public buildLogger(): Logger {
return new ConsoleLogger();
}
}
Or decorate a static method:
class LoggerBuilder {
@Logger.supplier
public static buildLogger(): Logger {
return new ConsoleLogger();
}
}
It can now be automagically instantiated and injected.
You can get a managed instance from the InjectableType
instance or from the buildInstance
function for classes that aren't decorated as providers:
import {buildInstance} from 'inclined-plane';
import {Logger} from './path/to/Logger';
import {WidgetService} from './path/to/WidgetService';
const logger = Logger.getInstance(); // will be a ConsoleLogger
const widgetService = buildInstance(WidgetService);
If you have multiple providers for a given type, you can ask for all of them with getInstances()
:
const loggers = Logger.getInstances();
Note that this will return an empty array if no providers have been loaded/imported.
In your main/index, you'll want to ensure that you import
all your injectable services/beans/components.
Because they are not directly referenced, they won't be seen unless their files are imported!
import './path/to/ConsoleLogger';
You don't need to do anything more—just import the file.
Generally, this is easiest to do by defining a file like services.ts
or implementations.ts
that lists all injectable imports, then include that one file from your main/index.
Constructor parameters can be decorated for injection:
import {DatabaseAdapter} from './path/to/DatabaseAdapter';
import {Logger} from './path/to/Logger';
export class WidgetService {
constructor(
@DatabaseAdapter.require private readonly db: DatabaseAdapter,
@Logger.optional private readonly logger: Logger,
) {}
}
Sometimes your type system will end up complex enough to have cycles:
interface Left {
right: Right | undefined;
}
interface Right {
left: Left | undefined;
}
const Left = injectableType<Left>('Left');
const Right = injectableType<Right>('Right');
@Left.implementation
class LeftImpl implements Left {
constructor(@Right.required private readonly right?: Right) {}
}
@Right.implementation
class RightImpl implements Right {
constructor(@Left.required private readonly left?: Left) {}
}
When attempting to build an instance you'll see a message like:
Dependency cycle detected while trying to build ...
You can break these cycles by moving injected items from constructor parameters to properties with the .inject
decorator:
class LeftImpl {
@Right.inject private readonly right: Right | undefined;
}
class RightImpl {
@Left.inject private readonly left: Left | undefined;
}
Properties values injected via .inject
are not available in the constructor:
class LeftImpl {
@Right.inject private readonly right: Right | undefined;
constructor() {
console.log(this.right); // undefined
}
}
Instead, you can define a postConstruct()
method (see the ManagedInstance
interface) to be called when all managed values have been injected:
import {ManagedInstance} from 'inclined-plane';
class LeftImpl implements ManagedInstance {
@Right.inject private readonly right: Right | undefined;
protected postConstruct() {
console.log(this.right); // not undefined
}
}
Generally, you'll want to make this protected
so it's not visible to other types, but you can still call it from subclasses for testing purposes.
Note that .inject
has the same semantics as .optional
and not .required
: it will silently allow an undefined
value if no providers can be found for the type.
Add a guard condition in postConstruct()
to detect undefined
if necessary.
-
Can I inject implementations based on type parameters?
Nope. You're trying to do something like this, right?
interface Enclosure<T> { /* ... */ } interface Animal { /* ... */ } const Enclosure = injectableType<Enclosure<any>>('Enclosure'); class ZooBuilding { constructor( @Enclosure.require private readonly animalEnclosure: Enclosure<Animal>, ) {} }
There are no plans to support this any time soon, as the complete lack of runtime type info makes this painful if not impossible.
-
v0.5.3 2019-08-07
- Export a few additional interfaces which might be useful to code which wants to do reflection-like things.
-
v0.5.2 2019-07-28
- Dependency updates
-
v0.5.1 2019-04-09
- Fixed the
.accessor
derp (you got an instance of the constructed proxy, not the desired interface implementation.)
- Fixed the
-
v0.5.0 2019-03-25
- Added
.accessor
for instance methods.
- Added
-
v0.4.0 2019-03-25
-
Breaking: Renamed
.provider
to.implementation
- Fixed: Supplier method names are no longer empty
- New: Introduced
InstanceResolver
to allow custom logic for tests
-
Breaking: Renamed
-
v0.3.0 2019-03-16
- Support
.supplier
for static methods.
- Support
-
v0.2.2 2019-03-15
- Clean up .npmignore (no code changes)
-
v0.2.1 2019-03-15
- Cover the case where super classes need values injected.
-
v0.2.0 2019-03-15
- Support
@Type.inject
for late property injection. It would be nice to be able to reuse the existing decorators, but see TypeScript issue 10777. - Add detection of dependency cycles.
- Documentation for
postConstruct()
. - A bunch of JSDoc for curious people.
- Support
-
v0.1.0 2019-03-14
- Initial release.
Copyright 2019 Rick Osborne
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: Apache 2.0 License.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.