Need help with typed-inject?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

nicojs
130 Stars 3 Forks Apache License 2.0 52 Commits 8 Opened issues

Description

Type safe dependency injection for TypeScript

Services available

!
?

Need anything else?

Contributors list

# 60,478
Shell
TypeScr...
mutatio...
dotnet-...
28 commits

Mutation testing badge Build Status NPM Node version Gitter

Typed Inject

Typesafe dependency injection for TypeScript

A tiny, 100% typesafe dependency injection framework for TypeScript. You can inject classes, interfaces, or primitives. If your project compiles, you know 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-inject

Or with yarn:

yarn add typed-inject

Note: this package uses advanced TypeScript features. Only TS 3.0 and above is supported!

Note: due to a bug in TypeScript 3.8 there is a small chance that the compiler doesn't catch all errors (as well as you might experience some performance issues).

🎁 Usage

An example:

import { createInjector } 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 = ['logger'] as const; }

class MyService { constructor(private http: HttpClient, private log: Logger) {} public static inject = ['httpClient', 'logger'] as const; }

const appInjector = createInjector().provideValue('logger', logger).provideClass('httpClient', HttpClient);

const myService = appInjector.injectClass(MyService); // Dependencies for MyService validated and injected

In this example:

  • The
    logger
    is injected into a new instance of
    HttpClient
    by value.
  • The instance of
    HttpClient
    and the
    logger
    are injected into a new instance of
    MyService
    .

Dependencies are resolved using the static

inject
property in 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 { createInjector } 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 = ['logger', 'httpClient'] as const; // ERROR! Types of parameters 'http' and 'args_0' are incompatible }

const appInjector = createInjector().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.

💭 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 are in Stage 2 of ecma script proposal at the moment of writing this, so they 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 JavaScript, if it ever does.

InversifyJS is not typesafe

InversifyJS is also not typesafe. There is no check to see of the injected type is actually injectable or that the corresponding type adheres to the expected type.

🗝️ Typesafe? How?

Type safe dependency injection works by combining excellent TypeScript features. Some of those features are:

Please read my blog article on Medium if you want to know how this works.

👶 Child injectors

The

Injector
interface is responsible for injecting classes or functions. You start off with an empty injector after calling
createInjector
. It can't provide any dependencies directly (expect for magic tokens).

To do anything useful with your injector, you'll need to create child injectors. This what you do with the

provideXXX
methods.
import { createInjector } from 'typed-inject';
function barFactory(foo: number) {
  return foo + 1;
}
barFactory.inject = ['foo'] as const;
class Baz {
  constructor(bar: number) {
    console.log(`bar is: ${bar}`);
  }
  static inject = ['bar'] as const;
}

// Create 3 child injectors here const childInjector = createInjector() .provideValue('foo', 42) // child injector can provide 'foo' .provideFactory('bar', barFactory) // child injector can provide both 'bar' and 'foo' .provideClass('baz', Baz); // child injector can provide 'baz', 'bar' and 'foo'

// Now use it here function run(baz: Baz) { // baz is created! } run.inject = ['baz'] as const; childInjector.injectFunction(run);

In the example above, a child injector is created. It can provide values for the tokens

'foo'
,
'bar'
and
'baz'
. You can create as many child injectors as you want.

Injectors keep track of their child injectors and values they've injected. This way it can provide functionality like cache the injected value or keep track of stuff to dispose.

🎄 Decorate your dependencies

A common use case for dependency injection is the decorator design pattern. It is used to dynamically add functionality to existing dependencies. Typed inject supports decoration of existing dependencies using its

provideFactory
and
provideClass
methods.
import { createInjector } from 'typed-inject';

class Foo { public bar() { console.log('bar!'); } }

function fooDecorator(foo: Foo) { return { bar() { console.log('before call'); foo.bar(); console.log('after call'); } }; } fooDecorator.inject = ['foo'] as const;

const fooProvider = createInjector().provideClass('foo', Foo).provideFactory('foo', fooDecorator); const foo = fooProvider.resolve('foo');

foo.bar(); // => "before call" // => "bar!" // => "after call"

In this example above the

Foo
class is decorated by the
fooDecorator
.

♻ Lifecycle control

You can determine the lifecycle of dependencies with the third

Scope
parameter of
provideFactory
and
provideClass
methods.
function loggerFactory(target: Function | null) {
  return getLogger((target && target.name) || 'UNKNOWN');
}
loggerFactory.inject('target');
class Foo {
  constructor(public log: Logger) {
    log.info('Foo created');
  }
  static inject = ['log'] as const;
}

const fooProvider = injector.provideFactory('log', loggerFactory, Scope.Transient).provideClass('foo', Foo, Scope.Singleton); const foo = fooProvider.resolve('foo'); const fooCopy = fooProvider.resolve('foo'); const log = fooProvider.resolve('log'); console.log(foo === fooCopy); // => true console.log(log === foo.log); // => false

A scope has 2 possible values.

  • Scope.Singleton
    (default value)
    Use
    Scope.Singleton
    to enable caching. Every time the dependency needs to be provided by the injector, the same instance is returned. Other injectors will still create their own instances, so it's only a
    Singleton
    for the specific injector (and child injectors created from it). In other words, the instance will be scoped to the
    Injector
  • Scope.Transient

    Use
    Scope.Transient
    to altogether disable cashing. You'll always get fresh instances.

🚮 Disposing provided stuff

Memory in JavaScript is garbage collected, so, we usually don't care about cleaning up after ourselves. However, there might be a need to explicit cleanup. For example removing a temp folder, or killing a child process.

As

typed-inject
is responsible for creating (providing) your dependencies, it only makes sense it is also responsible for the disposing of them.

Any

Injector
has a
dispose
method. Calling it will call
dispose
on any instance that was ever provided from it, as well as any child injectors that were created from it.
import { createInjector } from 'typed-inject';

class Foo { constructor() { console.log('Foo created'); } dispose() { console.log('Foo disposed'); } } const rootInjector = createInjector(); const fooProvider = rootInjector.provideClass('foo', Foo); fooProvider.resolve('foo'); // => "Foo created" await rootInjector.dispose(); // => "Foo disposed" fooProvider.resolve('foo'); // Error: Injector already disposed

Note: Always dispose from the top down! In this example, the

rootInjector
is disposed, which in turn disposes everything that was ever provided from one if it's child injectors.

To help you implementing the

dispose
method correctly,
typed-inject
exports the
Disposable
interface for convenience:
import { Disposable } from 'typed-inject';
class Foo implements Disposable {
  dispose() {}
}

Dispose methods are typically

async
. For example, you might need to clean up some files or get rid of a child process. If you do so, your dependencies should return a promise from the
dispose
method. In turn, calling
dispose
on an
Injector
is always async. You are responsible for the correct handling of the async behavior of the
dispose
method. This means you should either
await
the result or attach
then
/
catch
handlers.
import { createInjector, Disposable } from 'typed-inject';
class Foo implements Disposable {
  dispose(): Promise {
    return Promise.resolve();
  }
}
const rootInjector = createInjector();
const fooProvider = rootInjector
  .provideClass('foo', Foo);
const foo = fooProvider.resolve('foo');
async function disposeFoo() {
  await fooProvider.dispose();
}
disposeFoo()
  .then(() => console.log('Foo disposed'))
  .catch(err => console.error('Foo disposal resulted in an error', err);

Using

dispose
on the rootInjector will automatically dispose it's child injectors as well:
import { createInjector } from 'typed-inject';
class Foo {}
class Bar {}
const rootInjector = createInjector();
const fooProvider = rootInjector.provideClass('foo', Foo);
const barProvider = fooProvider.provideClass('bar', Bar);
await rootInjector.dispose(); // => fooProvider is also disposed!
fooProvider.resolve('foo'); // => Error: Injector already disposed

Disposing of provided values is done in order of child first. So they are disposed in the opposite order of respective

providedXXX
calls (like a stack):
import { createInjector } from 'typed-inject';

class Foo { dispose() { console.log('Foo disposed'); } } class Bar { dispose() { console.log('Bar disposed'); } } class Baz { static inject = ['foo', 'bar'] as const; constructor(public foo: Foo, public bar: Bar) {} } const rootInjector = createInjector(); rootInjector .provideClass('foo', Foo) .provideClass('bar', Bar) .injectClass(Baz); await fooProvider.dispose(); // => "Foo disposed" // => "Bar disposed",

Any instance created with

injectClass
or
injectFactory
will not be disposed when
dispose
is called. You were responsible for creating it, so you are also responsible for the disposing of it. In the same vain, anything provided as a value with
providedValue
will also not be disposed when
dispose
is called on it's injector.

✨ Magic tokens

Any

Injector
instance can always provide 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 are injected, or
undefined
if resolved directly |

An example:

import { createInjector, Injector, TARGET_TOKEN, INJECTOR_TOKEN } from 'typed-inject';

class Foo { constructor(injector: Injector, target: Function | undefined) {} static inject = [INJECTOR_TOKEN, TARGET_TOKEN] as const; }

const foo = createInjector().inject(Foo);

😬 Error handling

When a runtime error occurs, typed inject will provide you with the exact path where the error occurred.

class GrandChild {
  public baz = 'baz';
  constructor() {
    throw expectedCause;
  }
}
class Child {
  public bar = 'foo';
  constructor(public grandchild: GrandChild) {}
  public static inject = ['grandChild'] as const;
}
class Parent {
  constructor(public readonly child: Child) {}
  public static inject = ['child'] as const;
}
createInjector()
  .provideClass('grandChild', GrandChild)
  .provideClass('child', Child)
  .injectClass(Parent);
// => Error: Could not inject [class Parent] -> [token "child"] -> [class Child] -> [token "grandChild"] -> [class GrandChild]. Cause: Expected error

When you handle the error, you will be able to capture the original

cause
.
import { InjectionError } from 'typed-inject';
try {
  createInjector()
    .provideClass('grandChild', GrandChild)
    .provideClass('child', Child)
    .injectClass(Parent);
} catch (err) {
  if (err instanceof InjectionError) {
    console.error(err.cause.stack);
  }
}

📖 API reference

Note: some generic parameters are omitted for clarity.

createInjector

Create a new

Injector
. You generally want to create one per application/request. If you're using
typed-inject
also in your unit tests, you probably want to create a fresh one for each test, for example in global test setup.

Injector

The

Injector
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 argument 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. See creating child injectors for more information.

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 = ['bar'] as const;
}
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 = ['bar'] as const;
const baz /*: number*/ = injector.injectFunction(Foo);

injector.resolve(token: Token): CorrespondingType

The

resolve
method lets you resolve tokens by hand.
const foo = injector.resolve('foo');
// Equivalent to:
function retrieveFoo(foo: number) {
  return foo;
}
retrieveFoo.inject = ['foo'] as const;
const foo2 = injector.injectFunction(retrieveFoo);

injector.provideValue(token: Token, value: R): Injector>

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, scope = Scope.Singleton): Injector>

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 and 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 = [TARGET_TOKEN] as const;
const fooBarInjector = fooInjector.provideFactory('logger', loggerFactory, Scope.Transient);

injector.provideFactory(token: Token, Class: InjectableClass, scope = Scope.Singleton): Injector>

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
.

injector.dispose(): Promise

Use

dispose
to explicitly dispose the
injector
. This will result in the following (in order):
  1. Call
    dispose
    on each child injector created from this injector.
  2. It will call
    dispose
    on any dependency created by the injector (if it exists) using
    provideClass
    or
    provideFactory
    (not
    provideValue
    or
    injectXXX
    ).
  3. It will also await any promise that might have been returned by disposable dependencies.

Note: this behavior changed since v2. Before v2, the parent injector was always disposed before the child injector. Note: this behavior changed again in v3, calling

dispose
on a child injector will no longer dispose it's parent injector and instead will dispose it's child injectors. The order of disposal is still child first.

After an injector is disposed, you cannot use it anymore. Any attempt to do so will result in an

InjectorDisposedError
error.

Disposing of your dependencies is always done asynchronously. You should take care to handle this appropriately. The best way to do that is to

await
the result of
myInjector.dispose()
.

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 readonly tuple type filled with literal strings. It is mostly there for backward compatibility reasons, since we can now use
as const
, but one might also simply prefer to use
tokens
instead.
const inject = tokens('foo', 'bar');
// Equivalent to:
const inject = ['foo', 'bar'] as const;

InjectableClass[]>

The

InjectableClass
interface is used to identify the (static) interface of classes that can be injected. It is defined as follows:
{
  new(...args: CorrespondingTypes): R;
  readonly inject: Tokens;
}

In other words, it makes sure that the

inject
tokens is corresponding with the constructor types.

InjectableFunction[]>

Comparable to

InjectableClass
, but for (non-constructor) functions.

Disposable

You can implement the

Disposable
interface in your dependencies. It looks like this:
interface Disposable {
  dispose(): void;
}

With this, you can let the

Injector
call your dispose method.

Note: This is just a convenience interface. Due to TypeScripts structural typing system

typed-inject
calls your
dispose
method without you having to explicitly implement it.

InjectionError

The error class of which instances are thrown when an error occurs during injection or dependency resolving.

An example:

const explosion = new Error('boom!');
class Boom {
  constructor() {
    throw explosion;
  }
}
class Prison {
  constructor(public readonly child: Boom) {}
  public static inject = ['boom'] as const;
}
try {
  rootInjector.provideClass('boom', Boom).injectClass(Prison);
} catch (error) {
  if (error instanceof InjectionError) {
    error.path[0] === Prison;
    error.path[1] === 'boom';
    error.path[2] === Boom;
    error.cause === explosion;
  }
}

InjectionError.path

This will contain the path that was taken to get to the error.

InjectionError.cause

The original cause of the injection error.

🤝 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.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.