Home > Blockchain >  Why am I getting "Type instantiation is excessively deep and possibly infinite"?
Why am I getting "Type instantiation is excessively deep and possibly infinite"?

Time:01-02

Playground link

I have these general definitions:

type Module<P extends Payloads, C extends Children> = {
    payloads: P;
    children: C;
};

type Children = Record<string, any>; // some values will be nested Modules

type Payloads = Record<string, any>;

type ExtractPayloads<M extends Module<any, any>> = M extends Module<infer P, any> ? P : never;

type ExtractChildren<M extends Module<any, any>> = M extends Module<any, infer C> ? C : never;

Basically, Modules are types that specify a children type, which can contain nested Modules.

I have this type that can generate Actions based on a Module's payload type:

type ModuleRootActions<
  MODULE extends Module<any, any>,
  PAYLOADS = ExtractPayloads<MODULE>,
> = {
  [NAME in keyof PAYLOADS]: {
    type: NAME;
    payload: PAYLOADS[NAME];
  };
}[keyof PAYLOADS];

Next, I have a recursive type that helps me generate Actions for ALL Modules in a Module's tree (i.e., including its child modules, and grandchildren, etc):

type AllModuleActions<
  MODULE extends Module<any, any>,
  CHILDREN = ExtractChildren<MODULE>,
> =
  | ModuleRootActions<MODULE>
  | {
      [KEY in keyof CHILDREN]: CHILDREN[KEY] extends Module<any, any>
        ? AllModuleActions<CHILDREN[KEY]>
        : never;
    }[keyof CHILDREN];

Finally, I have these concrete examples:

type C = Module<{
  "incrementBy": number;
}, {}>;

type B = Module<{
  "setIsSignedIn": boolean;
}, {
  c: C;
}>;

type A = Module<{
  "concat": string;
  "setIsDarkMode": boolean;
}, {
  b: B;
}>;

All my types thus far are correct -- I've verified this manually. Now, I'm writing a function that takes in an Action of a generic Module. I can successfully define these types:

type ConcreteAction<M extends Module<any, any>> = AllModuleActions<M>;

const concreteAction: ConcreteAction<A> = {
  type: "concat",
  payload: "str",
}

But once I try to put them in a generic function, I get the error in the title.

const composedReducer = <MODULE extends Module<any, any>>(
  action: AllModuleActions<MODULE>,
) => {
  if (action) {

  }
};

You'll notice in the Playground link that action has the error: "Type instantiation is excessively deep and possibly infinite". I assume this is happening because the MODULE type is generic, and it's possible that there could be cycles in the Module definition, even though semantically I know that it's a tree.

How can I fix this error? Is there a way to tell the compiler that the graph will always be a tree and never contain infinite cycles?

CodePudding user response:

The AllModuleActions<M> type is recursive in a way that the compiler cannot handle very well. You are indexing arbitrarily deep into an object type all at once to produce a big union type; I've run into this problem before here when generating all the dotted paths of an object type (e.g., {a: {b: string, c: number}} becomes "a.b" | "a.c").

You don't usually see the problem when you evaluate something like AllModuleActions<M> when M is a specific type; but when it's an unspecified generic type parameter (or a type that depends on such a type parameter), you can run into trouble. You might see that "excessively deep" error. Even worse, the compiler tends to get bogged down, spiking CPU usage and slowing down your IDE. I don't know exactly why this happens.

Probably the best advice is not to build types like this. If you have to, then there are some ways I've found to help, but they aren't foolproof:


Sometimes you can cause the compiler to defer evaluation of one of these types by rephrasing it as a distributive conditional type. If M is a generic type parameter and AllModuleActions<M> gives you trouble, maybe M extends any ? AllModuleActions<M> : never won't:

const generic = <M extends Module<any, any>>(
  action: M extends any ? AllModuleActions<M> : never, // <-- defer
) => {
  action; // okay
};

If that doesn't work you can try to explicitly depth-limit your recursive type so that by default things only descend three or four levels:

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

type AllModuleActions<M extends Module<any, any>, D extends Prev[number] = 4> =
  [D] extends [never] ? never :
  | ModuleRootActions<M>
  | {
    [K in keyof M["children"]]: M["children"][K] extends Module<any, any>
    ? AllModuleActions<M["children"][K], Prev[D]>
    : never;
  }[keyof M["children"]];

This is similar to yours, except we have added a D parameter which (by default) starts at 4 and decreases each time AllModuleActions is evaluated (note that Prev[4] is 3 and Prev[3] is 2) until eventually it reaches never and the recursive type bails out:

const generic = <M extends Module<any, any>>(
  action: AllModuleActions<M>
) => {
  action; // okay
};

These workarounds may or may not help for a particular use case, and there may be observable side effects (e.g., types might not be identical; type inference may behave differently; displayed quickinfo might be more complicated), so be careful!

Playground link to code

  • Related