microsoft / tsyringe

Lightweight dependency injection container for JavaScript/TypeScript
MIT License
5.19k stars 172 forks source link

Injection decorators not working for Singletons with Interfaces #189

Open 39otrebla opened 2 years ago

39otrebla commented 2 years ago

Describe the bug Not sure whether this is a bug or I'm missing something, but surely it is a weird behavior (at least coming from other DI frameworks like Swift's Resolver).

I can't find a way to register two Singletons and inject the first one into the second one. I did nothing different than what I see in the Readme's Interface example, except that I need those implementations to be Singletons, thus registered with registerSingleton (which, by the way, I don't see documented in the Readme).

I also made an attempt with the following, with no luck:

container.register<Services.LoggerService>(
    LoggerServiceName,
    {
      useClass: Adapters.LogAdapter,
    },
    { lifecycle: Lifecycle.Singleton },
  );

Everything works perfectly using the exact same code but resolving without decorators:

export class RouterAdapter implements RouterService<T1, T2> {
  constructor() {
    this.logger = container.resolve<LoggerService>(LoggerServiceName);
  }
  [...]
}

To Reproduce Setup a ReactNative project with Typescript, following the official ReactNative doc. Also, follow the Tsyringe Getting Started to add required dependencies and TS configs (reflect-metadata, etc, etc). Then try the following:

//////////// index.js
import "reflect-metadata";
import "./ioc/setup.ts";

//////////// /ioc/setup.ts
import { container } from "tsyringe";
import { LoggerServiceName, RouterServiceName } from "./registry.ts";
import * as Services from "@ports"; // index for all services/ports
import * as Adapters from "@adapters"; // index for all adapters
import { T1, T2 } from "my-navigation-library";

container.registerSingleton<Services.LoggerService>(
  LoggerServiceName,
  Adapters.LoggerAdapter,
);

container.registerSingleton<Services.RouterService<T1, T2>>(
  RouterServiceName,
  Adapters.RouterAdapter,
);

//////////// ioc/registry.ts
export const LoggerServiceName = "LoggerService";
export const RouterServiceName = "RouterService";

//////////// adapters/router.adapter.ts
import { LoggerService, RouterService } from "@ports";
import { LoggerServiceName } from "@registry";
import { T1, T2 } from "my-navigation-library";

@injectable() // I also did an attempt with @authInjectable(), no luck
export class RouterAdapter implements RouterService<T1, T2> {
  constructor(@inject(LoggerServiceName) private logger?: LoggerService) {}
  [...]
}

//////////// adapters/logger.adapter.ts
import { LoggerService } from "@ports";

export class LoggerAdapter implements LoggerService {
  [...]
}

//////////// app/firstScreen.js
import { container } from "tsyringe";
import { RouterServiceName } from "@registry";

someFunction() {
  const router = container.resolve(RouterServiceName);
  // throws the following
  // Cannot inject the dependency at position #0 of "RouterAdapter" constructor. Reason: Attempted to construct an undefined constructor. Could mean a circular dependency problem. Try using `delay` function.
}

Expected behavior

  1. RouterAdapter gets instantiated upon first resolve() call, and that instance should be "cached" in the container
  2. LoggerAdapter gets instantiated upon first resolve() call, aand that instance should be "cached" in the container
  3. If the first resolve() call happens to be the injection into a class constructor, it should work

Version: 4.6.0

carlossalasamper commented 2 years ago

Im in a very similar situation. React Native, TypeScript and trying to inject a singleton interface implementation by constructor, but is not found

ArtVolchara commented 1 year ago

Looks like prevention from falling to Service Locator anti-pattern ;)

leppaott commented 1 year ago

Can't seem to get this working even the README way...

If we have such a decorator:

export function Decorator<T>(): (target: constructor<T>) => any {
  return (target: constructor<T>): void => {
   singleton(target);

I woul've expected something like this to work:

export function Decorator<T>(): (target: constructor<T>) => any {
  return (target: constructor<T>): void => {
   singleton(delay(() => target));

any workarounds?

Was able to come up with.

export function delayInjection<T>(target: constructor<T>): T {
  return delay(() => target).createProxy((ctor) => globalContainer.resolve(ctor));
}

class A {
  private service: Service = delayInjection(Service);