@stnekroman/inversityts
v0.0.6
Published
IoC library for TypeScript
Downloads
73
Maintainers
Readme
Lightweight IoC (Inversion of Control) and DI (Dependency Injection) library.
Inspired by Angular and Spring Frameworks.
Injectors serve function of IoC containers.
Features:
- Injectors have hierarchy - everything known on parent level will be available for DI at child level.
- Child injectors can override parent tokens for his level and for nested, if any.
- Injectors can be detached from root.
- Tokens can be multiple in scope of one injector - you can request singleton instance from injectable or all, it it's multi.
- Injectables have scopes.
- Has circular dependency detection
- Supports @Configuration classes
- Supprots different value providers for injectable
- This library doesn't use reflect-metadata, the solution is prototype-based.
Table of Contents
Installation
npm
npm i @stnekroman/inversityts
yarn
yarn add @stnekroman/inversityts
And in your tsconfig.json you need to set (if you want to use decorators)
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Terminology
token
- something, against what (or "for what") to create new instance
.
Token can be literally anything - Symbol (recommended), string, object, class.instance
- result of DI process (Injector.createInstance()
call) against requested token
.injectable
- defined rule for creation of new instance(s) for specific token.
You can treat it as "bridge" from token
to instance
injector
- IoC container, which holds already created instances
, holds definitions of injectables
and can create instances
API Overview
@Injectable
Defines new injectable, describes how to create instance of it (single value, factory or class))
Can be called:
- as decorator on class - then class will be used to create new instances via
new
- as decorator on class method - if class annotated with
@Configuration, then annotated method will be used as factory for new instances. (works both on static and non-static method, see
@Configuration` for more info) - as function call to manually define new injectable
| Param | Description |
| --------- | -------------------------------------------------------------------------------------------------------------------------- |
| token
| Token for new injectable |
| options
| Optional options structure, which may contain: |
| | * injector
- direct ref to injector to use |
| | *tags
- reversed for future, not in use currently |
| | *class
- (for function call only) specifies which class to use to generate new instance |
| | *value
- (for function call only) specifies static value, which will be used as instance |
| | *redirect
- (for function call only) specifies another token, which will be used instead |
| | *factory
- (for function call only) specifies factory function which will be used to generate new instance |
| | *dependencies
- (for function call only) additional possible input dependenes for factory
function - list of token
s |
| | *scope
- indicates when to re-use existing instance, when - to create new |
Scopes
List of possible scopes:
| Scope | Description |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| singleton
(default) | Only one instance is create for injectable, cached and shared for future injects |
| prototype
| Each new inject call will cause new instance to be created |
| cache
| Internal caching map will be used, you need only specify provider, which will provide string keys to that map. |
| | On key change - new instance will be created. In order to active this scope mode you need just pass in |
| | a function () => string
, cache key provider |
| weak
| Optional param, which is in use onlye in case of using caching scope. |
| | When set to true
- internal cache will hold WeakRefs, which won't prevent garbage collector from collecting such references. |
| | So weak caching scope can be used when we don't want to keep created instance in memory, if that is not used (referenced) already. |
| custom
| That is possible to completely override logic of scoping and instances creation. |
| | Via passing in class extends ScopeProvider
. |
@Inject
Function to create (or get, if already exists) instance against given token.
| Param | Description |
| ---------- | ----------------------------------------------------- |
| token
| Token against which to create (or get) new injectable |
| injector
| Optional injectorto use |
@Inject.Param
Decorator, used to annotate parameters in class constructor or factory function as injectable targets.
DI process will use this info to find/create corresponding instances and inject them as parameter.
Accepts only one argument - token
Injector
IoC container, used to register, get and create injectable instances against given tokens.
| Method | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------- |
| root
(static) | Built-in default global root injector |
| getCurrentInjector
(static) | Method to return 'current' injector, associated with current context |
| register
| Low-level method to register injectables. Better use Injectable
instead |
| get
| Resolves given token to instace. |
| | * If injector has it internal cache - returns ready-to-use instance |
| | * If injector has injectable (metadata to create instance) - creates it and returns ref |
| | (despite if parent injector already has created instance, new metadata/injectable - new instance) |
| | * If no instance and no injectable metadata - asks parent for it (he may have something) |
@Configuration
Annotation for class, which may product injectables via methods (static and/or non-static) as factory methods.
Doesn't contain any input parameters so far. But it required for method-based injectable processing.
It's singleton - only one instance of such class may be created.
ForwardRef
Used for redirect tokens to pass token, which is not yet constucted.
Token
In case cases you might want to wrap your token to Token
wrapper.
Cases are:
- passing dependency to list of
dependencies
for raw factory function withmulti: true
- using this wrapper to pass options (like
multi: true
) to directInject
call.
Usage examples
Inject example
@Injectable()
class SimpleService {
public doSomething() {
// ...
};
}
class WithDependencies {
public readonly simple : SimpleService = Inject(SimpleService);
}
const ins = new WithDependencies(); // if constructor doesn't have dependencies
constructor has dependencies example
@Injectable()
class SimpleService {
public doSomething() {
// ...
};
}
class WithDependencies {
constructor(@Inject.Param(SimpleService) simple : SimpleService) {}
}
const ins = Injector.getCurrentInjector().createInstance(WithDependencies);
Hide implementation from consumer's eyes example
const TYPES = {
SERVICE_A: Symbol("ServiceA"),
SERVICE_B: Symbol("ServiceB"),
};
interface ServiceA {...}
interface ServiceB {...}
@Injectable(TYPES.SERVICE_A)
class ServiceAImpl implements ServiceA {...}
@Injectable(TYPES.SERVICE_B)
class ServiceBImpl implements ServiceB { // can have inner dependencies
constructor( @Inject.Param(TYPES.SERVICE_A) serviceA : ServiceA) {...}
}
class WithDependencies {
constructor(
@Inject.Param(TYPES.SERVICE_A) serviceA : ServiceA,
@Inject.Param(TYPES.SERVICE_B) serviceB : ServiceB
) {...}
}
const ins = Injector.getCurrentInjector().createInstance(WithDependencies);
Manual definition of tokens example
const valueToken = Injectable(Symbol("TEST_TOKEN"), {
value: "aval"
}); // function call returns `token` back
class WithDependencies {
constructor(
@Inject.Param(Symbol("TEST_TOKEN")) public readonly value1 : string,
@Inject.Param(valueToken) public readonly value2 : string // valueToken is Symbol("TEST_TOKEN")
) {
expect(value1).toBe("aval");
expect(value2).toBe("aval");
}
}
Factories example
const valueToken = Injectable(Symbol("TEST_TOKEN"), {
factory: () => "aval" // just a provider function, without additional dependencies
});
const factoryToken = Injectable(Symbol("SECOND_TOKEN"), {
dependencies: [valueToken], // this factory relies on other dependencies
factory: (val: string) => val + "_bval"
});
Factories from class methods.
All classes, that can product injectables must be annotated with @Configuration()
Class can expose outside a static method, which will be used to generate injectable - this will work as regular factory method.
Or class instance can be instantiated/constructed and his regular methods (non-static, in order to access inner state) can be used.
Class, annotated with @Configuration()
decorator can be created only once (singleton). Exception will raise on attempt to create second instance.
@Configuration()
class Config {
constructor(private readonly prefix : string = "pref_") {}
@Injectable(ServiceA)
public createServiceA() : ServiceA {
return new ServiceA(this.prefix + "a");
}
@Injectable(ServiceB)
public static createServiceB(@Inject.Param(ServiceA) a: ServiceA) : ServiceB {
return new ServiceB(a);
}
}
new Config(); // is enough if constructor doesn't need dependencies
// OR
Injector.getCurrentInjector().createInstance(Config);
Injectors hierarchy example
There is always Injector.root
built-in injector, which is global and default if you don't specify else.
When you need create new injector, you do const injector = new Injector("name", Injector.root)
if you want to create child injector
or const injector = new Injector("name")
if you need completely detached injector.
All tokens and instances, which are knowns in parent - will be visible for child injector.
If injector was asked to create instance for some token - it will immediately return ready-to-use instance, if such exists in internal cache.
If doesn't exists - we look for definition, if current injector can construct new instance. If can - constructs, if cannot - delegates the call to parent injector.
This way you can override tokens as:
const TOKEN = Symbol("TOKEN");
Injectable(TOKEN, {
value: "rootVal",
injector: Injector.root
});
Injectable(TOKEN, {
value: "childVal",
injector: testInjector
});
Inject(TOKEN, Injector.root); // = "rootVal"
Inject(TOKEN, testInjector); // = "childVal"
Injector context example
When defining injectable via Injectable
function you can optionally pass parameters as second argument.
const TOKEN1 = Injectable(Symbol("TOKEN1"), {
value: "aval",
injector: customInjector
});
@Injectable(undefined, { // first argument is always `token`, if you want treat current class as token itself - pass `undefined`
injector: customInjector
})
class SomeServiceImpl {}
Or there is another alternative- use injector.runInContext
injector.runInContext(() => {
// do work
});
this will temporary bind injector
- from which you called runInContext
as currentInjector, which will be available via Injector.getCurrentInjector()
call.
For more examples - look at tests
token redirect
When want/need re-use another token under different name.
const TOKEN = Symbol("TOKEN");
const REDIRECT_TOKEN = Symbol("REDIRECT_TOKEN");
Injectable(REDIRECT_TOKEN, {
redirect: TOKEN
});
@Injectable(TOKEN)
class SimpleService {
public a = "aval";
}
class WithDependencies {
public readonly simple : SimpleService = Inject(TOKEN);
public readonly simple2 : SimpleService = Inject(REDIRECT_TOKEN);
}
multiple tokens
Watch options {multi: true}
- if specified - all created instances will be returned inside array, even if there is only one instance.
When not passing multi: true
(so you expect only one instance), but there are many inject candidates - exception will be thrown in runtime.
const TEST_TOKEN = Symbol("TEST_TOKEN");
Injectable(TEST_TOKEN, {
value: "aval",
multi: true
});
Injectable(TEST_TOKEN, {
value: "bval",
multi: true
});
class WithDependencies {
constructor(
@Inject.Param(TEST_TOKEN, {multi: true}) public readonly values : string[]
) {...}
}
multiple tokens - service handlers use-case
Want to have set of services/handlers (hightly lickely with common interface), which can be injectables, which can be used separately.
But, additionall, can be collected somewhere in single array.
interface Handler {
handle() : boolean;
}
Injectable("magicHandler", { // common token, which is multi
redirect: new ForwardRef(() => ServiceHandlerA),
multi: true
});
@Injectable() // individual injectable
class ServiceHandlerA implements Handler {
public handle() : boolean {
return true;
}
}
Injectable("magicHandler", {
redirect: new ForwardRef(() => ServiceHandlerA),
multi: true
});
@Injectable()
class ServiceHandlerB implements Handler {
public handle() : boolean {
return false;
}
}
class WithDependencies {
constructor(
@Inject.Param("magicHandler", {multi: true}) public readonly handlers : Handler[]
) {...}
}
scopes
Prototype
class SimpleService {}
Injectable("TEST_TOKEN", {
scope: "prototype",
factory: () => new SimpleService()
});
const service1 : SimpleService = Inject("TEST_TOKEN");
const service2 : SimpleService = Inject("TEST_TOKEN"); // prototype --> each time new instance
Caching
class SimpleService {}
let currentSessionId : string = "1"; // or requestId, or traceId, or whateverIdentifier
Injectable("TEST_TOKEN", {
scope: () => currentSessionId,
factory: () => new SimpleService()
});
const service1 : SimpleService = Inject("TEST_TOKEN");
const service2 : SimpleService = Inject("TEST_TOKEN"); // the same service should arrive, as currentSessionId unchanged
currentSessionId = "2"; // new request, or session, or something
const service3 : SimpleService = Inject("TEST_TOKEN");
currentSessionId = "1";
const service4 : SimpleService = Inject("TEST_TOKEN"); // reuse previous
License MIT