Unbounded memory in declaration emit for an inferred type built from non-exported recursive aliases (also hit by `--noEmit --watch`)

未關閉
#64,708 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 1 天內回覆

還沒有人認領這個 Issue。

評估

難度
5/5
預估耗時
一週以上
新手友好度
12/100
Issue 類型
缺陷
描述清晰度
基本清楚
活躍度
活躍
技術堆疊
go, typescript

研究方向

Start with the two-module repro in types.ts and api.ts, then trace the sampled stack: checker NodeBuilderImpl.typeToTypeNode, createMappedTypeNodeFromType and conditionalTypeToTypeNode, which call ast NodeFactory.DeepCloneNode on a recursive alias body. Done means the declaration emit for api.ts terminates with either a truncated type or TS7056, and the noEmit --watch case stays bounded in memory, with the exported-alias variant still producing the same output.

由索引模型根據 Issue 內容生成。

描述

🔎 Search Terms

DeepCloneNode, conditionalTypeToTypeNode, declaration emit out of memory, recursive conditional type, non-exported type alias, TS7056, watch mode memory, noEmit watch

🕗 Version & Regression Information
  • Reproduces on 7.0.2 and on 7.1.0-dev.20261009.1 (macOS, arm64).
  • Not yet compared with 6.0.
⏯ Playground Link

Needs two modules (the helper aliases must live in another file and stay unexported), so the repro is below.

💻 Code
// types.ts
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : [];
type Shift<K, P extends unknown[]> = P[0] extends K ? Tail<P> : never;
type NonUndefined<T> = T extends undefined ? never : T;
type Rec<T, P extends string[]> = T extends object ? { [K in Extract<keyof T, P[0]>]: NonUndefined<Rec<T[K], Shift<K, P>>> } : T;
export type Expanded<T, P extends string> = Rec<T, [P]>;
export interface Entity { id: string }
// api.ts
import type { Entity, Expanded } from "./types";
declare function get<T>(): T;
export const fetch = <P extends string>() => get<Expanded<Entity, P>>();
// tsconfig.json
{ "compilerOptions": { "strict": true, "declaration": true, "emitDeclarationOnly": true, "outDir": "out", "types": [] }, "files": ["types.ts", "api.ts"] }

Run tsc -p .

🙁 Actual behavior

The compiler never finishes. Memory passes 3 GB within about 5 seconds and keeps growing until the process is killed. On a real project with the same pattern in 11 modules, a full declaration emit reached 52 GB in 3.5 minutes before we stopped it.

The same thing happens in a project that never asks for declarations, through watch mode. With "noEmit": true instead of the declaration options, and a third file importing fetch from ./api:

  1. tsc -p . --watch completes the initial build normally (about 90 MB, Found 0 errors).
  2. Append a comment to api.ts.
  3. The watcher starts declaration emit for the changed file and grows without bound (3 GB after 5 to 7 seconds in this repro). In our project the watcher reached a 65 GB footprint.

A plain tsc -p . --noEmit is fine (about 100 MB).

Sampled stack (symbolized from the release binary, condensed):

compiler.(*Program).Emit.func2
compiler.(*emitter).emitDeclarationFile
transformers/declarations.(*DeclarationTransformer).transformVariableDeclaration
transformers/declarations.(*DeclarationTransformer).ensureType
checker.(*NodeBuilderImpl).serializeTypeForDeclaration
checker.(*NodeBuilderImpl).serializeReturnTypeForSignature
checker.(*NodeBuilderImpl).typeToTypeNode
  checker.(*NodeBuilderImpl).createMappedTypeNodeFromType
  checker.(*NodeBuilderImpl).conditionalTypeToTypeNode        (repeats)
    ast.(*NodeFactory).DeepCloneNode
      ast.(*ConditionalTypeNode).VisitEachChild                (very deep)
      ast.(*IndexedAccessTypeNode).VisitEachChild

Most of the remaining CPU time goes to GC (scanObject, findObject) and map lookups under printer.(*EmitContext).SetOriginalEx.

🙂 Expected behavior

Emit terminates, either with a truncated type or with TS7056. The nightly already does that for close variants of this repro:

Variant 7.0.2 7.1.0-dev.20261009.1
Repro as above runaway runaway
Without the NonUndefined<...> wrapper finishes, writes a 2.5 MB api.d.ts TS7056, about 30 MB
Rec<T[K], P> instead of Rec<T[K], Shift<K, P>> finishes, writes a 660 KB api.d.ts TS7056, about 30 MB
Helper aliases exported about 30 MB, 220 byte api.d.ts about 30 MB, 220 byte api.d.ts
Additional information about the issue
  • Workaround: export the helper aliases. The printer can then reference import("./types").Rec<...> instead of inlining the alias body into itself.
  • The real world shape is a common "make these dotted paths required" helper (DeepRequiredObj with a path to tuple split), used as the type argument of a generic API client method without an explicit return type.
  • Possibly related: #63825 (closed without a repro, same DeepCloneNode runaway on the declaration emit path), #63966 / #63969, #64556. The nightly includes those fixes and still reproduces this one.
  • The watch mode part seems worth noting on its own: a noEmit project can run into declaration emit problems just by saving a file, because the builder computes .d.ts signatures for changed files.
主要語言
Go
星號
112k
分支
15.4k
平均合併
3 天 1 小時
30 天內合併 PR
136

環境準備

在 Codespaces 中開啟

在瀏覽器裡用你自己的 GitHub 帳號啟動這個專案的開發容器。

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

microsoft/TypeScript 的其他 Issue

查看 microsoft/TypeScript 的全部 Issue

相似的 Issue

更多 Go Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。