Skip to content

TS5115 in published Zod 4.5–4.6 types after #64372 #64605

Description

After #64372 Zod 4.5.0-4.6.5 break in TS 7.1 nightlies. I'm merging a fix Zod side, but the affected versions account for ~62M weekly downloads. In the long term I think it's a good price to pay for the improved recursive inference stuff, and not that many people use z.json(), but just wanted to make sure all options were explored for avoiding this break (as long is it doesn't involve dropping any functionality from #64372 et al). 👍


🔎 Search Terms

TS5115, "appear infinitely circular", base type arguments, zod, 64372

🕗 Version & Regression Information

⏯ Playground Link

No response

💻 Code

interface Internals<I = unknown> { input: I }
interface Schema { _zod: Internals }
type RecordInput<V extends Schema> = V extends unknown ? Record<string, V["_zod"]["input"]> : never;
interface RecordSchema<V extends Schema> extends Schema { _zod: Internals<RecordInput<V>> }
interface UnionInternals<T extends readonly Schema[]> extends Internals<T[number]["_zod"]["input"]> {}
interface UnionSchema<T extends readonly Schema[]> extends Schema { _zod: UnionInternals<T> }
type JsonValue = { [k: string]: JsonValue };
type _Json = UnionSchema<[RecordSchema<Json>]>;
type _JsonInternals = _Json["_zod"];
interface JsonInternals extends _JsonInternals { input: JsonValue }
interface Json extends _Json { _zod: JsonInternals }

With the published package (zod@4.6.5, skipLibCheck: true):

import * as z from "zod";
export const A = z.array(z.json()); // TS5115

🙁 Actual behavior

TS5115 "Instantiations of the following types appear infinitely circular" on RecordSchema<Json>. The same shape is Zod's z.json() type, so Zod 4.5.0 through 4.6.5 fail on 7.1:

  • with skipLibCheck: false, every project that imports Zod gets TS5115 from Zod's own declarations
  • with skipLibCheck: true, z.array(z.json()), z.record(z.string(), z.json()) or .parse() on an object holding z.json() reports it in user code, and the record's input type becomes Record<string, any>

🙂 Expected behavior

No error, as in 7.0.2.

Additional information about the issue

Those versions get about 62M of Zod's 365M weekly npm downloads. The member-declaration fix from #64372 (comment) is merged in colinhacks/zod#6645 and goes out in the next Zod release, but projects that stay on 4.5.0–4.6.5 will still break when they move to 7.1.

Activity

  1. RedesignedRobot commented on Oct 8, 2026

    @RedesignedRobot

    I traced this one while checking Zod 4.6.5 on the 7.1 nightlies.

    Cause: #64372 stopped resolveObjectTypeMembers from storing declared members before the base types resolve. When a base type argument refers back to the type, member resolution starts over and instantiates the same bases again until the depth limit.

    In Zod, $ZodRecordInternals<ZodString, ZodJSONSchema> has $InferZodRecordInput in its base type arguments. That reads optin through IsOptionalIn over the union members, which leads back to $ZodRecordInternals<ZodString, ZodJSONSchema> itself. But that type declares optin, and addInheritedMembers never replaces a declared value member, so the answer is known before the bases resolve. The cycle isn't real. The reduction above reaches the same point through isStringIndexSignatureOnlyType.

    A fix that keeps what #64372 guarantees: while resolveObjectTypeMembers adds inherited members, keep the type's declared members on a small stack. In that window:

    • getPropertyOfTypeEx returns a value member the type declares itself, the same symbol the resolved members end up holding
    • isStringIndexSignatureOnlyTypeWorker returns false for a type that declares a property

    Every other query resolves the members again, as it does today, and nothing partial is stored on the type. It's 54 lines in checker.go.

    Results at 673a5f1:

    • the reduction above, and zod@4.6.5 with z.json() in arrays, records, unions, tuples, maps and .pipe(), with skipLibCheck on and off: all clean
    • TestLocal, the fourslash tests (including TestNoGhostErrors) and the checker tests pass
    • one baseline moves: mutuallyRecursiveInference.errors.txt loses the TS5114 that Restore idempotency to resolveObjectTypeMembers #64372 added. I think that one was a false cycle too, since X[X['a']] only reads members X declares. keyofGenericExtendingClassDoubleLayer keeps its TS5115.

    Happy to open a PR if this direction works for you.

  2. rsnodgrass commented on Oct 9, 2026

    @rsnodgrass

    Confirmed that this still reproduces with typescript@7.1.0-dev.20261009.1 and zod@4.6.5, using a minimal standalone project. The same project passes with typescript@7.0.2.

    index.ts:

    import * as z from 'zod';
    
    const item = z.object({ data: z.json() });
    
    export const value: z.infer<typeof item> = { data: 1 };

    tsconfig.json:

    {
      "compilerOptions": {
        "strict": true,
        "noEmit": true,
        "module": "preserve",
        "moduleResolution": "bundler",
        "target": "esnext",
        "skipLibCheck": true
      },
      "files": ["index.ts"]
    }

    Running tsc -p . --pretty false with the nightly produces:

    index.ts(5,44): error TS5115: Instantiations of the following types appear infinitely circular: '$ZodTypeInternals', '$InferZodRecordInput', 'IsOptionalIn'.
    
    Zod TypeScript 7.0.2 TypeScript 7.1.0-dev.20261009.1
    4.3.6 Pass Pass
    4.6.5 Pass TS5115

    The version comparison is for this reproduction only. It independently confirms the affected-version range already reported here; it does not establish that every older-version schema is unaffected.

    The Rust port also tracks this upstream regression in pingdotgg/ts-rust#20. Adding this nightly confirmation here rather than opening a duplicate.

    Tobias Fu (@realtobyfu), please review the standalone reproduction and version comparison above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions