Description
🔍 Search Terms
cyclical, recursive, inference, self-referential, extends, schema, zod
I'm trying to get recursive schemas working in Zod, powered by TypeScript's ability to handle self-reference inside getters. Example:
const Category = z.object({
name: z.string(),
get parent(){
// ^ 'parent' 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.ts(7023)
return Category.optional()
}
})
This would be a massive usability win for Zod and other schema/ORM libraries. None of these are able to directly represent cyclical types currently without redundant type annotations or some "scope"/"registry" concept to defer type resolution and later perform some kind of recursive substitution.
My understanding is that TypeScript is generally capable of inferring a recursive type with it's ...
approach as long as there aren't any assignability constraints on the data being inferred.
const a = {
get self() {
return a;
},
}
Applying any constraint breaks this capability. But in the case below, it seems potentially possible to typecheck the cyclical type against the constraint without issue.
const a = {
get self() {
// ^ does not have a return type annotation and is referenced directly or indirectly in one of its return expressions
return a;
},
} satisfies { self: any };
I think if the compiler was able to properly infer a
above despite the satisfies
clause, Zod could then support recursive objects in the way I'd described.
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ Suggestion
Support inference on self-referential/cyclical data structures in more cases
📃 Motivating Example
Below is a more motivated example that I think demonstrates the same fundamental limitation I referenced earlier.
// base type
interface ZodType {
optional: "true" | "false";
output: any;
}
// string
interface ZodString extends ZodType {
optional: "false";
output: string;
}
// object
type ZodShape = Record<string, any>;
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type InferObjectType<Shape extends ZodShape> = Prettify<
{
[k in keyof Shape as Shape[k] extends { optional: "true" } ? k : never]?: Shape[k]["output"];
} & {
[k in keyof Shape as Shape[k] extends { optional: "true" } ? never : k]: Shape[k]["output"];
}
>;
interface ZodObject<T extends ZodShape> extends ZodType {
optional: "false";
output: InferObjectType<T>;
}
// optional
interface ZodOptional<T extends ZodType> extends ZodType {
optional: "true";
output: T["output"] | undefined;
}
// factories
declare function object<T extends ZodShape>(shape: T): ZodObject<T>;
declare function string(): ZodString;
declare function optional<T extends ZodType>(schema: T): ZodOptional<T>;
// recursive type inference error
const Category = object({
name: string(),
get parent() {
// ^ 'parent' 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.ts(7023)
return optional(Category);
},
});
💻 Use Cases
- What do you want to use this for?
To support more ergonomic representations of inherently cyclical data structures (Zod schemas, database schemas, state diagrams, etc)
- What shortcomings exist with current approaches?
Not possible
- What workarounds are you using in the meantime?
None/not possible