You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Regression: recursive conditional+mapped "serialize" type no longer proves Transform<X> assignable to X on deeply-nested compound types (native-preview; works in tsc) #63845
Broken:7.0.0-dev.20260618.1 … 7.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 | bigint → string recursively through objects and arrays, leaving everything else unchanged (so for a type X containing noDate/bigint, Transform<X> is structurally identical to X):
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.
We really need a concrete repro in order to investigate these sorts of things. You can try using the reduce-repro skill in an AI agent if that's available to you.
src/transform.ts — the same generic recursive "serialize" mapped type as in the issue description (maps Date | bigint → string deeply; identity for shapes containing neither):
import{toJsxRuntime}from'hast-util-to-jsx-runtime';importtype{Root}from'hast';importtype{TransformJson}from'./transform';// keep the import live so it is not elidedexportconst_keep=toJsxRuntime;// A JSON client: responses have Dates serialized to strings, i.e. TransformJson<T>.declarefunctionfetchJson<T>(): TransformJson<T>;declarefunctionparseDate(s: string): Date;interfaceIMessage{createdAt: Date;content: Root;}interfaceIAggregate{createdAt: Date;messages: IMessage[];}exportfunctiongetAggregate(): IAggregate{// NOTE the double wrap: fetchJson<T> returns TransformJson<T>, and T here is// itself TransformJson<IAggregate> -> TransformJson<TransformJson<IAggregate>>.constresponse=fetchJson<TransformJson<IAggregate>>();return{
...response,createdAt: parseDate(response.createdAt),messages: response.messages.map(message=>({
...message,createdAt: parseDate(message.createdAt),// `content` is intentionally left untouched, so it stays// Transform<Transform<Root>> and must be checked against Root.})),};}
Reproduce
# stable tsc — clean
npx tsc -p tsconfig.json
# last good native-preview build — clean
npm install --no-save @typescript/native-preview@7.0.0-dev.20260421.2
npx tsgo -p tsconfig.json
# regressed — TS2322
npm install --no-save @typescript/native-preview@7.0.0-dev.20260618.1
npx tsgo -p tsconfig.json
# latest at time of writing — still TS2322
npm install --no-save @typescript/native-preview@7.0.0-dev.20260701.1
npx tsgo -p tsconfig.json
Results
Compiler
Result
tsc 5.9.3
✅ no errors
@typescript/native-preview7.0.0-dev.20260421.2
✅ no errors
@typescript/native-preview7.0.0-dev.20260618.1
❌ TS2322
@typescript/native-preview7.0.0-dev.20260701.1
❌ TS2322
Error on the regressed builds:
src/repro.ts:58:9 - error TS2322: Type '{ content: Transform<Transform<Root, bigint | Date, string>, bigint | Date, string>; createdAt: Date; }[]' is not assignable to type 'IMessage[]'.
The types of 'content.children' are incompatible between these types.
Type 'Transform<Transform<RootContent, ...>, ...>[]' is not assignable to type 'RootContent[]'.
...
Type 'Transform<Transform<Element, ...>, ...>' is not assignable to type 'Element'.
Types of property 'children' are incompatible.
Type 'Transform<Transform<ElementContent, ...>, ...>[]' is not assignable to type 'ElementContent[]'.
...
Transform<Transform<Root>> is structurally identical to Root (the inner Transform maps Date | bigint → string; the outer one finds nothing left to map), so the assignment is valid — as tsc and the 20260421.2 build both confirm.
Notes on the ingredients
All three of these are required — removing any one makes the code compile clean on every version (which is why this was hard to reduce out of a large real project):
The double TransformJson<TransformJson<T>>. A single TransformJson<T> is clean.
A recursively-typed field left un-narrowed — content: Root is spread through unchanged, so the checker must verify Transform<Transform<Root>> ⊆ Root, recursing through hast's mutually-recursive Root → RootContent → Element → ElementContent → Element.
Other recursive AST declarations loaded in the same program. The bare import { toJsxRuntime } from 'hast-util-to-jsx-runtime' (transitively pulling @types/estree, mdast-util-mdx-jsx, hast-util-whitespace, …) is enough; deleting that single import makes the error disappear, even though nothing from it is used in the failing code. In the original project, checking the same file in isolation is clean — the error only appears once a sibling file imports a package with a similar closure. So the failure appears to depend on checker-wide state accumulated from unrelated type declarations, not just on the types in the comparison itself.
Activity
RyanCavanaugh commentedon Jun 24, 2026
We really need a concrete repro in order to investigate these sorts of things. You can try using the reduce-repro skill in an AI agent if that's available to you.
dimofeev commentedon Jul 2, 2026
Here is a minimal, self-contained reproduction (~30 lines of code, public npm packages only).
Setup
tsconfig.json:{ "compilerOptions": { "strict": true, "noEmit": true, "module": "commonjs", "target": "es2022", "lib": ["es2022"], "skipLibCheck": true }, "include": ["src/**/*.ts"] }src/transform.ts— the same generic recursive "serialize" mapped type as in the issue description (mapsDate | bigint→stringdeeply; identity for shapes containing neither):src/repro.ts:Reproduce
Results
tsc5.9.3@typescript/native-preview7.0.0-dev.20260421.2@typescript/native-preview7.0.0-dev.20260618.1TS2322@typescript/native-preview7.0.0-dev.20260701.1TS2322Error on the regressed builds:
Transform<Transform<Root>>is structurally identical toRoot(the innerTransformmapsDate | bigint→string; the outer one finds nothing left to map), so the assignment is valid — astscand the20260421.2build both confirm.Notes on the ingredients
All three of these are required — removing any one makes the code compile clean on every version (which is why this was hard to reduce out of a large real project):
TransformJson<TransformJson<T>>. A singleTransformJson<T>is clean.content: Rootis spread through unchanged, so the checker must verifyTransform<Transform<Root>> ⊆ Root, recursing through hast's mutually-recursiveRoot → RootContent → Element → ElementContent → Element.import { toJsxRuntime } from 'hast-util-to-jsx-runtime'(transitively pulling@types/estree,mdast-util-mdx-jsx,hast-util-whitespace, …) is enough; deleting that single import makes the error disappear, even though nothing from it is used in the failing code. In the original project, checking the same file in isolation is clean — the error only appears once a sibling file imports a package with a similar closure. So the failure appears to depend on checker-wide state accumulated from unrelated type declarations, not just on the types in the comparison itself.jakebailey commentedon Jul 2, 2026
Bisects to microsoft/typescript-go#3445
jakebailey commentedon Jul 2, 2026
Possibly the same explanation as microsoft/typescript-go#4451 though I'm no expert
jakebailey commentedon Jul 2, 2026
Not so minimal repro
jakebailey commentedon Jul 2, 2026
More minimal:
jakebailey commentedon Jul 2, 2026
Anders Hejlsberg (@ahejlsberg) Is this exactly like microsoft/typescript-go#4451?
jakebailey commentedon Jul 2, 2026
Thinking harder, I'm 99% percent certain it's exactly the same and this is working as intended.
ahejlsberg commentedon Jul 2, 2026
Agree, this seems exactly like microsoft/typescript-go#4451. And thus working as intended.