Ask
156

ts2589 excessively deep on a dot-path type that worked at 4 levels of nesting Solved

TypeScript 5.8. I have the standard recursive path type over a config object:

type Path<T> = T extends object
  ? { [K in keyof T & string]:
        T[K] extends object ? K | `${K}.${Path<T[K]>}` : K
    }[keyof T & string]
  : never

Worked fine while the config was four levels deep. I added one more level and now get(config, "services.api.http.server.port") gives me:

error TS2589: Type instantiation is excessively deep and possibly infinite.

The config has 9 top-level keys and nothing recursive in it, so it is not actually infinite. Is there a way to make this scale or is the whole approach a dead end?

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @orm_tamsin · 2w ago · 3 replies

    Not a dead end, but you need a depth counter. The compiler is not confused, it hit its instantiation budget, and it has no way to know your object bottoms out.

    type Prev = [never, 0, 1, 2, 3, 4, 5, 6]
    
    type Path<T, D extends number = 4> =
      [D] extends [never] ? never
      : T extends readonly unknown[] ? never
      : T extends object
        ? { [K in keyof T & string]:
              T[K] extends object ? K | `${K}.${Path<T[K], Prev[D]>}` : K
          }[keyof T & string]
        : never
    

    The thing to internalise is that depth is not the real cost, the cross product is. Nine top keys with seven children each with six each with five is roughly 1,890 distinct string literals, and every one of them is a template literal instantiation the checker has to materialise. Add a level and you multiply, not add.

    Capping at 4 means anything deeper falls back to never for the tail, which in practice you handle by accepting string past that depth. Nobody has ever complained about not getting autocomplete at level six.

    104
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @wellhead_wanda · last wk.

      That fixed it, and the editor went from three seconds of lag on that file to instant. The array exclusion mattered more than I expected too — I had a plugins: [] in there.

      33
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @seedstart_sim · last wk.

      The array exclusion is doing a lot of quiet work in that snippet. Without it you enumerate length, toString, flatMap and about thirty other members at every level an array appears, which is usually where these things go from slow to impossible.

      21
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @seedstart_sim · 2w ago · 2 replies

    Worth learning to measure this rather than guessing at it, because the next one will be a different type.

    tsc -p . --generateTrace ./trace --incremental false
    

    Then drop trace/trace.json into a trace viewer and look for the wide bars under checkSourceFile and structuredTypeRelatedTo. tsc --diagnostics also prints instantiation counts, and if that number has a comma in an unexpected place you know where you stand before you have read a single line of type code.

    In my experience there are three usual culprits: a schema library chain producing a giant inferred type, a dependency shipping enormous conditional types, and exactly this — a path or deep-partial helper that someone wrote when the object was small.

    58
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @wellhead_wanda · last wk.

      Trace put that one type at 2.9 seconds of a 7 second check. I had no idea you could get that granular without instrumenting anything.

      15
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @resole_ruth · 2w ago · 2 replies

    Pragmatic question: do you actually need dot paths at all? config.services.api.http.server.port costs zero type instantiations, autocompletes better, refactors correctly under rename, and is shorter to read. Dot-path types are a genuinely impressive trick that quietly taxes every person who opens that file.

    I have removed two of these from codebases and nobody noticed anything except that the editor got faster.

    37
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @orm_tamsin · last wk.

      Agreed in the general case, and I would still keep it where the path arrives as a runtime string you want checked against the shape — feature flag keys, translation keys, that sort of thing. That is the one place it earns its cost, and it is also the place where capping the depth is painless.

      19
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @first_repo_finn · last wk.

    Before you reach for the depth cap, check how config is typed. If it is annotated as Record<string, unknown> or inferred without a const assertion somewhere in the chain, keyof T & string is string rather than a literal union, and then the recursion genuinely has no bottom — that produces the same TS2589 for a completely different reason and the depth cap only hides it.

    const config = { ... } as const satisfies AppConfig and see whether the error changes shape.

    24
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @kombucha_kai · last wk. · 2 replies

    // @ts-ignore on that line and move on. TS2589 is the compiler being conservative, the type is fine at runtime.

    13
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @rollback_rae · 2w ago

      It suppresses the diagnostic, it does not suppress the work. The checker still tries to instantiate that type at every call site, so your editor stays slow and your build stays slow, and now nothing tells you which file is responsible.

      11
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report