You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
4.7 regression: Array.flat(infinity) leads to "Type instantiation is excessively deep and possibly infinite." errorΒ #49280
I'm not quite sure how to reason about this. This error is, for all intents and purposes, correct. You could have written
interfaceConfig{prop?: string;}interfaceExtendedConfigextendsConfig{}typeNestedConfigs=Array<ExtendedConfig|NestedConfigs>;constconfigs: NestedConfigs[]=[];configs.push(configs);// <- legalconstflattened=configs.flat(Infinity);// <- crashes at runtime
which causes
VM273:1 Uncaught RangeError: Maximum call stack size exceeded
at Array.flat (<anonymous>)
at <anonymous>:1:5
If you have a finitely-deep (T | T[])[] then it seems best to pass a finite argument to flat. Type-asserting Infinity to a lower integer (I'd really recommend 1, not 10, as they actually do different levels of unwrapping) if you're confident that the depth is actually bounded by some unknowably-high integer (????) seems like an appropriate assertion, since you know more than TypeScript does about the data structure in this case.
I'm not sure what I think the correct type of the output is, but it does seem to me that the TypeScript compiler should allow .flat(Infinity) (without type assertion) without straight-up failing the compile since it is valid and common (e.g. example on MDN) JavaScript.
Maybe it would make sense to special-case Infinity to wind up with unknown[] or any[]?
I should also note that, as a user of TypeScript, the "possibly infinite" thing made enough sense to me, but I was a bit stumped how to get around the type error because this kind of error prevents doing a post-cast like configs.flat(Infinity) as Config[].
Type-asserting Infinity to a lower integer (I'd really recommend 1, not 10, as they actually do different levels of unwrapping) if you're confident that the depth is actually bounded by some unknowably-high integer (????) seems like an appropriate assertion, since you know more than TypeScript does about the data structure in this case.
Oh, that's really interesting, I actually didn't know that. So to get more specific, in my use case I was actually using it within a function that allows one to optionally declare a flattening depth. To go off of my example, that might look like:
The intent here was to mimic something like a flat(Infinity), Hence the assertion to a higher integer. I'm assuming that because in this instance depth can be Infinity, that it gives that error. If TS treats the type-assertion with different levels of unwrapping, it might be worth it to lower that depth instantiation just for those gains.
I tend to agree with Grant Dennison (@grant-dennison) above, it seems like .flat(Infinity) feels commonplace enough to warrant TypeScript handling this in a way that makes sense with (T | T[])[] types for flattening them. That being said, I do see how that gets very hairy, very fast.
A potential middle-ground here may be some form of 'non-infinite' integer or number type. To go back to my example:
That way I can tell TypeScript that this is actually bounded by some unknowably-high integer, and have type errors if I try to call fancyFlatten with Infinity
This is solvable now without infinity literal. T6 (@tjjfvi) unfortunately your code only checks direct recursive references not recursive loops, Playground Link
typeA=number|B[]typeB=string|A[]declareconstconfigsBreaking: A[]constconfigsInfinity2=configsBreaking.flat2(1e309);/// ERROR: Type instantiation is excessively deep and possibly infinite.(2589)
Much like a runtime algorithm, the way to prevent the type system from infinitely recusing is by tracking what data structures you've already seen and short circuiting if they are self referencing.
/** * Counter in type context, * `Counter.Value<Counter.ZERO>` gives 0 * `Counter.Value<Counter.Inc<Counter.ZERO>>` gives 1 etc */typeCounter=Counter.Valid;namespaceCounter{/** constraint for type context counters, same as top level `Counter` type. */exportinterfaceValid{[k]: e[]}declareconstk: unique symbol;typee='unused dummy';/** count of zero */exporttypeZERO=Record<typeofk,[]>/** extracts the number literal from a counter */exporttypeValue<CextendsCounter>=C[typeofk]["length"]/** increments a counter to one higher */exporttypeInc<CextendsCounter>={[k]: [e, ...C[typeofk]]}}/** replaces array types with their element type, leaves non array types as is */typeUnwrapArray<T>=TextendsReadonlyArray<infer E> ? E : T/** returns an array type that corresponds to flattening one layer of the list */typeUnwrapOneLayerTuple<Textendsany[]>=Textends[infer A, ...infer R]// if T is a tuple type handle just the first element and recurse to rest of tuple
? AextendsReadonlyArray<any>
? [...A, ...UnwrapOneLayerTuple<R>]
: [A, ...UnwrapOneLayerTuple<R>]// if T is not a tuple type use a homomorphic transformation on the elements// generally it maps [] to [] and (A|B)[] to (UnwrapArray<A>|UnwrapArray<B>)[]
: {[KinkeyofT]: UnwrapArray<T[K]>}typeFlatArray<Textendsunknown[],Depthextendsnumber,/** counter for how many steps down the depth we have traversed */CextendsCounter=Counter.ZERO,/** all possible list states visited so far, if adding a new step doesn't change this we short circuit. */AllStepsextendsunknown[]=T,/** the next step, effectively to cache it. Typescript is probably smart enough to reuse this computation * if present multiple times in the definition but idk how the internals work * so being here ensures it computes consistently based on the input T every time. */NextStepextendsUnwrapOneLayerTuple<T>=UnwrapOneLayerTuple<T>,>=[NextStep,T]extends[T,NextStep]// T (current flattening status) and NextStep are assignable to each other// so either it is finitely nested structure that has been fully flattened// or there is single step recursion like `type P = (1 | P)[]`// Since T and NextStep are resolved differently it is not obvious which would be more useful// to return in this case, I've included both with debugging info for your own reference.
? (T|NextStep)&DEBUG<"no change at finite depth",{depth:Counter.Value<C>,T:T,N:NextStep}>
: [AllSteps|NextStep]extends[AllSteps]// we have some form of recursive loop, such as `type A = 1 | B[]; type B = 2 | A[]`// such that unwrapping more layers results in the same possible types we've already seen// just give basic array type of the viable element types
? (AllSteps[number])[]&DEBUG<"loop recursion",Counter.Value<C>>
: (Counter.Value<C>extendsDepth ? T : never)// add T if the depth parameter contains the current depth// and union the possible result from this depth with the result from the next depth, // as long as there is a next depth to deal with.|(Exclude<Depth,Counter.Value<C>>extendsnever ? never
: FlatArray<NextStep,Exclude<Depth,Counter.Value<C>>,Counter.Inc<C>,AllSteps|NextStep,UnwrapOneLayerTuple<NextStep>>)
This has worked reasonably on all the cases I've thrown at it, I feel like there should be a way to define a type that gets more convoluted as you look deeper in its recursive definition but not sure how to actually construct it to see this throw an 'infinite recursion' error. The one place this does fall short is that negative or fractional depths are handled reasonably by the runtime but treated as "large enough we stopped getting new results from more flattening" by this type definition, I considered using string templates to give unknown[] in those cases but cases like 1.3e100 and 120e-1 would pose issues for that approach.
Another problem that is totally solvable is that passing a number gives a union of all unique depths where as an arbitrarily big number gives only the deepest option so Infinity being typed as number and not a literal is annoying. If it was an identify-able literal we could even strip out array type elements once we stabilize but doing that for recursive types in the same definition that retains tuple-ness in finite structures is tricky. I had a version that did it but it was much harder to test so dropped it while refactoring.
These are the tests I tried organized into namespaces so they are easy to collapse in the playground. There were a few more I tried to get a recursion error but they didn't do anything distinct from the behaviour of these tests. (same link as above)
/** a very large effectively infinite number, but typed as a literal number not 'number' */constInf=1e300;declare global {interfaceArray<T>{flat2<Aextendsunknown[],Dextendsnumber=1>(this: A,depth?: D): FlatArray<A,D>;}}namespaceSimpleRecursion{typeP=(1|P)[];declareconstconfig: P;constres=config.flat2(4asnumber);// ^? (P | (1 | P)[]) & DEBUG<"no change at finite depth", {depth: 0; T: P; N: (1 | P)[];}>// confirm that is equivelent to original nested structure.constsanity_check: P=res;}namespaceOP_case{interfaceConfig{prop?: string;}interfaceExtendedConfigextendsConfig{}exporttypeNestedConfigs=Array<ExtendedConfig|NestedConfigs>;exportdeclareconstconfig: NestedConfigs[];// unsure if the intention was to have a minimum of 2 layers of arrays but can confirm this works.constop_flatten_once: NestedConfigs=config.flat2(1);constres_op=config.flat2(Inf);// ^? (ExtendedConfig | NestedConfigs)[] & DEBUG<...>}namespaceRecursiveLoop{typeA=number|B[]typeB=string|A[]exportdeclareconstconfig: A[];// might be desirable for this to give (number | string)[] instead of (A|B)[]// but that would require an infinity literal type.// and more convoluted handling to not break tuple cases constres_loop=config.flat2(Inf);// ^? (B | A)[] & DEBUG<"loop recursion", 2>}namespaceFiniteTuple{exportdeclareconstconfig: [1,[[[["d"]]]],[],["a",{b: "c"}],];constres1=FiniteTuple.config.flat2();// ^? [1, [[["d"]]], "a", { b: "c"; }]constres2=FiniteTuple.config.flat2(2);// ^? [1, [["d"]], "a", {b: "c"; }]constres3=FiniteTuple.config.flat2(3);// ^? [1, ["d"], "a", {b: "c"; }]constres4=FiniteTuple.config.flat2(4);// ^? [1, "d", "a", {b: "c"; }] constres_inf: typeofres4=FiniteTuple.config.flat2(Inf);typeExp_depth_2_or_3=typeofres2|typeofres3;constres_union: Exp_depth_2_or_3=FiniteTuple.config.flat2(2as(2|3));// note `typeof config` is here because [[1]].flat(0) is valid.typeExp_any_depth=typeofconfig|typeofres1|Exp_depth_2_or_3|typeofres4constres_num: Exp_any_depth=FiniteTuple.config.flat2(Infinityasnumber);/// note that this implementation treats negative or fractional numbers /// as equivelent to arbitrarily big numbers that will fully flatten the array./// I tried using string templates to handle this but 1.3e300 and 100e-1 got treated incorrectly.constres_maybe_problematic_1: typeofres_tuple_inf=FiniteTuple.config.flat2(-3);constres_maybe_problematic_2: typeofres_tuple_inf=FiniteTuple.config.flat2(1.5);}namespaceAnyChecks{declareconstthreeD_matrix: any[][][];// This case probably should give any[] but it short circuits earlier// because of `any` assignability rules.constres=threeD_matrix.flat2(3);// ^? (any[][] | any[][][]) & DEBUG<"no change at finite depth", {depth: 0; T: any[][][]; N: any[][];}>}
I'm not sure if this is sufficiently more complicated than any other types in the standard library that it is undesirable for the complexity, even if we can establish that it works reasonably for every possible case, but this is absolutely doable.
I think the existing solutions using [-1,0,1,2,...][depth] to count down might give better results for fractional or negative indices compared to my solution with the counter. Identifying fractional and negative ones is technically doable with string templates but it seems highly fragile. To be production ready, the solution in the comment above needs some mechanism to differentiate negative or fractional numbers from sufficiently large numbers to flatten the whole array. (also probably more thorough testing) The code I link to in this comment does do this but I'm not sure it is a good fit for the standard library.
Activity
grant-dennison commentedon May 27, 2022
Ran into this today as well in 4.7.2. 4.6 typed the output as
any[]. (playground)sepiette commentedon May 30, 2022
also ran into this in an upgrade from 4.6.4 to 4.7.2. Entire app won't compile as a result.
taylorfsteele commentedon May 31, 2022
Just ran into this as well, here's a very concise example:
Same exact error on
args.flat(Infinity), "Type instantiation is excessively deep and possibly infinite."A quick solution for now is to assert your
.flatargument as an integer:args.flat(Infinity as 10)Should make the error go away.
RyanCavanaugh commentedon Jun 1, 2022
I'm not quite sure how to reason about this. This error is, for all intents and purposes, correct. You could have written
which causes
If you have a finitely-deep (T | T[])[] then it seems best to pass a finite argument to
flat. Type-assertingInfinityto a lower integer (I'd really recommend1, not10, as they actually do different levels of unwrapping) if you're confident that the depth is actually bounded by some unknowably-high integer (????) seems like an appropriate assertion, since you know more than TypeScript does about the data structure in this case.grant-dennison commentedon Jun 1, 2022
I'm not sure what I think the correct type of the output is, but it does seem to me that the TypeScript compiler should allow
.flat(Infinity)(without type assertion) without straight-up failing the compile since it is valid and common (e.g. example on MDN) JavaScript.Maybe it would make sense to special-case
Infinityto wind up withunknown[]orany[]?grant-dennison commentedon Jun 1, 2022
I should also note that, as a user of TypeScript, the "possibly infinite" thing made enough sense to me, but I was a bit stumped how to get around the type error because this kind of error prevents doing a post-cast like
configs.flat(Infinity) as Config[].taylorfsteele commentedon Jun 1, 2022
Oh, that's really interesting, I actually didn't know that. So to get more specific, in my use case I was actually using it within a function that allows one to optionally declare a flattening depth. To go off of my example, that might look like:
The intent here was to mimic something like a
flat(Infinity), Hence the assertion to a higher integer. I'm assuming that because in this instancedepthcan beInfinity, that it gives that error. If TS treats the type-assertion with different levels of unwrapping, it might be worth it to lower that depth instantiation just for those gains.I tend to agree with Grant Dennison (@grant-dennison) above, it seems like
.flat(Infinity)feels commonplace enough to warrant TypeScript handling this in a way that makes sense with (T | T[])[] types for flattening them. That being said, I do see how that gets very hairy, very fast.A potential middle-ground here may be some form of 'non-infinite' integer or number type. To go back to my example:
That way I can tell TypeScript that this is actually bounded by some unknowably-high integer, and have type errors if I try to call
fancyFlattenwithInfinitytjjfvi commentedon Jun 2, 2022
This could be addressed if a literal type for
Infinitywas added: playground link using1e309tadhgmister commentedon Dec 31, 2025
This is solvable now without infinity literal. T6 (@tjjfvi) unfortunately your code only checks direct recursive references not recursive loops, Playground Link
Much like a runtime algorithm, the way to prevent the type system from infinitely recusing is by tracking what data structures you've already seen and short circuiting if they are self referencing.
This does work: (Playground Link including tests)
This has worked reasonably on all the cases I've thrown at it, I feel like there should be a way to define a type that gets more convoluted as you look deeper in its recursive definition but not sure how to actually construct it to see this throw an 'infinite recursion' error. The one place this does fall short is that negative or fractional depths are handled reasonably by the runtime but treated as "large enough we stopped getting new results from more flattening" by this type definition, I considered using string templates to give
unknown[]in those cases but cases like1.3e100and120e-1would pose issues for that approach.Another problem that is totally solvable is that passing a
numbergives a union of all unique depths where as an arbitrarily big number gives only the deepest option soInfinitybeing typed asnumberand not a literal is annoying. If it was an identify-able literal we could even strip out array type elements once we stabilize but doing that for recursive types in the same definition that retains tuple-ness in finite structures is tricky. I had a version that did it but it was much harder to test so dropped it while refactoring.These are the tests I tried organized into
namespaces so they are easy to collapse in the playground. There were a few more I tried to get a recursion error but they didn't do anything distinct from the behaviour of these tests. (same link as above)I'm not sure if this is sufficiently more complicated than any other types in the standard library that it is undesirable for the complexity, even if we can establish that it works reasonably for every possible case, but this is absolutely doable.
tadhgmister commentedon Dec 31, 2025
I think the existing solutions using
[-1,0,1,2,...][depth]to count down might give better results for fractional or negative indices compared to my solution with the counter. Identifying fractional and negative ones is technically doable with string templates but it seems highly fragile. To be production ready, the solution in the comment above needs some mechanism to differentiate negative or fractional numbers from sufficiently large numbers to flatten the whole array. (also probably more thorough testing) The code I link to in this comment does do this but I'm not sure it is a good fit for the standard library.