Skip to content

Recursive schemas #1362

@james-tindal

Description

@james-tindal

Creating recursive schemas is limited by the need to manually define the type and lose the reliable type-to-value correspondence usually provided by valibot. Our best option at the moment is defining a non-recursive schema and then adding the recursion using lazy and intersection/union/tuple.

We can do better than this, but it would require adding a few bytes to existing operators.

Here's a type that creates recursive types:

const Recur = Symbol('Recur')
type Recur = typeof Recur

type Recursive<RootDesc, SubDesc = RootDesc> = {
  [K in keyof SubDesc]:
    SubDesc[K] extends undefined
      ? undefined :
    SubDesc[K] extends Recur | undefined
      ? Recursive<RootDesc>
      : Recursive<RootDesc, SubDesc[K]>
}

Here are tests that show TreeA = TreeB: Typescript Playground

type TreeA = {
  value: string
  children?: TreeA[]
}

type TreeB = Recursive<{
  value: string
  children?: Recur[]
}>

New operator

Requirements:
There would be an extension of the usual Schema type to also allow the Recur symbol anywhere. Many operators would need to accept this type as input and output. This should be a minor runtime change. Possibly:

if (dataset.value == Recur) return Recur

parse() would not accept this type. recursive() would replace all instances of the symbol with recursion.
parse should be otherwise unchanged, since it's already capable of parsing recursive schemas.

This would allow us the usual composition API.

Usage
const author = pipe(
  object({ 'dc:creator': string() }),
  transform(x => ({ author: x['dc:creator'] }))
)

const children = pipe(
  object({
    spine: object({
      itemref: pipe(
        object({ '@idref': optional(array(Recur)) }),          //  <---  Recur
        transform(x => x['@idref']),
      ),
    }),
  }),
  transform(x => ({ children: x.spine.itemref }))
)

const recursiveSchema = recursive(intersect([author, children]))

// inferred types
type Input = {
  'dc:creator': string
  spine: {
    itemref: {
      '@idref'?: Input[]
    }
  }
}
type Output = {
  author: string
  children?: Output[]
}

Limitation

The Recur symbol must be inside an object, not only a tuple.

Details

This is fine:

type List<T> = [T] | [T, List<T>]

But this doesn't work:

type List<T> = Recursive<[T] | [T, Recur]>

But this does work:

type List<T> = Recursive<{ head: T } | { head: T, tail: Recur }>

Solution

type List<T> = Recursive<[T] | { 0: T, 1: Recur }>

Test here

This could probably built in to Recursive<T>.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestpriorityThis has priority

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions