🔎 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.
🔎 Search Terms
recursive inference lazy callback property self-referential object literal 7022 7024 zod
🕗 Version & Regression Information
main(df1a31e) after Suppress type argument constraint checks in recursive call resolution #64311.get children() { return array(Self) }) and, as a side effect, the plain callback form (children: () => array(Self)). The form where the callback is wrapped in a call is still broken, and it's the one Zod documents (z.lazy).⏯ Playground Link
No response
💻 Code
🙁 Actual behavior
Lisany, so the wrong nested value inbadisn't reported.🙂 Expected behavior
Lgets the same recursive type asG, andbadreportsType 'number' is not assignable to type 'string'.Additional information about the issue
checkObjectLiteraltypes 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 likelazy(...), typing it means resolving that call, which infers the callback's return type, which readsLwhileL'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
newexpression 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 currentmain, it's about 70 lines inchecker.go/inference.go, and #64311's recursive-call handling takes care of the constraint side.Refs #64192, #64311, #64248.