Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @injectOptional() to handle optional constructor args #182

Open
willieseabrook opened this issue Oct 28, 2021 · 3 comments
Open

Add @injectOptional() to handle optional constructor args #182

willieseabrook opened this issue Oct 28, 2021 · 3 comments

Comments

@willieseabrook
Copy link

Is your feature request related to a problem? Please describe.

I'm about 2 days in to using tsyringe so I might be totally lost, but I can't see how to support optional constructor parameters out of the box.

  constructor(
    @inject('MyRequiredService') runtime: MyRequiredService,
    @inject('MyOptionalService') config?: MyOptionalService,
    ) {

If you look at the constructor above, config? is optional, and this is something the logic of my class expects.

However, if I do not register 'MyOptionalService', tsyringe will throw an error that it could not find a registration for 'MyOptionalService'

The logic of my code is that sometimes 'MyOptionalService' would be registered, sometimes it would not be registered.

Alternate solutions

My workaround is to hack with @injectWithTransform.

This is verbose.

And will (I think) only work with the global container, where as I am using child containers.

class Optional {
  getOptionalValue(token: string): unknown {
    try {
      return container.resolve(token)
    }
    catch (e) {
      return undefined
    }
  }
}

class OptionalTransform {
  public transform(optional: Optional, token: string): unknown {
    return optional.getOptionalValue(token)
  }
}

Then:

  constructor(
    @inject('MyRequiredService') runtime: MyRequiredService,
    @injectWithTransform(Optional, OptionalTransform, 'MyOptionalService') config?: MyOptionalService,
    ) {

Proposed Solution

Add @injectOptional() so that the following would work:

  constructor(
    @inject('MyRequiredService') runtime: MyRequiredService,
    @injectOptional('MyOptionalService') config?: MyOptionalService,
    ) {

@injectOptional would not throw an error if the 'MyOptionalService' is not registered, instead it would simply inject undefined.

Additional context

Inversify supports this feature. Just because that supports it doesn't mean tsyringe should, but it is an example implementation of how other people have approached this feature.

https://github.com/inversify/InversifyJS/blob/master/wiki/optional_dependencies.md

@MeltingMosaic
Copy link
Collaborator

Yeah, this has come up a time or two. Both Inversify and Angular implement it as well. I have been reticent to follow suit, because one thing I have always liked about DI is that there is a guarantee that either you get all of your parameters or you fail to resolve. Still, if people are performing workarounds like you have above, it seems sensible to allow users to opt into an optional resolution.

I'd at least look at a PR for this.

@JinuPC
Copy link

JinuPC commented Dec 10, 2021

I have extended @willieseabrook solution by introducing a custom decorator

export function injectOptional(token:string) {
    return injectWithTransform(Optional, OptionalTransform, token);
}

Now we can use this decorator inside the constructors

constructor(
    @inject('MyRequiredService') runtime: MyRequiredService,
    @injectOptional('MyOptionalService') config?: MyOptionalService,
    ) {

@eduardvercaemer
Copy link

eduardvercaemer commented Mar 24, 2024

I came up with this solution to overcome the coupling with the root container abusing factory providers a little bit:

Full example:

container.register("TransientContainer", { useFactory: (c) => c });

@injectable()
class Optional {
  constructor(
    @inject("TransientContainer")
    private readonly container: DependencyContainer,
  ) {}

  getOptionalValue(token: InjectionToken): unknown {
    try {
      return this.container.resolve(token);
    } catch (e) {
      return null;
    }
  }
}

class OptionalTransform {
  public transform(optional: Optional, token: InjectionToken): unknown {
    return optional.getOptionalValue(token);
  }
}

function injectOptional(token: InjectionToken) {
  return injectWithTransform(Optional, OptionalTransform, token);
}

const childA = container.createChildContainer();
const childB = container.createChildContainer();
childA.register("Bar", { useValue: "my bar" });
const barA = childA.resolve(Foo).bar; // my bar!
const barB = childB.resolve(Foo).bar; // null

I am a little wary of the implications of calling resolve during another resolve chain but so far seems to work ok.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants