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

Combination of 20 provideValue()/provideFactory() calls make vscode unusable #50

Open
matteralus opened this issue Apr 11, 2022 · 6 comments

Comments

@matteralus
Copy link

Firstly, I just want to say what a great concept this API is, very clean and easy to use, and that I really would like to use in my code base compared to the other typescript DI frameworks out there.

But as I have been adding more factories vscode has become unusable, when I make a change it will take 10 seconds for the intellisense to catch up (and my fans go on lol). I started out with only 5 .provideFactory()s and everything was fine, but I ported more of my code over and things started to get slow. Now it's clear when I comment out the new .provideFactory()s vscode will start behaving again. Right now I have a total of 4 provideValue() and 15 provideFactory() calls. If I had to guess there is some kind of extra work in tsc because the builder pattern might be nesting copies of same type over an over again in the generics.

There was another issue a few years ago that asked for a non-chained version of the API, I'm not sure if something like that would help in this situation #22

Typescript version I am using is is 4.5.5, vscode typescript default of 4.6.3 does not seem to change anything.

@nicojs
Copy link
Owner

nicojs commented Apr 12, 2022

Would a non-chainable API help you here? I mean, don't you have dependencies that depend on each other?

I.e. would you expect something like this to work?

interface Foo {
  foo: string;
}
class Bar {
  static readonly inject = ['foo'] as const;
  constructor(foo: Foo){ }
}
rootInjector.provide({
   foo: { value: { foo: 'hello'} },
   bar: { clazz: Bar }
})

Because something like that might be impossible to get statically type-checked.

@matteralus
Copy link
Author

matteralus commented Apr 12, 2022

Thanks for the response @nicojs. I see what you are saying. I actually took a stab at this today except using a proxy so I could do stuff like container.dep instead of container.resolve("dep"). The most type safe I could get it is was where the inject tokens had to be part of the keys passed in, and that the factory func args had to at least be one of the resulting types of the factory functions (or static values). It would be great to somehow link the args names to a type. When I have more time I may dig deeper into how you were doing your typing to understand if this is possible but I've kind of got something that unblocks me and has "better than nothing" typing lol. Here is what I ended up with if you are interested:

type AsContainerWithInstanceTypes<TIn extends Record<string, ((...args: any) => any) | { value: any }>> = {
  [prop in keyof TIn as prop]: TIn[prop] extends { value: any }
    ? TIn[prop]["value"]
    : TIn[prop] extends (...args: any) => any
    ? ReturnType<TIn[prop]>
    : unknown;
};

export function createDiContainer<
  T extends Record<
    string,
    | (((...args: AsContainerWithInstanceTypes<T>[keyof AsContainerWithInstanceTypes<T>][]) => any) & {
        inject?: readonly (keyof T)[];
      })
    | { value: any }
  >,
>(
  dependencies: T,
  options?: { logger?: Logger; onResolve?: (prop: string, val: any) => void },
): AsContainerWithInstanceTypes<T> {
  const _containerCache: Map<string, any> = new Map();
  const _onResolve = (prop: string, val: any) => {
    if (options?.onResolve) {
      options?.onResolve(prop, val);
    }
    _containerCache.set(prop, val);
    options?.logger?.debug(`returning ${prop} for first time`);
    return val;
  };

  const handler = {
    get: function (target: any, prop: string, receiver: any) {
      options?.logger?.debug(`resolving ${prop}`);
      if (_containerCache.has(prop)) {
        options?.logger?.debug(`returning ${prop} from cache`);
        return _containerCache.get(prop);
      }

      const targetDep = dependencies[prop];
      if (!targetDep) {
        throw new Error(`dependency "${prop}" not registered with container!`);
      }

      // TODO: implement classes. -mc
      if (!_.isFunction(targetDep)) {
        const resolved = targetDep.value;
        return _onResolve(prop, resolved);
      }

      const paramNames = (targetDep as any).inject || [];
      options?.logger?.debug(
        `${prop} has ${paramNames.length === 0 ? "no dependencies" : `dependencies: ${paramNames.join(", ")}`}`,
      );
      const params = paramNames.map((dep: string) => this.get(target, dep, receiver));
      const resolved = targetDep(...params);
      return _onResolve(prop, resolved);
    },
  };

  return new Proxy(dependencies, handler);
}

@DmitriyNikolenko
Copy link

It was a disaster, I spent a lot of time understanding what was happening with VSCode and eventually, I found this issue.

I really like this library but my project got stuck when the number of provideClass came to 20. I really don't know what to do.

Are there any ways to resolve this issue? Or I should refactor the whole project using another library?(((

@fobdy
Copy link

fobdy commented Aug 24, 2022

The same for me.
I have random Type instantiation is excessively deep and possibly infinite. errors encountered after adding one extra provideFactory.

@synkarius
Copy link

Chiming in to say that I got the same error as @fobdy on my 21st .provideXXXXXX.

@nasuboots
Copy link

The provideXxx method chains create heavily nested TChildContext.

image

So I patched to Injector type to flatten type as follows:

diff --git a/node_modules/typed-inject/dist/src/api/Injector.d.ts b/node_modules/typed-inject/dist/src/api/Injector.d.ts
index 9375bb6..49fed38 100644
--- a/node_modules/typed-inject/dist/src/api/Injector.d.ts
+++ b/node_modules/typed-inject/dist/src/api/Injector.d.ts
@@ -2,13 +2,14 @@ import { InjectableClass, InjectableFunction } from './Injectable';
 import { InjectionToken } from './InjectionToken';
 import { Scope } from './Scope';
 import { TChildContext } from './TChildContext';
 export interface Injector<TContext = {}> {
     injectClass<R, Tokens extends readonly InjectionToken<TContext>[]>(Class: InjectableClass<TContext, R, Tokens>): R;
     injectFunction<R, Tokens extends readonly InjectionToken<TContext>[]>(Class: InjectableFunction<TContext, R, Tokens>): R;
     resolve<Token extends keyof TContext>(token: Token): TContext[Token];
-    provideValue<Token extends string, R>(token: Token, value: R): Injector<TChildContext<TContext, R, Token>>;
-    provideClass<Token extends string, R, Tokens extends readonly InjectionToken<TContext>[]>(token: Token, Class: InjectableClass<TContext, R, Tokens>, scope?: Scope): Injector<TChildContext<TContext, R, Token>>;
-    provideFactory<Token extends string, R, Tokens extends readonly InjectionToken<TContext>[]>(token: Token, factory: InjectableFunction<TContext, R, Tokens>, scope?: Scope): Injector<TChildContext<TContext, R, Token>>;
+    provideValue<Token extends string, R>(token: Token extends keyof TContext ? never : Token, value: R): Injector<{ [K in keyof TContext]: TContext[K]} & { [T in Token]: R }>;
+    provideClass<Token extends string, R, Tokens extends readonly InjectionToken<TContext>[]>(token: Token extends keyof TContext ? never : Token, Class: InjectableClass<TContext, R, Tokens>, scope?: Scope): Injector<{ [K in keyof TContext]: TContext[K]} & { [T in Token]: R }>;
+    provideFactory<Token extends string, R, Tokens extends readonly InjectionToken<TContext>[]>(token: Token extends keyof TContext ? never : Token, factory: InjectableFunction<TContext, R, Tokens>, scope?: Scope): Injector<{ [K in keyof TContext]: TContext[K]} & { [T in Token]: R }>;
     dispose(): Promise<void>;
 }
 //# sourceMappingURL=Injector.d.ts.map
\ No newline at end of file
// argument  (to prevent from providing value with same token)
token: Token
// ↓
token: Token extends keyof TContext ? never : Token

// return type
Injector<TChildContext<TContext, R, Token>>
// ↓
Injector<{ [K in keyof TContext]: TContext[K]} & { [T in Token]: R }>;

https://github.com/nasuboots/typed-inject-types

Then I can chain the methods up to 49 without error. Also, it makes very faster to resolve type checking.

image

Over 49 method chains cause error: "Type instantiation is excessively deep and possibly infinite".
It looks like limitation of TypeScript.
microsoft/TypeScript#34933 (comment)

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

No branches or pull requests

6 participants