Skip to content

Performance: exponential check time as a chain of generic calls grows (index signature in the inferred spec type) #64378

Description

Summary

Type-check time for a chain of generic calls grows exponentially with the number of calls when the spec argument's inferred type carries a string index signature (& { [key: string]: unknown }). A 13-call chain checks in 0.12s; 20 calls take ~127s (~2.7x per added call). Removing the index signature from the spec type makes the same 20-call program check in ~15s.

Version

typescript@7.0.2 (native, npm latest)

Reproduction

Gist: https://gist.github.com/alan-albuquerque/2d7d0c0990618785dc1453344007a190 (repro.ts, tsconfig.json)

npx -p typescript@7.0.2 tsc -p tsconfig.json --noEmit

The repro defines a builder that accumulates entries into a tuple type parameter:

type CommandSpec<C, Prior, Inputs extends Record<string, unknown>, Outputs extends Record<string, OutputPortDecl>> = {
  // ...
  args?: (paths: Inputs, ctx: C) => readonly string[];
} & { [pluginField: string]: unknown };

interface StepBuilder<Steps extends readonly StepBuilderEntry[] = readonly [], /* ... */> {
  command<const Id extends string, const Inputs extends Record<string, unknown> = {}, const Outputs extends Record<string, OutputPortDecl> = {}>(
    id: Id,
    spec: CommandSpec<StepCtxIn<[...Steps, ...Values], InBody>, PriorHandles<Steps>, Inputs, Outputs>,
  ): StepBuilder<[...Steps, StepEntry<Id, CommandSpec<StepCtxIn<[...Steps, ...Values], InBody>, PriorHandles<Steps>, Inputs, Outputs>>], /* ... */>;
}

and chains 20 .command(...) calls; each call after the third reads ctx.s3?.text ?? "".

Timings (Apple M2 Pro, macOS, --noEmit):

calls time
13 0.12s
14 0.36s
15 0.97s
16 2.58s
20 126.5s

Ablation: deleting & { [pluginField: string]: unknown } from CommandSpec gives 20 calls in 14.9s (still superlinear, ~8.5x cheaper).

--generateTrace on the 20-call file: 236 recursiveTypeRelatedTo_DepthLimit events and 760 structuredTypeRelatedTo events; nearly all checker self-time lands on the single chained call expression.

Real-world impact

A workflow DSL with this shape (accumulated tuple, mapped ctx values, index-signature spec for plugin extension fields) produces ~20-call chains: checking four such files costs ~324s for a package's declaration build and ~400-840s for a workspace-wide type-aware pass. typescript@6.0.0-beta (JS) takes 1785s on the same files, so 7.0.2 is much faster, but the growth is still exponential and forces us to drop work from the hot path.

Expected behavior

Time roughly linear in the number of chained calls (or at least polynomial with a small exponent), as in Chains of 13 or fewer today.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions