Recursive schemas
Declare a field that refers to its own schema, or to a schema defined further down, with a getter, the same way as in Zod. TypeScript infers the type without annotations:
import * as s from '@sanity-labs/typed-schema'
const Person = s.document({
_type: s.literal('person'),
name: s.string(),
get friends() {
return s.array(s.reference(Person))
},
})
type Person = s.Infer<typeof Person>This also works for mutual recursion (a User with posts, each Post with an author), through optional(), union(), portableText() and block(), and in the fields passed to extend(). Converting to a Studio schema has an extra rule for recursion through images and files.
The schema creators store the getter as a lazy() schema without calling it, so Person.shape.friends is a lazy schema at runtime even though its static type is the array schema. For the same reason, TypeScript does not check that a getter returns a schema. If it returns something else, parsing throws Field "friends" is not a schema the first time it reaches that field.
When inference fails
Spreading another shape into the same object literal as a getter makes TypeScript read the getter while it infers the schema, and the schema becomes any with error TS7022 ("implicitly has type 'any' because it does not have a type annotation"):
import * as s from '@sanity-labs/typed-schema'
const CategoryFields = s.object({title: s.string()})
const Category = s.object({
...CategoryFields.shape,
get subcategories() {
return s.array(Category)
},
})Add the getter with extend instead:
import * as s from '@sanity-labs/typed-schema'
const CategoryFields = s.object({title: s.string()})
const Category = s.extend(CategoryFields, {
get subcategories() {
return s.array(Category)
},
})
type Category = s.Infer<typeof Category>