Skip to content
Open
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
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -405,6 +406,53 @@ class Foo {
const foo = createInjector().inject(Foo);
```

<a name="internally-registered-tokens"></a>

## 🏷️ 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);
```

<a name="error-handling"></a>

## 😬 Error handling
Expand Down
97 changes: 95 additions & 2 deletions src/InjectorImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -105,7 +107,38 @@ abstract class AbstractInjector<TContext> implements Injector<TContext> {
return provider;
}

public provideClass<
provideClass<
Token extends string,
R,
Tokens extends readonly InjectionToken<TContext>[],
>(
token: Token,
Class: InjectableClass<TContext, R, Tokens>,
scope?: Scope,
): Injector<TChildContext<TContext, R, Token>>;

provideClass<
KnownAsToken extends string,
R,
Tokens extends readonly InjectionToken<TContext>[],
>(
Class: KnownInjectableClass<TContext, R, Tokens, KnownAsToken>,
scope?: Scope,
): Injector<TChildContext<TContext, R, KnownAsToken>>;

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<TContext>[],
Expand All @@ -119,7 +152,53 @@ abstract class AbstractInjector<TContext> implements Injector<TContext> {
this.childInjectors.add(provider as Injector<any>);
return provider;
}
public provideFactory<

protected _provideClassWithKnownAs<
KnownAsToken extends string,
R,
Tokens extends InjectionToken<TContext>[],
>(
Class: KnownInjectableClass<TContext, R, Tokens, KnownAsToken>,
scope = DEFAULT_SCOPE,
): AbstractInjector<TChildContext<TContext, R, KnownAsToken>> {
this.throwIfDisposed(Class.knownAs);
const provider = new ClassProvider(this, Class.knownAs, scope, Class);
this.childInjectors.add(provider as Injector<any>);
return provider;
}

provideFactory<
Token extends string,
R,
Tokens extends readonly InjectionToken<TContext>[],
>(
token: Token,
factory: InjectableFunction<TContext, R, Tokens>,
scope?: Scope,
): Injector<TChildContext<TContext, R, Token>>;

provideFactory<
KnownAsToken extends string,
R,
Tokens extends readonly InjectionToken<TContext>[],
>(
factory: KnownInjectableFunction<TContext, R, Tokens, KnownAsToken>,
scope?: Scope,
): Injector<TChildContext<TContext, R, KnownAsToken>>;

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<TContext>[],
Expand All @@ -134,6 +213,20 @@ abstract class AbstractInjector<TContext> implements Injector<TContext> {
return provider;
}

protected _provideFactoryWithKnownAs<
KnownAsToken extends string,
R,
Tokens extends InjectionToken<TContext>[],
>(
factory: KnownInjectableFunction<TContext, R, Tokens, KnownAsToken>,
scope = DEFAULT_SCOPE,
): AbstractInjector<TChildContext<TContext, R, KnownAsToken>> {
this.throwIfDisposed(factory.knownAs);
const provider = new FactoryProvider(this, factory.knownAs, scope, factory);
this.childInjectors.add(provider as Injector<any>);
return provider;
}

public resolve<Token extends keyof TContext>(
token: Token,
target?: Function,
Expand Down
29 changes: 26 additions & 3 deletions src/api/Injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ export type InjectableClass<
Tokens extends readonly InjectionToken<TContext>[],
> = ClassWithInjections<TContext, R, Tokens> | ClassWithoutInjections<R>;

export type KnownInjectableClass<
TContext,
R,
Tokens extends readonly InjectionToken<TContext>[],
KnownAsToken extends string,
> = InjectableClass<TContext, R, Tokens> & {
readonly knownAs: KnownAsToken;
};

export interface ClassWithInjections<
TContext,
R,
Expand All @@ -26,6 +35,15 @@ export type InjectableFunction<
| InjectableFunctionWithInject<TContext, R, Tokens>
| InjectableFunctionWithoutInject<R>;

export type KnownInjectableFunction<
TContext,
R,
Tokens extends readonly InjectionToken<TContext>[],
KnownAsToken extends string,
> = InjectableFunction<TContext, R, Tokens> & {
readonly knownAs: KnownAsToken;
};

export interface InjectableFunctionWithInject<
TContext,
R,
Expand All @@ -41,6 +59,11 @@ export type Injectable<
TContext,
R,
Tokens extends readonly InjectionToken<TContext>[],
> =
| InjectableClass<TContext, R, Tokens>
| InjectableFunction<TContext, R, Tokens>;
KnownAsToken extends string | undefined = undefined,
> = KnownAsToken extends string
?
| KnownInjectableClass<TContext, R, Tokens, KnownAsToken>
| KnownInjectableFunction<TContext, R, Tokens, KnownAsToken>
:
| InjectableClass<TContext, R, Tokens>
| InjectableFunction<TContext, R, Tokens>;
33 changes: 32 additions & 1 deletion src/api/Injector.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -47,6 +52,19 @@ export interface Injector<TContext = {}> {
Class: InjectableClass<TContext, R, Tokens>,
scope?: Scope,
): Injector<TChildContext<TContext, R, Token>>;
/**
* 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<TContext>[],
>(
Class: KnownInjectableClass<TContext, R, Tokens, KnownAsToken>,
scope?: Scope,
): Injector<TChildContext<TContext, R, KnownAsToken>>;
/**
* 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.
Expand All @@ -62,6 +80,19 @@ export interface Injector<TContext = {}> {
factory: InjectableFunction<TContext, R, Tokens>,
scope?: Scope,
): Injector<TChildContext<TContext, R, Token>>;
/**
* 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<TContext>[],
>(
factory: KnownInjectableFunction<TContext, R, Tokens, KnownAsToken>,
scope?: Scope,
): Injector<TChildContext<TContext, R, KnownAsToken>>;

/**
* Create a child injector that can provide exactly the same as the parent injector.
Expand Down
Loading