Package Exports
- typed-inject
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (typed-inject) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Typed Inject
Type safe dependency injection for TypeScript
A tiny, 100% type safe dependency injection framework for TypeScript. You can inject classes, interfaces or primitives. If your project compiles, you know for sure your dependencies are resolved at runtime and have their declared types.
If you are new to 'Dependency Injection'/'Inversion of control', please read up on it in this blog article about it
If you want to know more about how typed-inject works, please read my blog article about it
πΊοΈ Installation
Install typed-inject locally within your project folder, like so:
npm i typed-injectOr with yarn:
yarn add typed-injectNote: this package uses advanced TypeScript features. Only TS 3.0 and above is supported!
π Usage
An example:
import { rootInjector, tokens } from 'typed-inject';
interface Logger {
info(message: string): void;
}
const logger: Logger = {
info(message: string) {
console.log(message);
}
};
class HttpClient {
constructor(private log: Logger) { }
public static inject = tokens('logger');
}
class MyService {
constructor(private http: HttpClient, private log: Logger) { }
public static inject = tokens('httpClient', 'logger');
}
const appInjector = rootInjector
.provideValue('logger', logger)
.provideClass('httpClient', HttpClient);
const myService = appInjector.injectClass(MyService);
// Dependencies for MyService validated and injectedIn this example:
- The
loggeris injected into a new instance ofHttpClientby value. - The instance of
HttpClientand theloggerare injected into a new instance ofMyService.
Dependencies are resolved using the static inject property on their classes. They must match the names given to the dependencies when configuring the injector with provideXXX methods.
Expect compiler errors when you mess up the order of tokens or forget it completely.
import { rootInjector, tokens } from 'typed-inject';
// Same logger as before
class HttpClient {
constructor(private log: Logger) { }
// ERROR! Property 'inject' is missing in type 'typeof HttpClient' but required
}
class MyService {
constructor(private http: HttpClient, private log: Logger) { }
public static inject = tokens('logger', 'httpClient');
// ERROR! Types of parameters 'http' and 'args_0' are incompatible
}
const appInjector = rootInjector
.provideValue('logger', logger)
.provideClass('httpClient', HttpClient);
const myService = appInjector.injectClass(MyService);The error messages are a bit cryptic at times, but it sure is better than running into them at runtime.
β¨ Magic tokens
Any Injector instance can always inject the following tokens:
| Token name | Token value | Description |
|---|---|---|
INJECTOR_TOKEN |
'$injector' |
Injects the current injector |
TARGET_TOKEN |
'$target' |
The class or function in which the current values is injected, or undefined if resolved directly |
An example:
import { rootInjector, Injector, tokens, TARGET_TOKEN, INJECTOR_TOKEN } from 'typed-inject';
class Foo {
constructor(injector: Injector<{}>, target: Function | undefined) {}
static inject = tokens(INJECTOR_TOKEN, TARGET_TOKEN);
}
const foo = rootInjector.inject(Foo);π Motivation
JavaScript and TypeScript development already has a great dependency injection solution with InversifyJS. However, InversifyJS comes with 2 caveats.
InversifyJS uses Reflect-metadata
InversifyJS works with a nice API using decorators. Decorators is in Stage 2 of ecma script proposal at the moment of writing this, so will most likely land in ESNext. However, it also is opinionated in that it requires you to use reflect-metadata, which is supposed to be an ecma script proposal, but isn't yet (at the moment of writing this). It might take years for reflect-metadata to land in Ecma script, if it ever does.
InversifyJS is not type-safe
InversifyJS is also not type-safe. There is no check to see of the injected type is actually injectable or that the corresponding type adheres to the expected type.
ποΈ Type safe? How?
Type safe dependency injection works by combining awesome TypeScript features. Some of those features are:
Please read my blog article on Medium if you want to know how this works.
π API reference
Note: some generic parameters are omitted for clarity.
Injector<TContext>
The Injector<TContext> is the core interface of typed-inject. It provides the ability to inject your class or function with injectClass and injectFunction respectively. You can create new child injectors from it using the provideXXX methods.
The TContext generic arguments is a lookup type. The keys in this type are the tokens that can be injected, the values are the exact types of those tokens. For example, if TContext extends { foo: string, bar: number }, you can let a token 'foo' be injected of type string, and a token 'bar' of type number.
Typed inject comes with only one implementation. The rootInjector. It implements Injector<{}> interface, meaning that it does not provide any tokens (except for magic tokens) Import it with import { rootInjector } from 'typed-inject'. From the rootInjector, you can create child injectors.
Don't worry about reusing the rootInjector in your application. It is stateless and read-only, so safe for concurrent use.
injector.injectClass(injectable: InjectableClass)
This method creates a new instance of class injectable and returns it.
When there are any problems in the dependency graph, it gives a compiler error.
class Foo {
constructor(bar: number) { }
static inject = tokens('bar');
}
const foo /*: Foo*/ = injector.injectClass(Foo);injector.injectFunction(fn: InjectableFunction)
This methods injects the function with requested tokens and returns the return value of the function. When there are any problems in the dependency graph, it gives a compiler error.
function foo(bar: number) {
return bar + 1;
}
foo.inject = tokens('bar');
const baz /*: number*/ = injector.injectFunction(Foo);injector.resolve(token: Token): CorrespondingType<TContext, Token>
The resolve method lets you resolve tokens by hand.
const foo = injector.resolve('foo');
// Equivalent to:
function retrieveFoo(foo: number){
return foo;
}
retrieveFoo.inject = tokens('foo');
const foo2 = injector.injectFunction(retrieveFoo);injector.provideValue(token: Token, value: R): Injector<ChildContext<TContext, Token, R>>
Create a child injector that can provide value value for token 'token'. The new child injector can resolve all tokens the parent injector can as well as 'token'.
const fooInjector = injector.provideValue('foo', 42);injector.provideFactory(token: Token, factory: InjectableFunction<TContext>, scope = Scope.Singleton): Injector<ChildContext<TContext, Token, R>>
Create a child injector that can provide a value using factory for token 'token'. The new child injector can resolve all tokens the parent injector can, as well as the new 'token'.
With scope you can decide whether the value must be cached after the factory is invoked once. Use Scope.Singleton to enable caching (default), or Scope.Transient to disable caching.
const fooInjector = injector.provideFactory('foo', () => 42);
function loggerFactory(target: Function | undefined) {
return new Logger((target && target.name) || '');
}
loggerFactory.inject = tokens(TARGET_TOKEN);
const fooBarInjector = fooInjector.provideFactory('logger', loggerFactory, Scope.Transient)injector.provideFactory(token: Token, Class: InjectableClass<TContext>, scope = Scope.Singleton): Injector<ChildContext<TContext, Token, R>>
Create a child injector that can provide a value using instances of Class for token 'token'. The new child injector can resolve all tokens the parent injector can, as well as the new 'token'.
Scope is also supported here, for more info, see provideFactory.
Scope
The Scope enum indicates the scope of a provided injectable (class or factory). Possible values: Scope.Transient (new injection per resolve) or Scope.Singleton (inject once, and reuse values). It generally defaults to Singleton.
tokens
The tokens function is a simple helper method that makes sure that an inject array is filled with a tuple type filled with literal strings.
const inject = tokens('foo', 'bar');
// Equivalent to:
const inject: ['foo', 'bar'] = ['foo', 'bar'].Note: hopefully TypeScript will introduce explicit tuple syntax, so this helper method can be removed
InjectableClass<TContext, R, Tokens extends InjectionToken<TContext>[]>
The InjectableClass interface is used to identify the (static) interface of classes that can be injected. It is defined as follows:
{
new(...args: CorrespondingTypes<TContext, Tokens>): R;
readonly inject: Tokens;
}In other words, it makes sure that the inject tokens is corresponding with the constructor types.
InjectableFunction<TContext, R, Tokens extends InjectionToken<TContext>[]>
Comparable to InjectableClass, but for (non-constructor) functions.
π€ Commendation
This entire framework would not be possible without the awesome guys working on TypeScript. Guys like Ryan, Anders and the rest of the team: a heartfelt thanks! π
Inspiration for the API with static inject method comes from years long AngularJS development. Special thanks to the Angular team.