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>.
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:
Here are tests that show TreeA = TreeB: Typescript Playground
New operator
Requirements:
There would be an extension of the usual Schema type to also allow the
Recursymbol anywhere. Many operators would need to accept this type as input and output. This should be a minor runtime change. Possibly:parse()would not accept this type.recursive()would replace all instances of the symbol with recursion.parseshould be otherwise unchanged, since it's already capable of parsing recursive schemas.This would allow us the usual composition API.
Usage
Limitation
The
Recursymbol must be inside an object, not only a tuple.Details
This is fine:
But this doesn't work:
But this does work:
Solution
Test here
This could probably built in to
Recursive<T>.