Skip to content

Regression: recursive conditional+mapped "serialize" type no longer proves Transform<X> assignable to X on deeply-nested compound types (native-preview; works in tsc) #4408

Description

@dimofeev

Environment

  • @typescript/native-preview (tsgo)
  • Last good: 7.0.0-dev.20260421.2
  • Broken: 7.0.0-dev.20260618.17.0.0-dev.20260622.1 (latest) — all fail
  • Stable typescript@5.9.3: OK (accepts the code)

So: same source, only the native-preview version changed → a tsgo regression somewhere between 20260421.2 and 20260618.1.

What breaks

A common recursive "serialize" utility type — maps Date | bigintstring recursively through objects and arrays, leaving everything else unchanged (so for a type X containing no Date/bigint, Transform<X> is structurally identical to X):

type Transform<Base, From, To> = {
  [K in keyof Base]: Exclude<Base[K], undefined | null> extends never ? Base[K]
    : Exclude<Base[K], undefined | null> extends object ? (Exclude<Base[K], undefined|null> extends From ? To | Extract<Base[K], null|undefined> : Transform<Base[K], From, To>)
    : Base[K];
};
type TransformJson<T> = Transform<T, Date | bigint, string>;

For a deeply-nested compound type — recursion + arrays + an 8-member discriminated union with a shared base ({base} & ({type:1;…} | {type:2;…} | …)) nested several levels behind arrays — tsgo fails to prove TransformJson<X> assignable to X even when X has no Date/bigint (i.e. when the transform is provably the identity):

error TS2322: Type 'Transform<…, bigint | Date, string>' is not assignable to type '…'.
  Type 'Transform<{base} & (…union…), bigint | Date, string>' is not assignable to '{base} & (…union…)'.

tsc proves this assignable; tsgo (new builds) does not.

Diagnosis / things tried (in case it helps narrow the regressed area)

  • Adding an up-front "does this subtree contain From?" guard so the transform returns Base verbatim for no-From subtrees → flips the failure into TS2589 "Type instantiation is excessively deep and possibly infinite" across the project, i.e. the extra recursive pass exhausts a depth/instantiation budget.
  • Bounding the transform's recursion depth (no extra pass) → tsgo then fully expands the type (no TS2589) but still rejects the expanded {base} & union intersection-with-union assignment.
  • So it looks like two compounding limits: (a) a recursion/instantiation budget that's tighter or counted differently than in tsc, and (b) assignability of a mapped-type result back to an intersection-with-union, at depth.

Reproduction note (the awkward part)

I could not reduce this to a small standalone repro: the minimal/standalone versions of the exact pattern (even with the same 8-member intersection-with-union behind arrays) compile clean under both tsgo and tsc. It only reproduces inside a large real project (many hundreds of interacting Transform<…> instantiations across the program), which points at a scale-dependent / cumulative instantiation budget rather than a single-type defect.

Happy to help bisect against the dev-build range, or to share a private reproduction with the team if that's an option.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions