Code Monkey home page Code Monkey logo

typed-inject's Introduction

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 <4.5 there is a small chance that the compiler doesn't catch all errors (as well as you might experience some performance issues).

Note: projects must enable --strictFunctionTypes (or --strict) in their Typescript config or some type errors may not be caught.

๐ŸŽ 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 (except 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'] as const;

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<void> {
    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<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 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 by populating its constructor arguments from the injector and returns it.

Basically it is a shortcut for resolving values from the injector and creating a new instance with those values:

const logger = appInjector.resolve('logger');
const httpClient = appInjector.resolve('httpClient');
const service = new MyService(httpClient, logger);

Any instance created with injectClass will not be disposed when dispose is called. It is the caller's responsiblity to dispose 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 method injects the function with requested tokens from the injector, invokes it and returns the result.

It is a shortcut for calling the provided function with the values from the injector.

const logger = appInjector.resolve('logger');
const httpClient = appInjector.resolve('httpClient');
const request = doRequest(httpClient, logger);

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<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 = ['foo'] as const;
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 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<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.

injector.dispose(): Promise<void>

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

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.

typed-inject's People

Contributors

ardordeosis avatar huan avatar kyle-johnson avatar nicojs avatar snnsnn avatar tynor avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

typed-inject's Issues

Async factories

Did you consider adding support for async factories?

This is how an async factory might be used:

class Server {
    public async init() {
    }
}

async function serverFactory() {
    const server = new Server();
    await server.init();
    return server;
}

class App {
    public static inject = ['server'];
    constructor(private server: Server) {
        // server is already initialized
   }
}

const injector = createInjector().provideFactory('server', serverFactory);
const app = await injector.injectClass(App);

Disposed children retained by parent

Hello,

First of all, I really like the premise and API of this package.

The pattern I use DI for is to have a base container for the application, then create a child container for a transaction, so all of those values will share the transaction state (current HTTP request, session, DB transaction, etc).

One thing I noticed when looking at the code, it seems like children are only removed from the childInjectors array when their parent is disposed.

Is this behavior intentional? It seems like it would make the injector used to create each transaction injector "collect" children as the program executes until memory is exhausted.

This little demo script demonstrates the behavior. I would expect an output of 0, but instead get 2.

import { createInjector } from 'typed-inject';

const root = createInjector();
[root.provideValue('a', 'a'), root.provideValue('a', 'a')]
  .reduce((p, x) => p.then(() => x.dispose()), Promise.resolve())
  .then(() => {
    console.log((root as any).childInjectors.length);
  });

Difference between resolve and injectClass

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

class FooFake extends Foo {
    override bar() {
        console.log('Foo what?')
    }
}

class Baz {
    static inject = ['foo'] as const
    constructor(protected readonly foo: Foo) {}

    boo() {
        this.foo.bar()
    }
}

const appInjector = createInjector().provideClass('foo', Foo).provideClass('baz', Baz).provideClass('foo', FooFake)
const baz = appInjector.resolve('baz')
const baz2 = appInjector.injectClass(Baz)

baz.boo() // prints 'Foobar!'
baz2.boo() // prints 'Foo what?' (expected)

Is this difference in behavior intentional? Makes it really hard to consistently overwrite dependencies for testing etc. Always using injectClass by convention would be an option, but in order to get an instance of the overwritten class I'd have to use resolve('foo').
Or is it possible to somehow bind FooFake as Foo?

Combination of 20 provideValue()/provideFactory() calls make vscode unusable

Firstly, I just want to say what a great concept this API is, very clean and easy to use, and that I really would like to use in my code base compared to the other typescript DI frameworks out there.

But as I have been adding more factories vscode has become unusable, when I make a change it will take 10 seconds for the intellisense to catch up (and my fans go on lol). I started out with only 5 .provideFactory()s and everything was fine, but I ported more of my code over and things started to get slow. Now it's clear when I comment out the new .provideFactory()s vscode will start behaving again. Right now I have a total of 4 provideValue() and 15 provideFactory() calls. If I had to guess there is some kind of extra work in tsc because the builder pattern might be nesting copies of same type over an over again in the generics.

There was another issue a few years ago that asked for a non-chained version of the API, I'm not sure if something like that would help in this situation #22

Typescript version I am using is is 4.5.5, vscode typescript default of 4.6.3 does not seem to change anything.

Question: IOC container in a react project

I am fairly new to dependency injection and IOC containers.
I find typed-inject's API much friendlier than similar libraries.
I am trying to make my react project work with typescript and an IOC container using typed-inject.
Has anybody ever done something similar? I would be interested to see any kind of examples. ๐Ÿ™

Allow multiple tokens for the same value

It would be useful if the provide... functions would accept multiple tokens. This would be useful for describing interfaces that are implemented in an aggregate concretion, for example:

interface GetThing { get(id: string) : Thing }
interface PutThing { put(thing: Thing): void }

class ThingRepository implements GetThing, PutThing {
  get(id:string) { return 'thing' }
  put(thing: Thing) { }
}

const injector = createInjector().provideClass(['GetThing','PutThing', 'ThingRepository'], ThingRepository)

In this way, consumers can specify the interface they want, and get a concretion which provides that, even if it provides other things we don't care about.

At present we can specify multiple provide... calls, but in this case, the instance/value will be created for each token specified by a consumer, where we might actually intended to have a singleton or transient scope. Whilst it would be possible to provide a factory that deals with this, its somewhat obtuse and repetitive if we want to use the pattern extensively.

Support for high order functions

i set up my project using higher-order functions for the dependencies similar to: https://medium.com/geekculture/dependency-injection-in-javascript-2d2e4ad9df49

// application/ports.ts
export interface Logger {
  log(msg: string): void;
}

// application/fooFunction.ts
interface Dependencies {
  logger: Logger;
}

export const makeFooFunction({ logger }: Dependencies ) = (foo: string) => {
   // foo bar
   logger.log("bung" + foo)
}

i was wondering if this higher-order pattern could technically also be implemented / be used for di with this project (if yes maybe i can build it myself but idk)

(fx. by setting makeFooFunction.inject = ['logger'])

Question: How to get an instance of a class only from its token?

I have code that runs in a pipeline and I want to use injection to provide an instance of 1 class for local dev, another for unit tests and the "real" one for production.

  1. Create the injector: jovo.$injector = createInjector();
  2. In code that only runs during dev, provide the class jovo.$injector.provideClass('myService', ServiceDev);
  3. ServiceDev class implements ServiceInterface. Other classes ServiceTest, ServiceProd.
  4. Get an instance of the class and assign to interface: const myService = jovo.$injector.injectClass('myService')
  5. Currently ServiceDev has no ctor params to inject. Other classes will.

Add a way to dispose all child injectors

Until now, a Injector does not store references to its child instances. We might want to add support for that in order to support more complex scenario's where you would want to dispose the entire dependency injection tree all at once.

An example from the Stryker source code:

    let mutantInstrumenterInjector: Injector<MutantInstrumenterContext> | undefined;
    let dryRunExecutorInjector: Injector<DryRunContext> | undefined;
    let mutationRunExecutorInjector: Injector<MutationTestContext> | undefined;
    try {
      // 1. Prepare. Load Stryker configuration, load the input files and starts the logging server
      const prepareExecutor = this.injector.provideValue(coreTokens.cliOptions, this.cliOptions).injectClass(PrepareExecutor);
      mutantInstrumenterInjector = await prepareExecutor.execute();

      // 2. Mutate and instrument the files and write to the sandbox.
      const mutantInstrumenter = mutantInstrumenterInjector.injectClass(MutantInstrumenterExecutor);
      dryRunExecutorInjector = await mutantInstrumenter.execute();

      // 3. Perform a 'dry run' (initial test run). Runs the tests without active mutants and collects coverage.
      const dryRunExecutor = dryRunExecutorInjector.injectClass(DryRunExecutor);
      mutationRunExecutorInjector = await dryRunExecutor.execute();

      // 4. Actual mutation testing. Will check every mutant and if valid run it in an available test runner.
      const mutationRunExecutor = mutationRunExecutorInjector.injectClass(MutationTestExecutor);
      const mutantResults = await mutationRunExecutor.execute();
      return mutantResults;
    } finally {
      if (mutationRunExecutorInjector) {
        await mutationRunExecutorInjector.dispose();
      } else if (dryRunExecutorInjector) {
        await dryRunExecutorInjector.dispose();
      } else if (mutantInstrumenterInjector) {
        await mutantInstrumenterInjector.dispose();
      }
    }

We provide the Injector in each executor which builds upon it and returns it. In the finally clause, where we do clean up, we need the reference of the deepest child injector, since that would dispose of other injectors.

Suggestion:

   const rootInjector = new RootInjector();
    try {
      // 1. Prepare. Load Stryker configuration, load the input files and starts the logging server
      const prepareExecutor = rootInjector.provideValue(coreTokens.cliOptions, this.cliOptions).injectClass(PrepareExecutor);
      const mutantInstrumenterInjector = await prepareExecutor.execute();

      // 2. Mutate and instrument the files and write to the sandbox.
      const mutantInstrumenter = mutantInstrumenterInjector.injectClass(MutantInstrumenterExecutor);
      const dryRunExecutorInjector = await mutantInstrumenter.execute();

      // 3. Perform a 'dry run' (initial test run). Runs the tests without active mutants and collects coverage.
      const dryRunExecutor = dryRunExecutorInjector.injectClass(DryRunExecutor);
      cosnt mutationRunExecutorInjector = await dryRunExecutor.execute();

      // 4. Actual mutation testing. Will check every mutant and if valid run it in an available test runner.
      const mutationRunExecutor = mutationRunExecutorInjector.injectClass(MutationTestExecutor);
      const mutantResults = await mutationRunExecutor.execute();
      return mutantResults;
    } finally {
       await rootInjector.dispose();
    }

Question: [typed-inject in Clean Architecture] How to inject instances so that other registered instances can reference them while being used as injectables themselves by other instances?

Hi! First of all, I really appreciate this project and it's type-safe DI approach to Typescript. However, while implementing a little demonstration project with a Clean Architecture approach, I could not figure out how to correctly set up my IoC container.

My question sounds a bit long-winded, but let my try to describe my approach.

Short description of my object relation / call flow:
UI/reducer -> Use-Case -> Service (impl. by a Repository) -> Datasource -> API client
To minimize the amount dependencies, increase performance and overall be more flexible/adaptable, in my UI, I want to get the injected Use-Case instance, which in itself references a Service registered in the IoC and so on...

Now, to my approach. I started by implementing my API client:

export interface ApiClient {
    get(url: string): Promise<any>;

    post(url: string, body: {},
         contentType: RequestContentType,
         authenticated: boolean): Promise<any>;
}

export class BlueprintOffersApiClient implements ApiClient {
    async get(url: string) {
       ...
    }

    async post(url: string, body: {},
               contentType: RequestContentType = RequestContentType.json,
               authenticated: boolean = true) {
        ...
    }

    private static async getAccessToken(): Promise<string> {
        ...
    }
}

Next, my API Datasource of my Account module:

export interface IAccountApiDatasource {
    getUserProfile(id: string): Promise<UserProfile>;

    directToSignIn(codeChallenge: string): void;

    authenticate(codeVerifier: string, authorizationCode: string, scope: string): Promise<Credentials>;

    directToSignOut(idToken: string): void;
}

export class AccountApiDatasource implements IAccountApiDatasource {
    public static inject = [Types.apiClient] as const;

    constructor(private readonly apiClient: ApiClient
    ) {
    }

    async getUserProfile(id: string): Promise<UserProfile> {
       ...
    }

    async authenticate(codeVerifier: string, authorizationCode: string, scope: string): Promise<Credentials> {
        ...
    }

    directToSignOut(idToken: string): void {
        ...
    }

    directToSignIn(codeChallenge: string): void {
        ...
    }
}

In my Account module, I have two other Datasource NOT referencing the API client or any other thing:

export interface IAccountLocalDatasource {
    ...
}

export class AccountLocalDatasource implements IAccountLocalDatasource {
    ...
}
export interface IAccountSessionDatasource {
    ...
}

export class AccountSessionDatasource implements IAccountSessionDatasource {
    ...
}

The Datasources are used in the module's Repositories (Service implementations):

export interface AuthenticationService {
    signIn(): void;

    authenticate(): Promise<void>;

    signOut(): void;
}
export class AuthenticationRepository implements AuthenticationService {
    public static inject = [Types.iAccountApiDatasource, Types.iAccountLocalDatasource, Types.iAccountSessionDatasource] as const;

    constructor(private readonly iAccountApiDatasource: IAccountApiDatasource,
                private readonly iAccountLocalDatasource: IAccountLocalDatasource,
                private readonly iAccountSessionDatasource: IAccountSessionDatasource) {
    }

    async authenticate(): Promise<void> {
       ...
    }

    signOut(): void {
        ...
    }

    signIn(): void {
        ...
    }
}

The Use-Cases then reference the Services:

export class SignIn {
    public static inject = [Types.authenticationService] as const;

    constructor(private readonly authenticationService: AuthenticationService) {
    }

    async invoke() : Promise<void> {
        return this.authenticationService.signIn();
    }
}

Ok, so far so good. Now I want to set up my IoC:

export class Types {
    // Clients
    static apiClient = "apiClient";

    // Data sources
    static iAccountApiDatasource = "iAccountApiDatasource";
    static iAccountLocalDatasource = "iAccountLocalDatasource";
    static iAccountSessionDatasource = "iAccountSessionDatasource";
    ...

    // Services
    static authenticationService = "authenticationService";
   ...

    // Use cases
    static signIn = "signIn";
    ...
}

// Called in index.tsx
export function initializeDependencies() {
    return createInjector().provideClass(Types.apiClient, BlueprintOffersApiClient, Scope.Singleton)
        .provideClass(Types.iAccountApiDatasource, AccountApiDatasource, Scope.Singleton)
        .provideClass(Types.iAccountLocalDatasource, AccountLocalDatasource, Scope.Singleton)
        .provideClass(Types.iAccountSessionDatasource, AccountSessionDatasource, Scope.Singleton)
        .provideClass(Types.authenticationService, AuthenticationRepository, Scope.Singleton)
        .provideClass(Types.signIn, SignIn, Scope.Singleton);
}

But with this setup, the compiler displays the following error in the line providing the AuthenticationRepository:
TS2345: Argument of type 'typeof AuthenticationRepository' is not assignable to parameter of type 'InjectableClass {}, BlueprintOffersApiClient, string>, AccountApiDatasource, string>, AccountLocalDatasource, string>, AccountSessionDatasource, string>, AuthenticationRepository, readonly [...]>'.

So, to get back to my (multipart) question:

  1. What is the error trying to tell me and and why does it occur - I really want to understand it
  2. How can I inject my API client(s), Datasources, Repositories and Use-Cases instances so that other registered instances can reference them while being used as injectables themselves by "higher" (closer to UI) instances? - But I also need a solution:D

Get the token of the target

Very cool library, type safety is especially nice!

We currently use inversify, and in toDynamicValue you get Context as an argument. In the context you can look up the token of the target where the current value is injected into. We use that for logging purposes.

With the magic token TARGET_TOKEN I am able to get the name of the constructor that requested this value. But I also would like the token, as we can give some more context in the token than the class name.

Is there way this is currently possible?

Minimal example:

function TestFactory (target: Function | undefined) {  
    // Here I would like to get access to the token of the target.
    console.log(target) // [Function: MagicTokenTest] { inject: [ 'TestFactory' ] }
    return 'hello, world!'
}
TestFactory.inject = [TARGET_TOKEN] as const

class MagicTokenTest {
    public static inject = ['TestFactory'] as const
    constructor(testFactory: string) {}
}

const appInjector = createInjector()
    .provideFactory('TestFactory', TestFactory, Scope.Transient)
    .provideClass('token.i.use.for.logging', MagicTokenTest);

appInjector.resolve('token.i.use.for.logging');

Support TS 3.8 with full type safety

Due to microsoft/TypeScript#37400 it is allowed to assign a parent injector to a child injector.

const fooInjector: Injector<{foo: string}> = rootInjector;
// ERROR in TS <3.8
// OK According to TS >= 3.8

fooInjector.resolve('foo'); // Breaks at runtime!!

I'll close this issue once it's fixed at TS side.

enhance needed: optional

@nicojs you did a great job ๐Ÿ‘!

I try to replace inversify with typed-inject, but it's failed. Because the optional injection is missed.

I think we could provide a API like provideOptional(name:string) which could still take advantage of TContext, or maybe we could just pass undefined into provideValue, provideClass, provideFactory. And it might be good for semantics that a function named optional for token.

Support `as const` syntax for injection tokens

Typescript 3.4 added a language feature to turn array literals into narrowly typed readonly tuples using the as const type assertion. It would improve the experience of typed-inject if we could use this syntax instead of having to import and use the tokens() helper function to specify dependencies. Right now it does not work because readonly arrays cannot be assigned to mutable arrays (which inject is assumed to be), so we get a type error in the provideXX() or injectXX() call.

As mentioned by @nicojs this would be a possible feature for a new (major) release.

Feature request: give access to the root injector

It would be nice to have access to the root injector via injector.root to later call dispose() on it.

Now I have to maintain two references to achieve disposability i.e. root.dispose():

  1. Root injector: const root = createInjector()
  2. Fully populated injector: const injector = root.provideClass('foo', Foo).provideClass('blah', Blah)

It would be cool if I could maintain just a single reference to later do this:

const injector = createInjector().provideClass('foo', Foo).provideClass('blah', Blah)
injector.root.dispose()

Typescript improvments... :(

Hi!

I've learnt a tonne working with this library. I'm going to see if can pull together a pull request if work allows but just to let you know if you didn't already - 4.8.x is hurting, right on the line where it takes an equality eval of the search token vs the current token. For now 4.7.x is fine.

Take it easy.

Use of mutation testing in typed-inject - Help needed

Hello there!

My name is Ana. I noted that you use the mutation testing tool in the project.
I am a postdoctoral researcher at the University of Seville (Spain), and my colleagues and I are studying how mutation testing tools are used in practice. With this aim in mind, we have analysed over 3,500 public GitHub repositories using mutation testing tools, including yours! This work has recently been published in a journal paper available at https://link.springer.com/content/pdf/10.1007/s10664-022-10177-8.pdf.

To complete this study, we are asking for your help to understand better how mutation testing is used in practice, please! We would be extremely grateful if you could contribute to this study by answering a brief survey of 21 simple questions (no more than 6 minutes). This is the link to the questionnaire https://forms.gle/FvXNrimWAsJYC1zB9.

Drop me an e-mail if you have any questions or comments ([email protected]). Thank you very much in advance!!

Can't provide a class before its dependencies

Take a look at this example code. With other IoC containers this would be perfectly valid, but in typed-inject this is an error.

class Foo {
  static inject = tokens('bar', 'baz');

  constructor(bar: string, baz: string) {}
}

const foo = rootInjector
  .provideValue('bar', 'bar value')
  .provideClass('foo', Foo)
  .provideValue('baz', 'baz value')
  .resolve('foo');

Currently this is considered an error at the provideClass call because the dependencies for that class are not satisfied. However, in my opinion, it should not check for the dependencies until you try to resolve it. Of course, the following should result in a compilation error:

class Foo {
  static inject = tokens('bar', 'baz');

  constructor(bar: string, baz: string) {}
}

const foo = rootInjector
  .provideValue('bar', 'bar value')
  .provideClass('foo', Foo)
  .resolve('foo');

However, I think the error here is with the resolve call, not the provideClass call. It's perfectly fine that you want to specify which class should be used when the 'foo' token is requested. However, it is not okay to try to resolve a 'foo' before all its dependencies are satisfied.

Take a look at this example of InversifyJS code. The Warrior is bound before its dependencies, Weapon and ThrowableWeapon. Now of course InversifyJS is not type safe. But after some experimentation, I'm quite confident that it would be possible to achieve this behavior while retaining 100% type safety and with the errors all being compilation errors, not runtime errors.

One major difference between Inversify and typed-inject is that typed-inject actually creates a new injector every time something is provided. I personally love this about typed-inject. However, there are some interesting implications for providing classes before their dependencies.

Take a look at this example:

const injector = rootInjector.provideClass('foo', Foo);

It will now be impossible for injector to ever resolve 'foo'. And if you were to later on provide the dependencies for 'foo', 'bar' and 'baz'. It would still be impossible for this injector instance to ever resolve 'foo'. Only the new child injector would be able to resolve 'foo'.

To me, this is not a bug, it's a feature. This is still a useful state for an injector to be in. For example, you could pass injector to some sub-module, and if the sub-module should want an instance of 'foo', it just needs to provide two strings. It does not need to know what specific implementation of 'foo' to use. This allows sub-modules of your app to provide configuration for certain services, without coupling those sub-modules to any specific implementation of those services.

Think about it as if the token has three states in the injector

  1. Unknown: The token has never been provided. The injector doesn't know it exists.
  2. Bound: The token has been bound to a certain class or factory. However, the injector is not capable of resolving this token because some of its dependencies have not been provided. The token will implicitly transition to the provided state in any child injectors where the dependencies are satisfied.
  3. Provided: All the dependencies of the token are satisfied, and the injector is capable of resolving it

To avoid changing the behavior of existing methods out from under people, new methods could be written that have this new behavior, bindFactory and bindClass. That way provide would still cause compilation errors if the dependencies are not satisfied, but if you want to specify which class should be used for a token before the dependencies are determined, you can use bindClass.

To further illustrate how these different states work take a look at this example.

const injector = rootInjector.bindClass('foo', Foo);

const childInjectorA = injector
  .provideValue('bar', 'bar value A');
  .provideValue('baz', 'baz value A');

const childInjectorB = injector
  .provideValue('bar', 'bar value B');
  .provideValue('baz', 'baz value B');

const fooA = childInjectorA.resolve('foo');

const fooB = childInjectorB.resolve('foo');

fooA and fooB are different instances of Foo. This is because while 'foo' was bound all the way up in injector it wasn't actually provided until the child injectors satisfied the dependencies.

I would be happy to attempt to write a PR for this myself, but first I wanted to get your feedback, Nico. Do you think this would be a good improvement to typed-inject?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.