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.
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, npmlatest)Reproduction
Gist: https://gist.github.com/alan-albuquerque/2d7d0c0990618785dc1453344007a190 (
repro.ts,tsconfig.json)The repro defines a builder that accumulates entries into a tuple type parameter:
and chains 20
.command(...)calls; each call after the third readsctx.s3?.text ?? "".Timings (Apple M2 Pro, macOS,
--noEmit):Ablation: deleting
& { [pluginField: string]: unknown }fromCommandSpecgives 20 calls in 14.9s (still superlinear, ~8.5x cheaper).--generateTraceon the 20-call file: 236recursiveTypeRelatedTo_DepthLimitevents and 760structuredTypeRelatedToevents; 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.