Skip to content

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:

ts
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
>
type Person = {
    _type: "person";
    _id?: string | undefined;
    _createdAt?: string | undefined;
    _updatedAt?: string | undefined;
    _rev?: string | undefined;
    name: string;
    friends: {
        _type: "reference";
        _key: string;
        _ref: string;
        _weak?: boolean | undefined;
    }[];
}

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"):

ts
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:

ts
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
>
type Category = {
    title: string;
    subcategories: {
        _key: string;
        title: string;
        subcategories: ...[];
    }[];
}