Skip to content

Recursive schema through a call-wrapped callback property (lazy(() => Self)) still infers any after #64311 #64420

Description

@ethndotsh

🔎 Search Terms

recursive inference lazy callback property self-referential object literal 7022 7024 zod

🕗 Version & Regression Information

⏯ Playground Link

No response

💻 Code

// @strict: true
interface Schema<O> { readonly out: O }
type Shape = Record<string, Schema<any>>;
type Infer<S extends Shape> = { [K in keyof S]: S[K]["out"] };
declare function object<S extends Shape>(shape: S): Schema<Infer<S>>;
declare function array<T extends Schema<any>>(el: T): Schema<T["out"][]>;
declare function string(): Schema<string>;
declare function lazy<T extends Schema<any>>(fn: () => T): T;

// Works since #64311
const G = object({ name: string(), get children() { return array(G); } });

// Still fails
const L = object({ name: string(), children: lazy(() => array(L)) });
const bad: (typeof L)["out"] = { name: "x", children: [{ name: 1, children: [] }] };

🙁 Actual behavior

error TS7022: 'L' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.

L is any, so the wrong nested value in bad isn't reported.

🙂 Expected behavior

L gets the same recursive type as G, and bad reports Type 'number' is not assignable to type 'string'.

Additional information about the issue

checkObjectLiteral types a property assignment eagerly. Getters are the only members it types lazily, which is why the getter form works. When the initializer is a call like lazy(...), typing it means resolving that call, which infers the callback's return type, which reads L while L's own type is still being resolved.

Colin's closed #64248 (commit 5c29a1a) already handled this shape: if a property's initializer is a call or new expression that contains a function body referring to a symbol whose type is currently being resolved, the property is typed lazily through its own symbol, like a getter. That part doesn't depend on the deferred-constraint machinery #64311 replaced. Ported onto current main, it's about 70 lines in checker.go/inference.go, and #64311's recursive-call handling takes care of the constraint side.

Refs #64192, #64311, #64248.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions