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

Abierto
#64,708 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
12/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
go, typescript

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

🔎 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.
Lenguaje dominante
Go
Estrellas
112k
Forks
15.4k
Merge medio
2 d 21 h
PR fusionados (30 d)
126

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de microsoft/TypeScript

Todos los issues de microsoft/TypeScript

Issues similares

Más issues de Go

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.