From 40b5dd222ca4d2bef54a86631521fd3d6d5a2638 Mon Sep 17 00:00:00 2001 From: Etienne Noel Date: Sat, 5 Apr 2025 13:19:43 -0700 Subject: [PATCH] Supports passing a default provider in the `inject` decorator. --- README.md | 29 ++++++++++ src/__tests__/global-container.test.ts | 74 ++++++++++++++++++++++++++ src/decorators/inject.ts | 13 ++++- 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fc9877..3878cf1 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,35 @@ class Foo { } ``` +#### Default Provider +By default, if a dependency is not marked as optional (see above), a provider must exist for this dependency. However, you can use `{ defaultProvider: Provider }` to specify a default provider for this dependency. This provider will be used if no other provider is registered for this dependency. + +If you pass both a `defaultProvider` and `isOptional:true`, the `defaultProvider` will be used, making `isOptional: true` useless. + +```typescript +import {injectable, inject} from "tsyringe"; + +class Database {} + +@injectable() +class Foo { + constructor(@inject("Database", { defaultProvider: { useValue: new Database() } }) private database: Database) {} +} +``` + +All types of providers are supported. Here's an example using a factory: + +```typescript +import {injectable, inject} from "tsyringe"; +import dependencyContainer from "./dependency-container"; + +@injectable() +class Foo { + constructor(@inject("Database", {defaultProvider: {useFactory: (dependencyContainer: DependencyContainer) => { return dependencyContainer.resolve("Database")}}}) private database: Database) { + } +} +``` + ### injectAll() Parameter decorator for array parameters where the array contents will come from the container. diff --git a/src/__tests__/global-container.test.ts b/src/__tests__/global-container.test.ts index 249e7a3..54192e0 100644 --- a/src/__tests__/global-container.test.ts +++ b/src/__tests__/global-container.test.ts @@ -768,6 +768,80 @@ test("allows explicit array dependencies to be resolved by inject decorator", () expect(bar.foo === fooArray).toBeTruthy(); }); +test("allows inject to provide a default value, which will be used if not registered in other ways", () => { + @injectable() + class Foo {} + + const fooArray = [new Foo()]; + + @injectable() + class Bar { + constructor( + @inject("FooArray", {defaultProvider: {useValue: fooArray}}) + public foo: Foo[] + ) {} + } + + globalContainer.register(Bar, {useClass: Bar}); + + const bar = globalContainer.resolve(Bar); + expect(bar.foo).toEqual(fooArray); +}); + +test("allows inject to provide a default value, if injected afterwards it will be overwritten", () => { + @injectable() + class Bar { + constructor( + @inject("MyString", {defaultProvider: {useValue: "MyDefaultString"}}) + public foo: string + ) {} + } + const str = "NewString"; + globalContainer.register("MyString", {useValue: str}); + + globalContainer.register(Bar, {useClass: Bar}); + + const bar = globalContainer.resolve(Bar); + expect(bar.foo).toEqual(str); +}); + +test("allows inject to have other kinds of provider", () => { + @injectable() + class Bar implements IBar { + public value = ""; + } + + @injectable() + class FooWithInterface { + constructor( + @inject("IBar", {defaultProvider: {useClass: Bar}}) public myBar: IBar + ) {} + } + + const myFoo = globalContainer.resolve(FooWithInterface); + + expect(myFoo.myBar).toBeInstanceOf(Bar); +}); + +test("passing both isOptional and defaultProvider will default to use the defaultProvider", () => { + @injectable() + class Bar implements IBar { + public value = ""; + } + + @injectable() + class FooWithInterface { + constructor( + @inject("IBar", {defaultProvider: {useClass: Bar}, isOptional: true}) + public myBar?: IBar + ) {} + } + + const myFoo = globalContainer.resolve(FooWithInterface); + + expect(myFoo.myBar).toBeInstanceOf(Bar); +}); + // --- @injectAll --- test("injects all dependencies bound to a given interface", () => { diff --git a/src/decorators/inject.ts b/src/decorators/inject.ts index e2c2ae7..06e6f4e 100644 --- a/src/decorators/inject.ts +++ b/src/decorators/inject.ts @@ -1,5 +1,7 @@ import {defineInjectionTokenMetadata} from "../reflection-helpers"; import InjectionToken, {TokenDescriptor} from "../providers/injection-token"; +import {Provider} from "../providers"; +import {instance as globalContainer} from "../dependency-container"; /** * Parameter decorator factory that allows for interface information to be stored in the constructor's metadata @@ -8,7 +10,10 @@ import InjectionToken, {TokenDescriptor} from "../providers/injection-token"; */ function inject( token: InjectionToken, - options?: {isOptional?: boolean} + options?: { + isOptional?: boolean; + defaultProvider?: Provider; + } ): ( target: any, propertyKey: string | symbol | undefined, @@ -19,6 +24,12 @@ function inject( multiple: false, isOptional: options && options.isOptional }; + + if (options && options.defaultProvider) { + // @ts-expect-error options.defaultProvider is the right type but this Typescript version doesn't seem to realize that one of the overloads method would accept this type. + globalContainer.register(token, options.defaultProvider); + } + return defineInjectionTokenMetadata(data); }