diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9fcfde7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} diff --git a/README.md b/README.md index d92abaf..9dd6d5b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A light-weight TypeScript-first library for dependency injection. * Stand-alone, no need to install other dependencies * Intended for both JavaScript and TypeScript projects +* Supports three-shakeable injection tokens * Inspired by [Angular](https://angular.dev/) and [InversifyJS](https://github.com/inversify/InversifyJS) * Uses native [ECMAScript TC39 decorators](https://github.com/tc39/proposal-decorators) (currently stage 3) * No need for `experimentalDecorators` and `emitDecoratorMetadata` @@ -64,6 +65,7 @@ Check out the [advanced examples](#advanced-examples) below to learn more! * Static values * Dynamic factories * Async factories + * Multi providers * Inheritance support ## Limitations @@ -75,7 +77,6 @@ However, if you prefer a light-weight library that works out of the box and prov * Extend the Container API * Scoping -* Multi-injection * ... Please file an issue if you like to propose new features. diff --git a/src/container.test.ts b/src/container.test.ts index 8baec6b..8096c48 100644 --- a/src/container.test.ts +++ b/src/container.test.ts @@ -1,11 +1,5 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { - bootstrap, - bootstrapAsync, - Container, - inject, - injectAsync, -} from "./container.js"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { bootstrap, bootstrapAsync, Container, inject, injectAsync } from "./container.js"; import { injectable } from "./decorators.js"; import { InjectionToken } from "./tokens.js"; diff --git a/src/container.ts b/src/container.ts index 1c40614..9588668 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,9 +1,4 @@ -import { - type Token, - isClassToken, - toString, - isInjectionToken, -} from "./tokens.js"; +import { type Token, isClassToken, toString, isInjectionToken } from "./tokens.js"; import { isClassProvider, isFactoryProvider, @@ -11,8 +6,10 @@ import { isValueProvider, type Provider, isAsyncFactoryProvider, + isMultiProvider, + isExistingProvider, } from "./providers.js"; -import { getInjectableTarget, isInjectable } from './decorators.js'; +import { getInjectableTargets, isInjectable } from "./decorators.js"; export class Container { private providers: ProviderMap = new Map(); @@ -20,108 +17,157 @@ export class Container { bind(provider: Provider): this { const token = isConstructorProvider(provider) ? provider : provider.provide; + const multi = isMultiProvider(provider); + + if (isExistingProvider(provider) && provider.provide === provider.useExisting) { + throw Error(`A provider with "useExisting" cannot refer to itself`); + } - if (this.providers.has(token)) { - // todo: log warning, since we're overwriting a provider? - // alternatively, provide both .bind() and .rebind() semantics? + if (!isExistingProvider(provider) && this.singletons.has(token)) { + throw Error( + `Cannot bind a new provider for ${toString(token)}, since the existing provider was already constructed.`, + ); } - this.providers.set(token, provider); + const existingProviders = this.providers.get(token) ?? []; + + if (multi && existingProviders.some((it) => !isMultiProvider(it))) { + // todo: should we be this strict, or only throw an error in mismatches upon retrieving? + throw Error( + `Cannot bind ${toString(token)} as multi-provider, since there is already a provider which is not a multi-provider.`, + ); + } else if (!multi && existingProviders.some((it) => isMultiProvider(it))) { + throw Error( + `Cannot bind ${toString(token)} as provider, since there are already provider(s) that are multi-providers.`, + ); + } + + if (multi) { + this.providers.set(token, [...existingProviders, provider]); + } else { + if (existingProviders.length > 0) { + // todo: log warning, since we're overwriting a provider? + // alternatively, provide both .bind() and .rebind() semantics? + } + + this.providers.set(token, [provider]); + } // todo: should support eagerly resolved providers or not? return this; } + get(token: Token, options: { multi: true }): T[]; get(token: Token, options: { optional: true }): T | undefined; - get(token: Token, options?: { optional: boolean }): T; - get(token: Token, options?: { optional: boolean }): T | undefined { + get(token: Token, options: { multi: true; optional: true }): T[] | undefined; + get(token: Token, options?: { optional?: boolean; multi?: boolean }): T; + get(token: Token, options?: { optional?: boolean; multi?: boolean }): T | T[] | undefined { this.autoBindIfNeeded(token); if (!this.providers.has(token)) { if (options?.optional) { return undefined; } - throw Error(`No provider found for ${toString(token)}`); + throw Error(`No provider(s) found for ${toString(token)}`); } - const provider = assertNotNull(this.providers.get(token)); + const providers = assertPresent(this.providers.get(token)); if (!this.singletons.has(token)) { - if (isAsyncFactoryProvider(provider)) { + if (providers.some(isAsyncFactoryProvider)) { throw new Error( - `Provider for token ${toString(token)} is async, please use injectAsync() or container.getAsync() instead`, + `One or more providers for token ${toString(token)} are async, please use injectAsync() or container.getAsync() instead`, ); } - this.singletons.set(token, construct(provider, this)); + this.singletons.set( + token, + providers.map((it) => construct(it, this)), + ); } - return assertNotNull(this.singletons.get(token)); + if (options?.multi) { + return assertPresent(this.singletons.get(token)); + } else { + return assertPresent(this.singletons.get(token)?.at(0)); + } } + async getAsync(token: Token, options: { multi: true }): Promise; + async getAsync(token: Token, options: { optional: true }): Promise; + async getAsync(token: Token, options: { multi: true; optional: true }): Promise; + async getAsync(token: Token, options?: { optional?: boolean; multi?: boolean }): Promise; async getAsync( token: Token, - options: { optional: true }, - ): Promise; - async getAsync( - token: Token, - options?: { optional: boolean }, - ): Promise; - async getAsync( - token: Token, - options?: { optional: boolean }, - ): Promise { + options?: { + optional?: boolean; + multi?: boolean; + }, + ): Promise { this.autoBindIfNeeded(token); if (!this.providers.has(token)) { if (options?.optional) { return Promise.resolve(undefined); } - throw Error(`No provider found for ${toString(token)}`); + throw Error(`No provider(s) found for ${toString(token)}`); } - const provider = assertNotNull(this.providers.get(token)); + const existingProviders = this.providers.get(token) ?? []; if (!this.singletons.has(token)) { - const value = await constructAsync(provider, this); - this.singletons.set(token, value); + const values = await Promise.all(existingProviders.map((it) => constructAsync(it, this))); + this.singletons.set(token, values); } - return await promisify(assertNotNull(this.singletons.get(token))); + if (options?.multi) { + return Promise.all(assertPresent(this.singletons.get(token)).map((it) => promisify(it))); + } else { + return promisify(assertPresent(this.singletons.get(token)?.at(0))); + } } private autoBindIfNeeded(token: Token) { - if (!this.providers.has(token)) { - if (isClassToken(token) && isInjectable(token)) { - this.bind({ - provide: token, - useClass: getInjectableTarget(token), - }); + if (this.singletons.has(token)) { + return; + } + + if (isClassToken(token) && isInjectable(token)) { + const targetClasses = getInjectableTargets(token); - // inheritance support: also bind for its super classes - let superClass = Object.getPrototypeOf(token); - while (superClass.name) { + targetClasses + .filter((targetClass) => !this.providers.has(targetClass)) + .forEach((targetClass) => { this.bind({ - provide: superClass, - useClass: token, + provide: targetClass, + multi: true, + useClass: targetClass, }); - superClass = Object.getPrototypeOf(superClass) - } - } else if (isInjectionToken(token) && token.options?.factory) { - if (!token.options.async) { - this.bind({ - provide: token, - async: false, - useFactory: token.options.factory, - }); - } else if (token.options.async) { + }); + + targetClasses + .filter((it) => it !== token) + .forEach((targetClass) => { this.bind({ provide: token, - async: true, - useFactory: token.options.factory, + multi: true, + useExisting: targetClass, }); - } + }); + } else if (isInjectionToken(token) && token.options?.factory) { + if (!token.options.async) { + this.bind({ + provide: token, + async: false, + useFactory: token.options.factory, + }); + } else if (token.options.async) { + this.bind({ + provide: token, + async: true, + useFactory: token.options.factory, + }); } } } @@ -129,34 +175,20 @@ export class Container { let currentScope: Container | undefined = undefined; -export function inject( - token: Token, - options: { optional: true }, -): T | undefined; +export function inject(token: Token, options: { optional: true }): T | undefined; export function inject(token: Token): T; -export function inject( - token: Token, - options?: { optional: boolean }, -): T | undefined { +export function inject(token: Token, options?: { optional: boolean }): T | undefined { if (currentScope === undefined) { throw new Error("You can only invoke inject() from the injection context"); } return currentScope.get(token, options); } -export function injectAsync( - token: Token, - options: { optional: true }, -): Promise; +export function injectAsync(token: Token, options: { optional: true }): Promise; export function injectAsync(token: Token): Promise; -export function injectAsync( - token: Token, - options?: { optional: boolean }, -): Promise { +export function injectAsync(token: Token, options?: { optional: boolean }): Promise { if (currentScope === undefined) { - throw new Error( - "You can only invoke injectAsync() from the injection context", - ); + throw new Error("You can only invoke injectAsync() from the injection context"); } return currentScope.getAsync(token, options); } @@ -171,10 +203,7 @@ function construct(provider: Provider, scope: Container): Promise | T { } } -async function constructAsync( - provider: Provider, - scope: Container, -): Promise { +async function constructAsync(provider: Provider, scope: Container): Promise { const originalScope = currentScope; try { currentScope = scope; @@ -189,10 +218,7 @@ async function promisify(value: T | Promise): Promise { return new Promise((resolve) => resolve(value)); } -function doConstruct( - provider: Provider, - scope: Container, -): T | Promise { +function doConstruct(provider: Provider, scope: Container): T | Promise { if (isConstructorProvider(provider)) { return new provider(); } else if (isClassProvider(provider)) { @@ -206,14 +232,16 @@ function doConstruct( } } -interface ProviderMap extends Map, Provider> { - get(key: Token): Provider | undefined - set(key: Token, value: Provider): this +interface ProviderMap extends Map, Provider[]> { + get(key: Token): Provider[] | undefined; + + set(key: Token, value: Provider[]): this; } -interface SingletonMap extends Map, unknown> { - get(token: Token): T | undefined - set(token: Token, value: T): this +interface SingletonMap extends Map, unknown[]> { + get(token: Token): T[] | undefined; + + set(token: Token, value: T[]): this; } export function bootstrap(token: Token): T { @@ -224,7 +252,7 @@ export function bootstrapAsync(token: Token): Promise { return new Container().getAsync(token); } -function assertNotNull(value: T | null | undefined): T { +function assertPresent(value: T | null | undefined): T { if (value === null || value === undefined) { throw Error(`Expected value to be not null or undefined`); } diff --git a/src/decorators.test.ts b/src/decorators.test.ts new file mode 100644 index 0000000..b80cd6f --- /dev/null +++ b/src/decorators.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { getInjectableTargets, injectable, type InjectableClass } from './decorators.js'; + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +abstract class AbstractService {} + +@injectable() +class FooService extends AbstractService {} + +@injectable() +class BarService extends AbstractService {} + +@injectable() +class SpecialBarService extends BarService {} + +class BazService extends AbstractService {} + +@injectable() +class SpecialBazService extends BazService {} + +describe("Decorators", () => { + it("should register every annotated class across its hierarchy", () => { + + expect(getInjectableTargets(AbstractService as InjectableClass).map(it => it.name)) + .toEqual(['FooService', 'BarService', 'SpecialBarService', 'SpecialBazService']) + + expect(getInjectableTargets(FooService as InjectableClass).map(it => it.name)) + .toEqual(['FooService']) + + expect(getInjectableTargets(BarService as InjectableClass).map(it => it.name)) + .toEqual(['BarService', 'SpecialBarService']) + + expect(getInjectableTargets(SpecialBarService as InjectableClass).map(it => it.name)) + .toEqual(['SpecialBarService']) + + expect(getInjectableTargets(BazService as InjectableClass).map(it => it.name)) + .toEqual(['SpecialBazService']) + + expect(getInjectableTargets(SpecialBazService as InjectableClass).map(it => it.name)) + .toEqual(['SpecialBazService']) + }); +}); diff --git a/src/decorators.ts b/src/decorators.ts index dca7fbd..9c9fee7 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -5,31 +5,35 @@ type ClassDecorator> = (target: C) => C | void; export const injectableSymbol = Symbol("injectable"); +export type InjectableClass = Class & { [injectableSymbol]: Class[]}; + export function injectable>(): ClassDecorator { return (target) => { let superClass = Object.getPrototypeOf(target); while (superClass.name) { - Object.defineProperty(superClass, injectableSymbol, { - get: () => target, - }); + if (!Object.getOwnPropertyDescriptor(superClass, injectableSymbol)) { + Object.defineProperty(superClass, injectableSymbol, { + value: [target], + writable: true, + }); + } else { + superClass[injectableSymbol] = [...superClass[injectableSymbol], target]; + } superClass = Object.getPrototypeOf(superClass); } Object.defineProperty(target, injectableSymbol, { - get: () => target, + value: [target], + writable: true, }); }; } -export function isInjectable(target: Class): target is Class & { [injectableSymbol]: Class } { +export function isInjectable(target: Class): target is InjectableClass { // eslint-disable-next-line no-prototype-builtins return target.hasOwnProperty(injectableSymbol); } -export function getInjectableTarget(target: Class): Class { - if (!isInjectable(target)) { - throw Error(`Target ${target} nor any of its subclasses is not annotated with @injectable`) - } - +export function getInjectableTargets(target: InjectableClass): Class[] { return target[injectableSymbol]; } diff --git a/src/examples.test.ts b/src/examples.test.ts index d2a5a86..78372d1 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -150,13 +150,201 @@ describe("Container", () => { expect(factoryFn).toHaveBeenCalledTimes(1); expect(() => container.get(tokenWithoutProvider)).toThrowError(); - expect(() => - container.get(tokenWithoutProvider, { optional: false }), - ).toThrowError(); + expect(() => container.get(tokenWithoutProvider, { optional: false })).toThrowError(); expect(() => container.get(tokenProvidedAsync)).toThrowError(); const fooAsync = await container.getAsync(tokenProvidedAsync); expect(fooAsync).toEqual({ foo: "async" }); }); + + it("should support multi-providers (example without auto-binding)", () => { + abstract class AbstractService { + protected constructor(private name: string) {} + } + + class FooService extends AbstractService { + constructor() { + super("Foo"); + } + } + + class BarService extends AbstractService { + constructor() { + super("Bar"); + } + } + + const container = new Container(); + + container + .bind({ + provide: AbstractService, + multi: true, + useClass: FooService, + }) + .bind({ + provide: AbstractService, + multi: true, + useClass: BarService, + }); + + const services = container.get(AbstractService, { multi: true }); + + expect(services).not.toBeUndefined(); + expect(services).toHaveLength(2); + + const [serviceA, serviceB] = services; + + expect(serviceA).toBeInstanceOf(FooService); + expect(serviceB).toBeInstanceOf(BarService); + }); + + it("should support multi-providers (example with auto-binding)", () => { + abstract class AbstractService { + protected constructor(private name: string) {} + } + + @injectable() + class FooService extends AbstractService { + constructor() { + super("Foo"); + } + } + + @injectable() + class BarService extends AbstractService { + constructor() { + super("Bar"); + } + } + + const container = new Container(); + + const services = container.get(AbstractService, { multi: true }); + + expect(services).not.toBeUndefined(); + expect(services).toHaveLength(2); + + const [serviceA, serviceB] = services; + + expect(serviceA).toBeInstanceOf(FooService); + expect(serviceB).toBeInstanceOf(BarService); + }); + + describe("should support multi-providers with multi-inheritance (example with auto-binding)", () => { + abstract class AbstractService { + protected constructor(private name: string) {} + } + + @injectable() + class FooService extends AbstractService { + constructor() { + super("Foo"); + } + } + + @injectable() + class BarService extends AbstractService { + constructor() { + super("Bar"); + } + } + + @injectable() + class SpecialBarService extends BarService { + constructor(public age = 6) { + super(); + } + } + + class BazService extends AbstractService { + constructor() { + super("Bar"); + } + } + + @injectable() + class SpecialBazService extends BazService { + constructor(public age = 8) { + super(); + } + } + + it('first parent class, then child class ', () => { + const container = new Container(); + + const abstractServices = container.get(AbstractService, { multi: true }); + + expect(abstractServices).not.toBeUndefined(); + expect(abstractServices).toHaveLength(4); + + const [abstractServiceFoo, abstractServiceBar, abstractServiceSpecialBar, abstractServiceSpecialBaz] = abstractServices; + + expect(abstractServiceFoo).toBeInstanceOf(FooService); + expect(abstractServiceBar).toBeInstanceOf(BarService); + expect(abstractServiceSpecialBar).toBeInstanceOf(SpecialBarService); + expect(abstractServiceSpecialBaz).toBeInstanceOf(SpecialBazService); + + const barServices = container.get(BarService, { multi: true }); + + expect(barServices).not.toBeUndefined(); + expect(barServices).toHaveLength(2); + + const [barServicesBar, barServicesSpecialBar] = barServices; + + expect(barServicesBar).toBeInstanceOf(BarService); + expect(barServicesBar).toBe(abstractServiceBar); + expect(barServicesSpecialBar).toBeInstanceOf(SpecialBarService); + expect(barServicesSpecialBar).toBe(abstractServiceSpecialBar); + + const bazServices = container.get(BazService, { multi: true }); + + expect(bazServices).not.toBeUndefined(); + expect(bazServices).toHaveLength(1); + + const [barServiceSpecialBaz] = bazServices; + + expect(barServiceSpecialBaz).toBeInstanceOf(SpecialBazService); + expect(barServiceSpecialBaz).toBe(abstractServiceSpecialBaz); + }); + + it('first child class, then parent class ', () => { + const container = new Container(); + + const bazServices = container.get(BazService, { multi: true }); + + expect(bazServices).not.toBeUndefined(); + expect(bazServices).toHaveLength(1); + + const [barServiceSpecialBaz] = bazServices; + + expect(barServiceSpecialBaz).toBeInstanceOf(SpecialBazService); + + const barServices = container.get(BarService, { multi: true }); + + expect(barServices).not.toBeUndefined(); + expect(barServices).toHaveLength(2); + + const [barServicesBar, barServicesSpecialBar] = barServices; + + expect(barServicesBar).toBeInstanceOf(BarService); + expect(barServicesSpecialBar).toBeInstanceOf(SpecialBarService); + + const abstractServices = container.get(AbstractService, { multi: true }); + + expect(abstractServices).not.toBeUndefined(); + expect(abstractServices).toHaveLength(4); + + const [abstractServiceFoo, abstractServiceBar, abstractServiceSpecialBar, abstractServiceSpecialBaz] = abstractServices; + + expect(abstractServiceFoo).toBeInstanceOf(FooService); + expect(abstractServiceBar).toBeInstanceOf(BarService); + expect(abstractServiceBar).toBe(barServicesBar); + expect(abstractServiceSpecialBar).toBeInstanceOf(SpecialBarService); + expect(abstractServiceSpecialBar).toBe(barServicesSpecialBar); + expect(abstractServiceSpecialBaz).toBeInstanceOf(SpecialBazService); + expect(abstractServiceSpecialBaz).toBe(barServiceSpecialBaz); + }); + }); }); diff --git a/src/providers.test.ts b/src/providers.test.ts index 197bbf5..aa4f601 100644 --- a/src/providers.test.ts +++ b/src/providers.test.ts @@ -32,6 +32,8 @@ describe("Providers", () => { expect(container.get(MyService)).toBe(myService); expect(container.get(MyService, { optional: true })).toBe(myService); expect(myServiceConstructorSpy).toHaveBeenCalledTimes(1); + + expect(() => container.bind(MyService)).toThrowError(); }); it("Class providers should be provided once", () => { @@ -180,11 +182,11 @@ describe("Providers", () => { }); describe("abstract classes and inheritance", () => { - abstract class AbstractService { - protected constructor(public name = "AbstractService") {} - } - it("should support annotated subclasses", () => { + abstract class AbstractService { + protected constructor(public name = "AbstractService") {} + } + @injectable() class FooService extends AbstractService { constructor(public fooProp = "foo") { @@ -196,12 +198,17 @@ describe("Providers", () => { expect(container.get(AbstractService)).toBeInstanceOf(FooService); expect(container.get(FooService)).toBeInstanceOf(FooService); + expect(container.get(FooService)).toBe(container.get(AbstractService)); expect(container.get(FooService)).toBeInstanceOf(AbstractService); expect(container.get(AbstractService)).toBeInstanceOf(FooService); }); it("should support binding subclasses", () => { + abstract class AbstractService { + protected constructor(public name = "AbstractService") {} + } + class FooService extends AbstractService { constructor(public fooProp = "foo") { super("FooService"); @@ -238,4 +245,71 @@ describe("Providers", () => { expect(container.get(AbstractService)).toBeInstanceOf(FooService); }); }); + + describe("Multi-provider injection", () => { + it("should support multi-value providers", () => { + const container = new Container(); + + const TOKEN = new InjectionToken("TOKEN"); + const OTHER_TOKEN = new InjectionToken("OTHER_TOKEN"); + + container + .bind({ + provide: TOKEN, + multi: true, + useValue: 1, + }) + .bind({ + provide: TOKEN, + multi: true, + useValue: 2, + }); + + expect(container.get(TOKEN, { multi: true })).toEqual([1, 2]); + expect(() => container.get(OTHER_TOKEN, { multi: true })).toThrowError(); + expect(container.get(OTHER_TOKEN, { multi: true, optional: true })).toBeUndefined(); + + expect(() => { + container.bind({ + provide: TOKEN, + multi: true, + useValue: 1, + }); + }).toThrowError(); + }); + + it("should support multi-value async providers", async () => { + const container = new Container(); + + const TOKEN = new InjectionToken("TOKEN"); + const OTHER_TOKEN = new InjectionToken("OTHER_TOKEN"); + + container + .bind({ + provide: TOKEN, + multi: true, + async: true, + useFactory: () => Promise.resolve(1), + }) + .bind({ + provide: TOKEN, + multi: true, + async: true, + useFactory: () => Promise.resolve(2), + }); + + expect(await container.getAsync(TOKEN, { multi: true })).toEqual([1, 2]); + expect(container.getAsync(OTHER_TOKEN, { multi: true })).rejects.toThrowError(); + expect(await container.getAsync(OTHER_TOKEN, { multi: true, optional: true })).toBeUndefined(); + + expect(() => { + container.bind({ + provide: TOKEN, + multi: true, + async: true, + useFactory: () => Promise.resolve(1), + }); + }).toThrowError(); + }); + }); }); diff --git a/src/providers.ts b/src/providers.ts index 18cb20b..495164e 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -14,56 +14,59 @@ export type ConstructorProvider = Class; export interface ClassProvider { provide: Token; useClass: Class; + multi?: true; } export interface ValueProvider { provide: Token; useValue: T; + multi?: true; } export interface FactoryProvider { provide: Token; async?: false; + multi?: true; useFactory: () => NoInfer; } export interface AsyncFactoryProvider { provide: Token; async: true; + multi?: true; useFactory: () => Promise>; } export interface ExistingProvider { provide: Token; useExisting: Token; + multi?: boolean; } -export function isConstructorProvider( - provider: Provider, -): provider is ConstructorProvider { +export function isConstructorProvider(provider: Provider): provider is ConstructorProvider { return isClass(provider); } -export function isClassProvider( - provider: Provider, -): provider is ClassProvider { +export function isClassProvider(provider: Provider): provider is ClassProvider { return "provide" in provider && "useClass" in provider; } -export function isValueProvider( - provider: Provider, -): provider is ValueProvider { +export function isValueProvider(provider: Provider): provider is ValueProvider { return "provide" in provider && "useValue" in provider; } -export function isFactoryProvider( - provider: Provider, -): provider is FactoryProvider | AsyncFactoryProvider { +export function isFactoryProvider(provider: Provider): provider is FactoryProvider | AsyncFactoryProvider { return "provide" in provider && "useFactory" in provider; } -export function isAsyncFactoryProvider( - provider: Provider, -): provider is AsyncFactoryProvider { +export function isAsyncFactoryProvider(provider: Provider): provider is AsyncFactoryProvider { return isFactoryProvider(provider) && (provider.async ?? false); } + +export function isMultiProvider(provider: Provider): boolean { + return "provide" in provider && "multi" in provider && provider.multi === true; +} + +export function isExistingProvider(provider: Provider): provider is ExistingProvider { + return "provide" in provider && "useExisting" in provider; +} diff --git a/src/tokens.ts b/src/tokens.ts index 1e81527..fe4cdaa 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1,20 +1,15 @@ import { type AbstractClass, type Class, isClass } from "./utils.js"; -export type Token = - | Class - | AbstractClass - | string - | symbol - | InjectionToken; +export type Token = Class | AbstractClass | string | symbol | InjectionToken; interface InjectionTokenOptions { - async?: false, - factory: () => T + async?: false; + factory: () => T; } interface AsyncInjectionTokenOptions { - async: true, - factory: () => Promise + async: true; + factory: () => Promise; } export class InjectionToken { @@ -32,9 +27,7 @@ export function isClassToken(token: Token): token is Class { return isClass(token); } -export function isInjectionToken( - token: Token, -): token is InjectionToken { +export function isInjectionToken(token: Token): token is InjectionToken { return token instanceof InjectionToken; } diff --git a/src/utils.ts b/src/utils.ts index e9b2805..26f6e08 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,9 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Class = new (...args: any[]) => T; -export interface AbstractClass { prototype: T; name: string } +export interface AbstractClass { + prototype: T; + name: string; +} export function isClass(target: unknown): target is Class | AbstractClass { return typeof target === "function";