Skip to content

Constraint check resolves an un-annotated accessor while its object literal is still being inferred #64415

Description

🔎 Search Terms

recursive object literal getter accessor constraint check circular "implicitly has return type" mapped type zod

🕗 Version & Regression Information

⏯ Playground Link

No response

💻 Code

type output<T> = T extends { _zod: { output: any } } ? T["_zod"]["output"] : unknown;
type input<T> = T extends { _zod: { input: any } } ? T["_zod"]["input"] : unknown;
type NoUndefined<T> = T extends undefined ? never : T;
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
type Prettify<T> = { [K in keyof T]: T[K] } & {};
interface $ZodTypeInternals<out O = unknown, out I = unknown> { def: unknown; output: O; input: I; optin?: "optional" | undefined; optout?: "optional" | undefined }
interface StandardProps<I, O> { readonly types?: { readonly input: I; readonly output: O } | undefined }
interface $ZodType<O = unknown, I = unknown, Internals extends $ZodTypeInternals<O, I> = $ZodTypeInternals<O, I>> {
  _zod: Internals;
  "~standard": StandardProps<input<this>, output<this>>;
}
interface ZodType<out Internals extends $ZodTypeInternals = $ZodTypeInternals> extends $ZodType<any, any, Internals> {
  default(def: NoUndefined<output<this>>): ZodDefault<this>;
  default(def: () => NoUndefined<output<this>>): ZodDefault<this>;
}
type Shape = Readonly<{ [k: string]: $ZodType }>;
type OptionalOut = { _zod: { optout: "optional" } };
type OptionalIn = { _zod: { optin: "optional" } };
type $InferObjectOutput<T extends Shape> = Prettify<
  { -readonly [k in keyof T as T[k] extends OptionalOut ? never : k]: T[k]["_zod"]["output"] } &
  { -readonly [k in keyof T as T[k] extends OptionalOut ? k : never]?: T[k]["_zod"]["output"] }
>;
type $InferObjectInput<T extends Shape> = Prettify<
  { -readonly [k in keyof T as T[k] extends OptionalIn ? never : k]: T[k]["_zod"]["input"] } &
  { -readonly [k in keyof T as T[k] extends OptionalIn ? k : never]?: T[k]["_zod"]["input"] }
>;
interface $ZodStringInternals extends $ZodTypeInternals { def: { type: "string" }; output: string; input: string }
interface ZodString extends ZodType<$ZodStringInternals> {}
declare function string(): ZodString;
interface $ZodObjectInternals<S extends Shape> extends $ZodTypeInternals { def: { type: "object"; shape: S }; output: $InferObjectOutput<S>; input: $InferObjectInput<S> }
interface ZodObject<S extends Shape> extends ZodType<$ZodObjectInternals<S>> { shape: S }
declare function object<T extends Shape>(shape: T): ZodObject<Writeable<T>>;
interface $ZodArrayInternals<T extends $ZodType> extends $ZodTypeInternals { def: { type: "array"; element: T }; output: output<T>[]; input: input<T>[] }
interface ZodArray<T extends $ZodType> extends ZodType<$ZodArrayInternals<T>> { element: T }
declare function array<T extends $ZodType>(element: T): ZodArray<T>;
interface $ZodDefaultInternals<T extends $ZodType> extends $ZodTypeInternals { def: { type: "default"; inner: T }; output: NoUndefined<output<T>>; input: input<T> | undefined; optin: "optional" }
interface ZodDefault<T extends $ZodType> extends ZodType<$ZodDefaultInternals<T>> {}

const Tree = object({
  name: string(),
  get children() {
    return array(Tree).default([]);
  },
});

🙁 Actual behavior

children is reported as circular, TS7023 on the getter and TS2615 in Writeable.

🙂 Expected behavior

The getter infers ZodDefault<ZodArray<typeof Tree>>, as it does when array's constraint omits output.

Additional information about the issue

The check of array's type argument against $ZodType compares _zod, whose output property instantiates Tree's inferred output type, and the mapped property of Writeable for children reads the getter while its return type is still being inferred. The same happens through the outer call when object constrains its shape to Readonly<{ [k: string]: $ZodType }>. Zod avoids both today by constraining its builders to a looser type that omits output. #64413 defers those checks until the file's deferred nodes have run.

Activity

  1. ahejlsberg commented on Sep 24, 2026

    @ahejlsberg
    Member

    The thing that seems to trip up your example is the "~standard": StandardProps<input<this>, output<this>> property in $ZodType. Or, more specifically, the references to this in that property declaration. When I comment it out your example checks with no errors. The references to the this type actually cause an extra type parameter to be injected into the interface and this type arguments to be automatically passed at every reference to the interface. That in turn makes things more sensitive to circularities.

  2. colinhacks commented on Sep 24, 2026

    @colinhacks
    Author

    Removing this from the base clears the small repro but not the library. ZodType and each classic schema interface redeclare "~standard" through a subtype, and that copy resolves during the check. Repro closer to the library, base declaration kept, ZodType redeclares it:

    type output<T> = T extends { _zod: { output: any } } ? T["_zod"]["output"] : unknown;
    type input<T> = T extends { _zod: { input: any } } ? T["_zod"]["input"] : unknown;
    type NoUndefined<T> = T extends undefined ? never : T;
    type Writeable<T> = { -readonly [P in keyof T]: T[P] };
    type Prettify<T> = { [K in keyof T]: T[K] } & {};
    interface $ZodTypeInternals<out O = unknown, out I = unknown> { def: unknown; output: O; input: I; optin?: "optional" | undefined; optout?: "optional" | undefined }
    interface StandardProps<I, O> { readonly types?: { readonly input: I; readonly output: O } | undefined }
    interface StandardPropsWithJSON<I, O> extends StandardProps<I, O> { readonly jsonSchema?: unknown }
    interface $ZodType<O = unknown, I = unknown, Internals extends $ZodTypeInternals<O, I> = $ZodTypeInternals<O, I>> {
      _zod: Internals;
      "~standard": StandardProps<input<this>, output<this>>;
    }
    interface ZodType<out Internals extends $ZodTypeInternals = $ZodTypeInternals> extends $ZodType<any, any, Internals> {
      "~standard": StandardPropsWithJSON<input<this>, output<this>>;
      default(def: NoUndefined<output<this>>): ZodDefault<this>;
      default(def: () => NoUndefined<output<this>>): ZodDefault<this>;
    }
    type Shape = Readonly<{ [k: string]: $ZodType }>;
    type OptionalOut = { _zod: { optout: "optional" } };
    type OptionalIn = { _zod: { optin: "optional" } };
    type $InferObjectOutput<T extends Shape> = Prettify<
      { -readonly [k in keyof T as T[k] extends OptionalOut ? never : k]: T[k]["_zod"]["output"] } &
      { -readonly [k in keyof T as T[k] extends OptionalOut ? k : never]?: T[k]["_zod"]["output"] }
    >;
    type $InferObjectInput<T extends Shape> = Prettify<
      { -readonly [k in keyof T as T[k] extends OptionalIn ? never : k]: T[k]["_zod"]["input"] } &
      { -readonly [k in keyof T as T[k] extends OptionalIn ? k : never]?: T[k]["_zod"]["input"] }
    >;
    interface $ZodStringInternals extends $ZodTypeInternals { def: { type: "string" }; output: string; input: string }
    interface ZodString extends ZodType<$ZodStringInternals> {}
    declare function string(): ZodString;
    interface $ZodObjectInternals<S extends Shape> extends $ZodTypeInternals { def: { type: "object"; shape: S }; output: $InferObjectOutput<S>; input: $InferObjectInput<S> }
    interface ZodObject<S extends Shape> extends ZodType<$ZodObjectInternals<S>> { shape: S }
    declare function object<T extends Shape>(shape: T): ZodObject<Writeable<T>>;
    interface $ZodArrayInternals<T extends $ZodType> extends $ZodTypeInternals { def: { type: "array"; element: T }; output: output<T>[]; input: input<T>[] }
    interface ZodArray<T extends $ZodType> extends ZodType<$ZodArrayInternals<T>> { element: T }
    declare function array<T extends $ZodType>(element: T): ZodArray<T>;
    interface $ZodDefaultInternals<T extends $ZodType> extends $ZodTypeInternals { def: { type: "default"; inner: T }; output: NoUndefined<output<T>>; input: input<T> | undefined; optin: "optional" }
    interface ZodDefault<T extends $ZodType> extends ZodType<$ZodDefaultInternals<T>> {}
    
    const Tree = object({
      name: string(),
      get children() {
        return array(Tree).default([]);
      },
    });

    Same file as fixture 08 in the fixture repo.

    Two variants of the base declaration narrow it down. StandardProps<unknown, output<this>> is clean, StandardProps<input<this>, unknown> fails, and swapping the interface's parameter order so output resolves first is clean too. So the trigger is which of input<this> and output<this> resolves first. #64413 passes every variant.

  3. ahejlsberg commented on Sep 24, 2026

    @ahejlsberg
    Member

    Explored why the "~standard" property causes an issue. It comes down to the $InferObjectOutput and $InferObjectInput types that feed an intersection of two mapped types to Prettify, which in turn applies a mapped type to that intersection. When a homomorphic mapped type is applied to an intersection, we first reduce the intersection (using getReducedType) because, if the intersection reduces to never, we want the mapped type to also reduce to never. However, the intersection reduction check forces resolution of the members of the constituent object types, and that in turn sets off the whole circularity.

    In this particular scenario the intersection never reduces to never so the reduction check is all for naught. I'm going to think about possible heuristics for when we can omit member resolution in intersection reduction checks. That would fix the issue here.

  4. colinhacks commented on Sep 25, 2026

    @colinhacks
    Author

    Thanks Anders!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Possible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some cases

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions