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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> }` 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.
Expand Down
74 changes: 74 additions & 0 deletions src/__tests__/global-container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(Bar, {useClass: Bar});

const bar = globalContainer.resolve<Bar>(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>(Bar, {useClass: Bar});

const bar = globalContainer.resolve<Bar>(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", () => {
Expand Down
13 changes: 12 additions & 1 deletion src/decorators/inject.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,7 +10,10 @@ import InjectionToken, {TokenDescriptor} from "../providers/injection-token";
*/
function inject(
token: InjectionToken<any>,
options?: {isOptional?: boolean}
options?: {
isOptional?: boolean;
defaultProvider?: Provider<any>;
}
): (
target: any,
propertyKey: string | symbol | undefined,
Expand All @@ -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.
Copy link

Copilot AI Apr 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider refactoring this section to remove the reliance on @ts-expect-error by either upgrading TypeScript or adjusting the overload definitions to properly type-check the defaultProvider.

Copilot uses AI. Check for mistakes.
globalContainer.register(token, options.defaultProvider);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like defaultProvider has the side-effect of making a registration for the token. I'm not sure how I feel about this.

Seems we could get into undefined behavior territory depending on the order things resolve if multiple things use default providers for the same token.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's very true. We almost want something like, on resolve, if there is no registration, then check to see if we have a provider in a list of default providers, that we can check into before throwing an error.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so this may need to go into the token descriptor, similar to isOptional.

Though I wonder if we could get it working with ES6's default parameters syntax, since I feel most usages of this would be for default values as opposed to factories and the like.

(if we're registering a factory, we may as well have a full fledged registration; though I could be wrong and not seeing the right use case yet)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Factories, I could see wanting easily access to the container at runtime. Let me try to put this in the token descriptor to see if that makes more sense.

}

return defineInjectionTokenMetadata(data);
}

Expand Down
Loading