Skip to content

4.7 regression: Array.flat(infinity) leads to "Type instantiation is excessively deep and possibly infinite." errorΒ #49280

Open

Description

Bug Report

πŸ”Ž Search Terms

  • Array.flat
  • Type instantiation is excessively deep and possibly infinite.
  • Nested arrays

πŸ•— Version & Regression Information

  • This changed between versions 4.6 and 4.7

⏯ Playground Link

TypeScript Workbench with relevant code

πŸ’» Code

interface Config {
  prop?: string;
}

interface ExtendedConfig extends Config {}

type NestedConfigs = Array<ExtendedConfig | NestedConfigs>;

const configs: NestedConfigs[] = [];

const flattened = configs.flat(Infinity);

πŸ™ Actual behavior

TypeScript raises the following error for the configs.flat(Infinity) call:

Type instantiation is excessively deep and possibly infinite. ts(2589)

πŸ™‚ Expected behavior

No error should be raised.

Activity

  1. grant-dennison commented on May 27, 2022

    @grant-dennison

    Ran into this today as well in 4.7.2. 4.6 typed the output as any[]. (playground)

  2. sepiette commented on May 30, 2022

    @sepiette

    also ran into this in an upgrade from 4.6.4 to 4.7.2. Entire app won't compile as a result.

  3. taylorfsteele commented on May 31, 2022

    @taylorfsteele

    Just ran into this as well, here's a very concise example:

    type StringOrStringArray = string | StringOrStringArray[];
    
    const flatten = (...args: StringOrStringArray[]) => args.flat(Infinity);
    

    Same exact error on args.flat(Infinity), "Type instantiation is excessively deep and possibly infinite."

    A quick solution for now is to assert your .flat argument as an integer:

    args.flat(Infinity as 10)

    Should make the error go away.

  4. RyanCavanaugh commented on Jun 1, 2022

    @RyanCavanaugh
    Member

    I'm not quite sure how to reason about this. This error is, for all intents and purposes, correct. You could have written

    interface Config {
      prop?: string;
    }
    
    interface ExtendedConfig extends Config {}
    
    type NestedConfigs = Array<ExtendedConfig | NestedConfigs>;
    
    const configs: NestedConfigs[] = [];
    configs.push(configs); // <- legal
    const flattened = 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.

  5. grant-dennison commented on Jun 1, 2022

    @grant-dennison

    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[]?

  6. grant-dennison commented on Jun 1, 2022

    @grant-dennison

    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[].

  7. taylorfsteele commented on Jun 1, 2022

    @taylorfsteele

    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:

    type StringOrStringArray = string | StringOrStringArray[];
    
    const curriedFlatten = (depth = 10) => (...args: StringOrStringArray[]) => args.flat(depth); // <--- Same instantiation error
    

    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:

    type StringOrStringArray = string | StringOrStringArray[];
    
    const fancyFlatten = (depth: NonInfiniteNumber, ...args: StringOrStringArray[]) => args.flat(depth);
    

    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

  8. added
    BugA bug in TypeScript
    Cursed?It's likely this is extremely difficult to fix without making something else much, much worse
    on Jun 1, 2022
  9. tjjfvi commented on Jun 2, 2022

    @tjjfvi
    Contributor

    This could be addressed if a literal type for Infinity was added: playground link using 1e309

  10. tadhgmister commented on Dec 31, 2025

    @tadhgmister

    This is solvable now without infinity literal. T6 (@tjjfvi) unfortunately your code only checks direct recursive references not recursive loops, Playground Link

    type A = number | B[]
    type B = string | A[]
    
    declare const configsBreaking: A[]
    
    const configsInfinity2 = 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.

    This does work: (Playground Link including tests)

    /** 
     * Counter in type context, 
     * `Counter.Value<Counter.ZERO>` gives 0
     * `Counter.Value<Counter.Inc<Counter.ZERO>>` gives 1 etc
     */
    type Counter = Counter.Valid;
    namespace Counter {
      /** constraint for type context counters, same as top level `Counter` type. */ 
      export interface Valid { [k]: e[] }
      declare const k: unique symbol;
      type e = 'unused dummy';
    
      /** count of zero */
      export type ZERO = Record<typeof k,[]>
      /** extracts the number literal from a counter */
      export type Value<C extends Counter> = C[typeof k]["length"]
      /** increments a counter to one higher */
      export type Inc<C extends Counter> = {[k]: [e, ...C[typeof k]]}
    }
    
    /** replaces array types with their element type, leaves non array types as is */
    type UnwrapArray<T> = T extends ReadonlyArray<infer E> ? E : T
    /** returns an array type that corresponds to flattening one layer of the list */
    type UnwrapOneLayerTuple<
        T extends any[]
    > = T extends [infer A, ...infer R]
        // if T is a tuple type handle just the first element and recurse to rest of tuple
        ? A extends ReadonlyArray<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>)[]
        : {[K in keyof T]: UnwrapArray<T[K]>}
    
    type FlatArray<
      T extends unknown[], 
      Depth extends number, 
      /** counter for how many steps down the depth we have traversed */
      C extends Counter = Counter.ZERO,
      /** all possible list states visited so far, if adding a new step doesn't change this we short circuit. */ 
      AllSteps extends unknown[] = 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.
       */
      NextStep extends UnwrapOneLayerTuple<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> extends Depth ? 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>> extends never ? 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' */
    const Inf = 1e300;
    
    
    declare global {
      interface Array<T> {
        flat2<A extends unknown[], D extends number = 1>(this: A, depth?: D): FlatArray<A, D>;
      }
    }
    namespace SimpleRecursion {
      type P = (1|P)[];
      declare const config: P;
      const res = config.flat2(4 as number);
      //    ^? (P | (1 | P)[]) & DEBUG<"no change at finite depth", {depth: 0; T: P; N: (1 | P)[];}>
      // confirm that is equivelent to original nested structure.
      const sanity_check: P = res;
    }
    
    namespace OP_case {
      interface Config {
        prop?: string;
      }
      interface ExtendedConfig extends Config {}
      export type NestedConfigs = Array<ExtendedConfig | NestedConfigs>;
      export declare const config: NestedConfigs[];
    
      // unsure if the intention was to have a minimum of 2 layers of arrays but can confirm this works.
      const op_flatten_once: NestedConfigs = config.flat2(1);
      const res_op = config.flat2(Inf);
      //    ^?  (ExtendedConfig | NestedConfigs)[] & DEBUG<...>
    
    }
    namespace RecursiveLoop {
      type A = number | B[]
      type B = string | A[]
      export declare const config: 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 
      const res_loop = config.flat2(Inf);
      //    ^? (B | A)[] & DEBUG<"loop recursion", 2>
    }
    namespace FiniteTuple{
      export declare const config: [
        1, 
        [[[["d"]]]],
        [], 
        [
          "a", 
          { b: "c" }
        ], 
      ];
    
      const res1 = FiniteTuple.config.flat2();
      //    ^? [1, [[["d"]]], "a", { b: "c"; }]
      const res2 = FiniteTuple.config.flat2(2);
      //    ^? [1, [["d"]], "a",  {b: "c"; }]
      const res3 = FiniteTuple.config.flat2(3);
      //    ^? [1, ["d"], "a",  {b: "c"; }]
      const res4 = FiniteTuple.config.flat2(4);
      //    ^? [1, "d", "a",  {b: "c"; }] 
    
      const res_inf: typeof res4 = FiniteTuple.config.flat2(Inf);
      type Exp_depth_2_or_3 = typeof res2 | typeof res3;
      const res_union: Exp_depth_2_or_3 = FiniteTuple.config.flat2(2 as (2|3));
      // note `typeof config` is here because [[1]].flat(0) is valid.
      type Exp_any_depth = typeof config | typeof res1 | Exp_depth_2_or_3 | typeof res4
      const res_num: Exp_any_depth = FiniteTuple.config.flat2(Infinity as number);
    
    
      /// 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.
      const res_maybe_problematic_1: typeof res_tuple_inf = FiniteTuple.config.flat2(-3);
      const res_maybe_problematic_2: typeof res_tuple_inf = FiniteTuple.config.flat2(1.5);
    }
    
    namespace AnyChecks {
      declare const threeD_matrix: any[][][];
      // This case probably should give any[] but it short circuits earlier
      // because of `any` assignability rules.
      const res = 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.

  11. tadhgmister commented on Dec 31, 2025

    @tadhgmister

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptCursed?It's likely this is extremely difficult to fix without making something else much, much worseDomain: check: Type Circularitystackoverflows caused by circularities, or unexpected errors where no circularity appears to exist

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions