Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"typescript"
],
"eslint.validate": ["javascript", "typescript"],
"typescript.tsdk": "node_modules\\typescript\\lib",
"cSpell.words": [
"tseslint",
"Typesafe"
]
"cSpell.words": ["tseslint", "Typesafe"],
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }]
}
110 changes: 75 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,18 @@ _If you are new to 'Dependency Injection'/'Inversion of control', please read up

_If you want to know more about how typed-inject works, please read [my blog article about it](https://medium.com/@jansennico/advanced-typescript-type-safe-dependency-injection-873426e2cc96)_


* [🗺️ Installation](#installation)
* [🎁 Usage](#usage)
* [💭 Motivation](#motivation)
* [🗝️ Typesafe? How?](#typesafe-how)
* [👶 Child injectors](#child-injectors)
* [🎄 Decorate your dependencies](#decorate-your-dependencies)
* [♻ Lifecycle control](#lifecycle-control)
* [🚮 Disposing provided stuff](#disposing-provided-stuff)
* [✨ Magic tokens](#magic-tokens)
* [😬 Error handling](#error-handling)
* [📖 API reference](#api-reference)
* [🤝 Commendation](#commendation)
- [🗺️ Installation](#installation)
- [🎁 Usage](#usage)
- [💭 Motivation](#motivation)
- [🗝️ Typesafe? How?](#typesafe-how)
- [👶 Child injectors](#child-injectors)
- [🎄 Decorate your dependencies](#decorate-your-dependencies)
- [♻ Lifecycle control](#lifecycle-control)
- [🚮 Disposing provided stuff](#disposing-provided-stuff)
- [✨ Magic tokens](#magic-tokens)
- [😬 Error handling](#error-handling)
- [📖 API reference](#api-reference)
- [🤝 Commendation](#commendation)

<a name="installation"></a>

Expand Down Expand Up @@ -66,7 +65,7 @@ interface Logger {
const logger: Logger = {
info(message: string) {
console.log(message);
}
},
};

class HttpClient {
Expand All @@ -75,11 +74,16 @@ class HttpClient {
}

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

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

const myService = appInjector.injectClass(MyService);
// Dependencies for MyService validated and injected
Expand All @@ -105,12 +109,17 @@ class HttpClient {
}

class MyService {
constructor(private http: HttpClient, private log: Logger) {}
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 appInjector = createInjector()
.provideValue('logger', logger)
.provideClass('httpClient', HttpClient);

const myService = appInjector.injectClass(MyService);
```
Expand Down Expand Up @@ -205,12 +214,14 @@ function fooDecorator(foo: Foo) {
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 fooProvider = createInjector()
.provideClass('foo', Foo)
.provideFactory('foo', fooDecorator);
const foo = fooProvider.resolve('foo');

foo.bar();
Expand Down Expand Up @@ -240,7 +251,9 @@ class Foo {
static inject = ['log'] as const;
}

const fooProvider = injector.provideFactory('log', loggerFactory, Scope.Transient).provideClass('foo', Foo, Scope.Singleton);
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');
Expand Down Expand Up @@ -349,13 +362,13 @@ class Bar {
}
class Baz {
static inject = ['foo', 'bar'] as const;
constructor(public foo: Foo, public bar: Bar) {}
constructor(
public foo: Foo,
public bar: Bar,
) {}
}
const rootInjector = createInjector();
rootInjector
.provideClass('foo', Foo)
.provideClass('bar', Bar)
.injectClass(Baz);
rootInjector.provideClass('foo', Foo).provideClass('bar', Bar).injectClass(Baz);
await fooProvider.dispose();
// => "Foo disposed"
// => "Bar disposed",
Expand All @@ -369,15 +382,20 @@ Any instance created with `injectClass` or `injectFactory` will _not_ be dispose

Any `Injector` instance can always provide the following tokens:

| Token name | Token value | Description |
| ---------------- | ------------- | -------------------------------------------------------------------------------------------------- |
| `INJECTOR_TOKEN` | `'$injector'` | Injects the current injector |
| 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:

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

class Foo {
constructor(injector: Injector<{}>, target: Function | undefined) {}
Expand Down Expand Up @@ -476,7 +494,7 @@ 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.
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.

Expand Down Expand Up @@ -530,22 +548,44 @@ 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);
const fooBarInjector = fooInjector.provideFactory(
'logger',
loggerFactory,
Scope.Transient,
);
```

#### `injector.provideFactory(token: Token, Class: InjectableClass<TContext>, scope = Scope.Singleton): Injector<ChildContext<TContext, Token, R>>`
#### `injector.provideClass(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.createChildInjector(): Injector<TContext>`

Create a child injector that can provide exactly the same as the parent injector. Contrary to its `provideXxx` counterparts,this will create a new disposable scope without providing additional injectable values.

```ts
const parentInjector = createInjector().provideValue('foo', 'bar');
for (const task of tasks) {
try {
const scope = parentInjector.createChildInjector();
const foo = scope.provideClass('baz', DisposableBaz).injectClass(Foo);
foo.handle(task);
} finally {
await scope.dispose(); // Dispose the scope, including instances of DisposableBaz
// Next task gets a fresh scope
}
}
```

#### `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.
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._
Expand Down
65 changes: 45 additions & 20 deletions src/InjectorImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ const DEFAULT_SCOPE = Scope.Singleton;
┗━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━┻━━━━━━━━━━━━━┓
┃ ChildWithProvidedInjector ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━┻━┳━━━━━━━━━━━━━━━━┓
┏━━━━━━━━┻━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓
┃ FactoryInjector ┃ ┃ ClassInjector ┃ ┃ ValueInjector ┃
Expand Down Expand Up @@ -149,6 +154,10 @@ abstract class AbstractInjector<TContext> implements Injector<TContext> {

private isDisposed = false;

public createChildInjector(): Injector<TContext> {
return new ChildInjector(this);
}

public async dispose() {
if (!this.isDisposed) {
this.isDisposed = true; // be sure new disposables aren't added while we're disposing
Expand Down Expand Up @@ -179,38 +188,47 @@ class RootInjector extends AbstractInjector<{}> {
}
}

abstract class ChildInjector<
class ChildInjector<
TParentContext,
TContext,
> extends AbstractInjector<TContext> {
protected override async disposeInjectedValues(): Promise<void> {}
protected override resolveInternal<Token extends keyof TContext>(
token: Token,
target?: Function,
): TContext[Token] {
return this.parent.resolve(token as any, target) as any;
}
constructor(protected readonly parent: AbstractInjector<TParentContext>) {
super();
}

public override async dispose() {
this.parent.removeChild(this as Injector<any>);
await super.dispose();
}
}

abstract class ChildWithProvidedInjector<
TParentContext,
TProvided,
CurrentToken extends string,
> extends AbstractInjector<
> extends ChildInjector<
TParentContext,
TChildContext<TParentContext, TProvided, CurrentToken>
> {
private cached: { value?: any } | undefined;
private readonly disposables = new Set<Disposable>();

constructor(
protected readonly parent: AbstractInjector<TParentContext>,
parent: AbstractInjector<TParentContext>,
protected readonly token: CurrentToken,
private readonly scope: Scope,
) {
super();
super(parent);
}

protected abstract result(target: Function | undefined): TProvided;

public override async dispose() {
this.parent.removeChild(this as Injector<any>);
await super.dispose();
}

protected override async disposeInjectedValues() {
const promisesToAwait = [...this.disposables.values()].map((disposable) =>
disposable.dispose(),
);
await Promise.all(promisesToAwait);
}

protected override resolveInternal<
SearchToken extends keyof TChildContext<
TParentContext,
Expand Down Expand Up @@ -250,13 +268,20 @@ abstract class ChildInjector<
}
return value;
}

protected override async disposeInjectedValues() {
const promisesToAwait = [...this.disposables.values()].map((disposable) =>
disposable.dispose(),
);
await Promise.all(promisesToAwait);
}
}

class ValueProvider<
TParentContext,
TProvided,
ProvidedToken extends string,
> extends ChildInjector<TParentContext, TProvided, ProvidedToken> {
> extends ChildWithProvidedInjector<TParentContext, TProvided, ProvidedToken> {
constructor(
parent: AbstractInjector<TParentContext>,
token: ProvidedToken,
Expand All @@ -274,7 +299,7 @@ class FactoryProvider<
TProvided,
ProvidedToken extends string,
Tokens extends InjectionToken<TParentContext>[],
> extends ChildInjector<TParentContext, TProvided, ProvidedToken> {
> extends ChildWithProvidedInjector<TParentContext, TProvided, ProvidedToken> {
constructor(
parent: AbstractInjector<TParentContext>,
token: ProvidedToken,
Expand All @@ -299,7 +324,7 @@ class ClassProvider<
TProvided,
ProvidedToken extends string,
Tokens extends InjectionToken<TParentContext>[],
> extends ChildInjector<TParentContext, TProvided, ProvidedToken> {
> extends ChildWithProvidedInjector<TParentContext, TProvided, ProvidedToken> {
constructor(
parent: AbstractInjector<TParentContext>,
token: ProvidedToken,
Expand Down
Loading
Loading