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 Action
s 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 Action
s 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!