diff --git a/README.md b/README.md
index e51d085..6dbdff1 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ _If you want to know more about how typed-inject works, please read [my blog art
- [♻ Lifecycle control](#lifecycle-control)
- [🚮 Disposing provided stuff](#disposing-provided-stuff)
- [✨ Magic tokens](#magic-tokens)
+- [🏷️ Internally registered tokens](#internally-registered-tokens)
- [😬 Error handling](#error-handling)
- [📖 API reference](#api-reference)
- [🤝 Commendation](#commendation)
@@ -405,6 +406,53 @@ class Foo {
const foo = createInjector().inject(Foo);
```
+
+
+## 🏷️ Internally registered tokens / Known Classes and Known Factories
+
+You can register class tokens internally in the classes that need to be provided, and then provide such classes by using the `provideClass` method's overload. To implement the "Known Class", add a `static knownAs` property with a string literal value. This way, it is possible to create a hierarchy of class dependencies without relying on arbitrarily specified tokens, instead opting to consolidate said tokens within the classes themselves.
+
+```ts
+import { createInjector } from 'typed-inject';
+
+class Foo {
+ static knownAs = "FooToken" as const;
+}
+
+class Bar {
+ static knownAs = "BarToken" as const;
+
+ static inject = [ Foo.knownAs ] as const;
+
+ constructor(foo: Foo) {}
+}
+
+const injector = createInjector()
+ .provideClass(Foo)
+ .provideClass(Bar);
+```
+
+Similarly, it works with `provideFactory` method's overload.
+
+```ts
+import { createInjector } from 'typed-inject';
+
+function fooFactory() {
+ return "foo factory called";
+}
+fooFactory.knownAs = 'Foo' as const;
+
+function barFactory() {
+ return "bar factory called";
+}
+barFactory.inject = [ fooFactory.knownAs ] as const;
+barFactory.knownAs = "Bar" as const;
+
+const injector = createInjector()
+ .provideFactory(fooFactory)
+ .provideFactory(barFactory);
+```
+
## 😬 Error handling
diff --git a/src/InjectorImpl.ts b/src/InjectorImpl.ts
index a932d3f..cd61473 100644
--- a/src/InjectorImpl.ts
+++ b/src/InjectorImpl.ts
@@ -11,6 +11,8 @@ import type {
InjectableClass,
InjectableFunction,
Injectable,
+ KnownInjectableClass,
+ KnownInjectableFunction,
} from './api/Injectable.js';
import type { Injector } from './api/Injector.js';
import type { Disposable } from './api/Disposable.js';
@@ -105,7 +107,38 @@ abstract class AbstractInjector implements Injector {
return provider;
}
- public provideClass<
+ provideClass<
+ Token extends string,
+ R,
+ Tokens extends readonly InjectionToken[],
+ >(
+ token: Token,
+ Class: InjectableClass,
+ scope?: Scope,
+ ): Injector>;
+
+ provideClass<
+ KnownAsToken extends string,
+ R,
+ Tokens extends readonly InjectionToken[],
+ >(
+ Class: KnownInjectableClass,
+ scope?: Scope,
+ ): Injector>;
+
+ public provideClass(...args: any[]): any {
+ if (typeof args[0] === 'string') {
+ return this._provideClassWithToken(
+ args[0],
+ args[1],
+ args[2] ?? DEFAULT_SCOPE,
+ );
+ } else {
+ return this._provideClassWithKnownAs(args[0], args[1] ?? DEFAULT_SCOPE);
+ }
+ }
+
+ protected _provideClassWithToken<
Token extends string,
R,
Tokens extends InjectionToken[],
@@ -119,7 +152,53 @@ abstract class AbstractInjector implements Injector {
this.childInjectors.add(provider as Injector);
return provider;
}
- public provideFactory<
+
+ protected _provideClassWithKnownAs<
+ KnownAsToken extends string,
+ R,
+ Tokens extends InjectionToken[],
+ >(
+ Class: KnownInjectableClass,
+ scope = DEFAULT_SCOPE,
+ ): AbstractInjector> {
+ this.throwIfDisposed(Class.knownAs);
+ const provider = new ClassProvider(this, Class.knownAs, scope, Class);
+ this.childInjectors.add(provider as Injector);
+ return provider;
+ }
+
+ provideFactory<
+ Token extends string,
+ R,
+ Tokens extends readonly InjectionToken[],
+ >(
+ token: Token,
+ factory: InjectableFunction,
+ scope?: Scope,
+ ): Injector>;
+
+ provideFactory<
+ KnownAsToken extends string,
+ R,
+ Tokens extends readonly InjectionToken[],
+ >(
+ factory: KnownInjectableFunction,
+ scope?: Scope,
+ ): Injector>;
+
+ public provideFactory(...args: any[]): any {
+ if (typeof args[0] === 'string') {
+ return this._provideFactoryWithToken(
+ args[0],
+ args[1],
+ args[2] ?? DEFAULT_SCOPE,
+ );
+ } else {
+ return this._provideFactoryWithKnownAs(args[0], args[1] ?? DEFAULT_SCOPE);
+ }
+ }
+
+ protected _provideFactoryWithToken<
Token extends string,
R,
Tokens extends InjectionToken[],
@@ -134,6 +213,20 @@ abstract class AbstractInjector implements Injector {
return provider;
}
+ protected _provideFactoryWithKnownAs<
+ KnownAsToken extends string,
+ R,
+ Tokens extends InjectionToken[],
+ >(
+ factory: KnownInjectableFunction,
+ scope = DEFAULT_SCOPE,
+ ): AbstractInjector> {
+ this.throwIfDisposed(factory.knownAs);
+ const provider = new FactoryProvider(this, factory.knownAs, scope, factory);
+ this.childInjectors.add(provider as Injector);
+ return provider;
+ }
+
public resolve(
token: Token,
target?: Function,
diff --git a/src/api/Injectable.ts b/src/api/Injectable.ts
index fb4b324..27755f3 100644
--- a/src/api/Injectable.ts
+++ b/src/api/Injectable.ts
@@ -7,6 +7,15 @@ export type InjectableClass<
Tokens extends readonly InjectionToken[],
> = ClassWithInjections | ClassWithoutInjections;
+export type KnownInjectableClass<
+ TContext,
+ R,
+ Tokens extends readonly InjectionToken[],
+ KnownAsToken extends string,
+> = InjectableClass & {
+ readonly knownAs: KnownAsToken;
+};
+
export interface ClassWithInjections<
TContext,
R,
@@ -26,6 +35,15 @@ export type InjectableFunction<
| InjectableFunctionWithInject
| InjectableFunctionWithoutInject;
+export type KnownInjectableFunction<
+ TContext,
+ R,
+ Tokens extends readonly InjectionToken[],
+ KnownAsToken extends string,
+> = InjectableFunction & {
+ readonly knownAs: KnownAsToken;
+};
+
export interface InjectableFunctionWithInject<
TContext,
R,
@@ -41,6 +59,11 @@ export type Injectable<
TContext,
R,
Tokens extends readonly InjectionToken[],
-> =
- | InjectableClass
- | InjectableFunction;
+ KnownAsToken extends string | undefined = undefined,
+> = KnownAsToken extends string
+ ?
+ | KnownInjectableClass
+ | KnownInjectableFunction
+ :
+ | InjectableClass
+ | InjectableFunction;
diff --git a/src/api/Injector.ts b/src/api/Injector.ts
index 03a7d18..f368506 100644
--- a/src/api/Injector.ts
+++ b/src/api/Injector.ts
@@ -1,4 +1,9 @@
-import { InjectableClass, InjectableFunction } from './Injectable.js';
+import {
+ InjectableClass,
+ InjectableFunction,
+ KnownInjectableClass,
+ KnownInjectableFunction,
+} from './Injectable.js';
import { InjectionToken } from './InjectionToken.js';
import { Scope } from './Scope.js';
import { TChildContext } from './TChildContext.js';
@@ -47,6 +52,19 @@ export interface Injector {
Class: InjectableClass,
scope?: Scope,
): Injector>;
+ /**
+ * Create a child injector that can provide a value using instances of `Class` for internally registered token `'knownAs'`. The new child injector can resolve all tokens the parent injector can, as well as the new `'knownAs'` token.
+ * @param Class The class with internally registered token `'knownAs'` to instantiate to provide the value.
+ * @param scope 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.
+ */
+ provideClass<
+ KnownAsToken extends string,
+ R,
+ Tokens extends readonly InjectionToken[],
+ >(
+ Class: KnownInjectableClass,
+ scope?: Scope,
+ ): 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'`.
* @param token The token to associate with the value.
@@ -62,6 +80,19 @@ export interface Injector {
factory: InjectableFunction,
scope?: Scope,
): Injector>;
+ /**
+ * Create a child injector that can provide a value using `factory` for internally registered token `'knownAs'`. The new child injector can resolve all tokens the parent injector can and the new `'knownAs'` token.
+ * @param factory A function, with internally registered token `'knownAs'`, that creates a value using instances of the tokens in `'Tokens'`.
+ * @param scope 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.
+ */
+ provideFactory<
+ KnownAsToken extends string,
+ R,
+ Tokens extends readonly InjectionToken[],
+ >(
+ factory: KnownInjectableFunction,
+ scope?: Scope,
+ ): Injector>;
/**
* Create a child injector that can provide exactly the same as the parent injector.
diff --git a/test/unit/Injector.spec.ts b/test/unit/Injector.spec.ts
index 7c7407e..f846f8e 100644
--- a/test/unit/Injector.spec.ts
+++ b/test/unit/Injector.spec.ts
@@ -87,6 +87,33 @@ describe('InjectorImpl', () => {
expect(actualFoo.name).eq('foo -> barFactory -> bar -> Foo');
});
+ it('should be able to provide a target into a function with knownAs token', () => {
+ // Arrange
+ function fooFactory(target: undefined | Function) {
+ return `foo -> ${target && target.name}`;
+ }
+ fooFactory.inject = tokens(TARGET_TOKEN);
+ fooFactory.knownAs = 'Foo' as const;
+ function barFactory(target: undefined | Function, fooName: string) {
+ return `${fooName} -> bar -> ${target && target.name}`;
+ }
+ barFactory.inject = tokens(TARGET_TOKEN, fooFactory.knownAs);
+ barFactory.knownAs = 'Bar' as const;
+ class Foo {
+ constructor(public name: string) {}
+ public static inject = tokens(barFactory.knownAs);
+ }
+
+ // Act
+ const actualFoo = rootInjector
+ .provideFactory(fooFactory)
+ .provideFactory(barFactory)
+ .injectClass(Foo);
+
+ // Assert
+ expect(actualFoo.name).eq('foo -> barFactory -> bar -> Foo');
+ });
+
it('should be able to provide a target into a class', () => {
// Arrange
class Foo {
@@ -121,6 +148,44 @@ describe('InjectorImpl', () => {
expect(actualBaz.bar.foo.target).eq(Bar);
});
+ it('should be able to provide a target into a class with knownAs token', () => {
+ // Arrange
+ class Foo {
+ constructor(public target: undefined | Function) {}
+ public static inject = tokens(TARGET_TOKEN);
+ public static knownAs = 'Foo' as const;
+ }
+
+ class Bar {
+ constructor(
+ public target: undefined | Function,
+ public foo: Foo,
+ ) {}
+ public static inject = tokens(TARGET_TOKEN, Foo.knownAs);
+ public static knownAs = 'Bar' as const;
+ }
+
+ class Baz {
+ constructor(
+ public bar: Bar,
+ public target: Function | undefined,
+ ) {}
+ public static inject = tokens(Bar.knownAs, TARGET_TOKEN);
+ public static injectableAs = 'Baz' as const;
+ }
+
+ // Act
+ const actualBaz = rootInjector
+ .provideClass(Foo)
+ .provideClass(Bar)
+ .injectClass(Baz);
+
+ // Assert
+ expect(actualBaz.target).undefined;
+ expect(actualBaz.bar.target).eq(Baz);
+ expect(actualBaz.bar.foo.target).eq(Bar);
+ });
+
it('should throw when no provider was found for a class', () => {
class FooInjectable {
constructor(public foo: string) {}
@@ -179,6 +244,32 @@ describe('InjectorImpl', () => {
parentInjector.createChildInjector().injectFunction(() => {}),
).not.throw();
});
+
+ it('should be able to create a child injector with its own scope with knownAs token', async () => {
+ // Arrange
+ const parentInjector = rootInjector.provideValue('foo', 42);
+ let fooDisposed = false;
+ class Foo implements Disposable {
+ constructor(public foo: number) {}
+ public static inject = tokens('foo');
+ public static knownAs = 'foo' as const;
+ public dispose(): void {
+ fooDisposed = true;
+ }
+ }
+
+ // Act
+ const actualChildInjector = parentInjector.createChildInjector();
+ const appInjector = actualChildInjector.provideClass(Foo);
+ appInjector.resolve('foo');
+
+ // Assert
+ await actualChildInjector.dispose();
+ expect(fooDisposed).true;
+ expect(() =>
+ parentInjector.createChildInjector().injectFunction(() => {}),
+ ).not.throw();
+ });
});
describe('ChildInjector', () => {
@@ -288,6 +379,22 @@ describe('InjectorImpl', () => {
expect(actual.foobar).eq(expectedValue);
});
+ it('should be able to provide the return value of the factoryMethod with knownAs token', () => {
+ const expectedValue = { foo: 'bar' };
+ function foobar() {
+ return expectedValue;
+ }
+ foobar.knownAs = 'foobar' as const;
+
+ const actual = rootInjector.provideFactory(foobar).injectClass(
+ class {
+ constructor(public foobar: { foo: string }) {}
+ public static inject = tokens('foobar');
+ },
+ );
+ expect(actual.foobar).eq(expectedValue);
+ });
+
it('should be able to provide parent injector values', () => {
function answer() {
return 42;
@@ -306,6 +413,25 @@ describe('InjectorImpl', () => {
expect(actual.answer).eq(42);
});
+ it('should be able to provide parent injector values with knownAs token', () => {
+ function answer() {
+ return 42;
+ }
+ answer.knownAs = 'answer' as const;
+ const factoryProvider = rootInjector.provideFactory(answer);
+ const actual = factoryProvider.injectClass(
+ class {
+ constructor(
+ public injector: Injector<{ answer: number }>,
+ public answer: number,
+ ) {}
+ public static inject = tokens(INJECTOR_TOKEN, answer.knownAs);
+ },
+ );
+ expect(actual.injector).eq(factoryProvider);
+ expect(actual.answer).eq(42);
+ });
+
it('should throw after disposed', async () => {
const sut = rootInjector.provideFactory('answer', function answer() {
return 42;
@@ -337,6 +463,22 @@ describe('InjectorImpl', () => {
expect(answerProvider.resolve('answer')).eq(42);
});
+ it('should be able to decorate an existing token with knownAs token', () => {
+ function incrementDecorator(n: number) {
+ return ++n;
+ }
+ incrementDecorator.inject = tokens('answer');
+ incrementDecorator.knownAs = 'answer' as const;
+
+ const answerProvider = rootInjector
+ .provideValue('answer', 40)
+ .provideFactory(incrementDecorator)
+ .provideFactory(incrementDecorator);
+
+ expect(answerProvider.resolve('answer')).eq(42);
+ expect(answerProvider.resolve('answer')).eq(42);
+ });
+
it('should be able to change the type of a token', () => {
const answerProvider = rootInjector
.provideValue('answer', 42)
@@ -377,6 +519,24 @@ describe('InjectorImpl', () => {
expect(answerProvider.resolve('answer').answer).eq(42);
});
+
+ it('should be able to decorate an existing token with knownAs token', () => {
+ class Foo {
+ public static inject = tokens('answer');
+ public static knownAs = 'answer' as const;
+ constructor(innerFoo: { answer: number }) {
+ this.answer = innerFoo.answer + 1;
+ }
+ public answer: number;
+ }
+
+ const answerProvider = rootInjector
+ .provideValue('answer', { answer: 40 })
+ .provideClass(Foo)
+ .provideClass(Foo);
+
+ expect(answerProvider.resolve('answer').answer).eq(42);
+ });
});
describe('dispose', () => {
@@ -386,19 +546,37 @@ describe('InjectorImpl', () => {
public dispose2 = sinon.stub();
public dispose = sinon.stub();
}
+ class KnownFoo {
+ public dispose21 = sinon.stub();
+ public dispose = sinon.stub();
+ public static knownAs = 'known-foo' as const;
+ }
function barFactory(): Disposable & { dispose3(): void } {
return { dispose: sinon.stub(), dispose3: sinon.stub() };
}
+ function knownBarFactory(): Disposable & { dispose31(): void } {
+ return { dispose: sinon.stub(), dispose31: sinon.stub() };
+ }
+ knownBarFactory.knownAs = 'known-bar' as const;
class Baz {
constructor(
public readonly bar: Disposable & { dispose3(): void },
+ public readonly knownBar: Disposable & { dispose31(): void },
public readonly foo: Foo,
+ public readonly knownFoo: KnownFoo,
) {}
- public static inject = tokens('bar', 'foo');
+ public static inject = tokens(
+ 'bar',
+ knownBarFactory.knownAs,
+ 'foo',
+ KnownFoo.knownAs,
+ );
}
const baz = rootInjector
.provideClass('foo', Foo)
+ .provideClass(KnownFoo)
.provideFactory('bar', barFactory)
+ .provideFactory(knownBarFactory)
.injectClass(Baz);
// Act
@@ -407,27 +585,48 @@ describe('InjectorImpl', () => {
// Assert
expect(baz.bar.dispose).called;
expect(baz.foo.dispose).called;
+ expect(baz.knownBar.dispose).called;
+ expect(baz.knownFoo.dispose).called;
expect(baz.foo.dispose2).not.called;
expect(baz.bar.dispose3).not.called;
+ expect(baz.knownFoo.dispose21).not.called;
+ expect(baz.knownBar.dispose31).not.called;
});
it('should also dispose transient dependencies', async () => {
class Foo {
public dispose = sinon.stub();
}
+ class KnownFoo {
+ public dispose = sinon.stub();
+ public static knownAs = 'known-foo' as const;
+ }
function barFactory(): Disposable {
return { dispose: sinon.stub() };
}
+ function knownBarFactory(): Disposable {
+ return { dispose: sinon.stub() };
+ }
+ knownBarFactory.knownAs = 'known-bar' as const;
class Baz {
constructor(
public readonly bar: Disposable,
+ public readonly knownBar: Disposable,
public readonly foo: Foo,
+ public readonly knownFoo: KnownFoo,
) {}
- public static inject = tokens('bar', 'foo');
+ public static inject = tokens(
+ 'bar',
+ knownBarFactory.knownAs,
+ 'foo',
+ KnownFoo.knownAs,
+ );
}
const baz = rootInjector
.provideClass('foo', Foo, Scope.Transient)
+ .provideClass(KnownFoo, Scope.Transient)
.provideFactory('bar', barFactory, Scope.Transient)
+ .provideFactory(knownBarFactory, Scope.Transient)
.injectClass(Baz);
// Act
@@ -469,6 +668,40 @@ describe('InjectorImpl', () => {
expect(child.dispose).calledBefore(child.parent.dispose);
});
+ it('should dispose dependencies in correct order with knownAs token (child first)', async () => {
+ class Grandparent {
+ public dispose = sinon.stub();
+ public static knownAs = 'Grandparent' as const;
+ }
+ class Parent {
+ public dispose = sinon.stub();
+ public static knownAs = 'Parent' as const;
+ }
+ class Child {
+ constructor(
+ public readonly parent: Parent,
+ public readonly grandparent: Grandparent,
+ ) {}
+ public static inject = tokens(Parent.knownAs, Grandparent.knownAs);
+ public static knownAs = 'Child' as const;
+ public dispose = sinon.stub();
+ }
+ const bazProvider = rootInjector
+ .provideClass(Grandparent, Scope.Transient)
+ .provideClass(Parent)
+ .provideClass(Child);
+ const child = bazProvider.resolve('Child');
+ const newGrandparent = bazProvider.resolve('Grandparent');
+
+ // Act
+ await rootInjector.dispose();
+
+ // Assert
+ expect(child.parent.dispose).calledBefore(child.grandparent.dispose);
+ expect(child.parent.dispose).calledBefore(newGrandparent.dispose);
+ expect(child.dispose).calledBefore(child.parent.dispose);
+ });
+
it('should not dispose injected classes or functions', async () => {
class Foo {
public dispose = sinon.stub();
@@ -521,6 +754,35 @@ describe('InjectorImpl', () => {
expect(baz.foo.dispose).eq(true);
});
+ it('should not break on non-disposable dependencies with knownAs token', async () => {
+ class Foo {
+ public dispose = true;
+ public static knownAs = 'Foo' as const;
+ }
+ function barFactory(): { dispose: string } {
+ return { dispose: 'no-fn' };
+ }
+ barFactory.knownAs = 'Bar' as const;
+ class Baz {
+ constructor(
+ public readonly bar: { dispose: string },
+ public readonly foo: Foo,
+ ) {}
+ public static inject = tokens(barFactory.knownAs, Foo.knownAs);
+ }
+ const bazInjector = rootInjector
+ .provideClass(Foo)
+ .provideFactory(barFactory);
+ const baz = bazInjector.injectClass(Baz);
+
+ // Act
+ await bazInjector.dispose();
+
+ // Assert
+ expect(baz.bar.dispose).eq('no-fn');
+ expect(baz.foo.dispose).eq(true);
+ });
+
it('should not dispose dependencies twice', async () => {
const fooProvider = rootInjector.provideClass(
'foo',
@@ -534,6 +796,19 @@ describe('InjectorImpl', () => {
expect(foo.dispose).calledOnce;
});
+ it('should not dispose dependencies twice with knownAs token', async () => {
+ class Foo implements Disposable {
+ public dispose = sinon.stub();
+ public static knownAs = 'Foo' as const;
+ }
+
+ const fooProvider = rootInjector.provideClass(Foo);
+ const foo = fooProvider.resolve('Foo');
+ await fooProvider.dispose();
+ await fooProvider.dispose();
+ expect(foo.dispose).calledOnce;
+ });
+
it('should await dispose()', async () => {
// Arrange
const fooStub = sinon.stub();
@@ -617,6 +892,39 @@ describe('InjectorImpl', () => {
expect(child.dispose).called;
expect(child.parent.dispose).not.called;
});
+
+ it("should not dispose it's parent provider with knownAs token", async () => {
+ // Arrange
+ class Grandparent {
+ public dispose = sinon.stub();
+ public static knownAs = 'Grandparent' as const;
+ }
+ class Parent {
+ public dispose = sinon.stub();
+ public static knownAs = 'Parent' as const;
+ }
+ class Child {
+ constructor(
+ public readonly parent: Parent,
+ public readonly grandparent: Grandparent,
+ ) {}
+ public static inject = tokens(Parent.knownAs, Grandparent.knownAs);
+ public static knownAs = 'Child' as const;
+ public dispose = sinon.stub();
+ }
+ const parentProvider = rootInjector
+ .provideClass(Grandparent, Scope.Transient)
+ .provideClass(Parent);
+ const childProvider = parentProvider.provideClass(Child);
+ const child = childProvider.resolve('Child');
+
+ // Act
+ await childProvider.dispose();
+
+ // Assert
+ expect(child.dispose).called;
+ expect(child.parent.dispose).not.called;
+ });
});
describe('dependency tree', () => {
@@ -672,6 +980,60 @@ describe('InjectorImpl', () => {
expect(actual.log).eq(expectedLogger);
});
+ it('should be able to inject a dependency tree with classes with knownAs token', () => {
+ // Arrange
+ class Logger {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public info(_msg: string) {}
+ }
+ class GrandChild {
+ public baz = 'qux';
+ constructor(public log: Logger) {}
+ public static inject = tokens('logger');
+ public static knownAs = 'GrandChild' as const;
+ }
+ class Child1 {
+ public bar = 'foo';
+ constructor(
+ public log: Logger,
+ public grandchild: GrandChild,
+ ) {}
+ public static inject = tokens('logger', GrandChild.knownAs);
+ public static knownAs = 'Child1' as const;
+ }
+ class Child2 {
+ public foo = 'bar';
+ constructor(public log: Logger) {}
+ public static inject = tokens('logger');
+ }
+ class Parent {
+ constructor(
+ public readonly child: Child1,
+ public readonly child2: Child2,
+ public readonly log: Logger,
+ ) {}
+ public static inject = tokens(Child1.knownAs, 'child2', 'logger');
+ }
+ const expectedLogger = new Logger();
+
+ // Act
+ const actual = rootInjector
+ .provideValue('logger', expectedLogger)
+ .provideClass(GrandChild)
+ .provideClass(Child1)
+ .provideClass('child2', Child2)
+ .injectClass(Parent);
+
+ // Assert
+ expect(actual.child.bar).eq('foo');
+ expect(actual.child2.foo).eq('bar');
+ expect(actual.child.log).eq(expectedLogger);
+ expect(actual.child2.log).eq(expectedLogger);
+ expect(actual.child.grandchild.log).eq(expectedLogger);
+ expect(actual.child.grandchild.baz).eq('qux');
+ expect(actual.log).eq(expectedLogger);
+ });
+
it('should throw an Injection error with correct message when injection failed with a runtime error', () => {
// Arrange
const expectedCause = Error('Expected error');
@@ -707,5 +1069,44 @@ describe('InjectorImpl', () => {
path: [Parent, 'child', Child, 'grandChild', GrandChild],
});
});
+
+ it('should throw an Injection error with correct message when injection failed with knownAs token with a runtime error', () => {
+ // Arrange
+ const expectedCause = Error('Expected error');
+ class GrandChild {
+ public baz = 'baz';
+ public static knownAs = 'GrandChildToken' as const;
+ constructor() {
+ throw expectedCause;
+ }
+ }
+ class Child {
+ public bar = 'foo';
+ constructor(public grandchild: GrandChild) {}
+ public static inject = tokens(GrandChild.knownAs);
+ public static knownAs = 'ChildToken' as const;
+ }
+ class Parent {
+ constructor(public readonly child: Child) {}
+ public static inject = tokens(Child.knownAs);
+ public static knownAs = 'ParentToken' as const;
+ }
+
+ // Act
+ const act = () =>
+ rootInjector
+ .provideClass(GrandChild)
+ .provideClass(Child)
+ .injectClass(Parent);
+
+ // Assert
+ expect(act)
+ .throws(InjectionError)
+ .which.deep.includes({
+ message:
+ 'Could not inject [class Parent] -> [token "ChildToken"] -> [class Child] -> [token "GrandChildToken"] -> [class GrandChild]. Cause: Expected error',
+ path: [Parent, 'ChildToken', Child, 'GrandChildToken', GrandChild],
+ });
+ });
});
});