TypeScript issues with large recursive schema
I have a rather large schema, but I would not call it extreme in any way:
import { type } from "arktype";
const componentModule = type.module(
{
"#accordionItem": {
"title?": "component",
"content?": "component",
},
accordion: {
type: "'accordion'",
items: "accordionItem[]",
},
alert: {
type: "'alert'",
variant: "'default' | 'destructive'",
"title?": "component",
content: "component",
},
button: {
type: "'button'",
variant:
"'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'",
size: "'default' | 'sm' | 'lg' | 'icon' | 'compact'",
"href?": "string",
"disabled?": "unknown",
label: "component",
},
card: {
type: "'card'",
"title?": "component",
"description?": "component",
content: "component",
"footer?": "component",
},
container: {
type: "'container'",
content: "component",
},
conversation: {
type: "'conversation'",
"promptFormat?": "'plaintext' | 'markdown' | 'json'",
"responseFormat?": "'plaintext' | 'markdown' | 'json'",
messageHistoryVariable: "string",
latestPromptVariable: "string",
latestResponseVariable: "string",
pendingPromptVariable: "string",
pendingResponseVariable: "string",
"evaluation?": { scale: "number.integer >= 2" },
"footer?": "component",
},
converter: {
type: "'converter'",
from: {
"autofocus?": "boolean",
"placeholder?": "string",
variable: "string",
"label?": "component",
},
to: {
"autofocus?": "boolean",
"placeholder?": "string",
variable: "string",
"label?": "component",
},
"rows?": "number",
button: "component",
},
"#dropdownMenuItemLabel": {
type: "'label'",
label: "component",
},
"#dropdownMenuItemSeparator": {
type: "'separator'",
},
"#dropdownMenuItemItem": {
type: "'item'",
label: "component",
},
"#dropdownMenuItem":
"dropdownMenuItemLabel | dropdownMenuItemSeparator | dropdownMenuItemItem",
dropdownMenu: {
type: "'dropdownMenu'",
"variant?":
"'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'",
label: "component",
items: "dropdownMenuItem | dropdownMenuItem[]",
},
editableText: {
type: "'editableText'",
"format?": "'plaintext' | 'markdown'",
"showCopyButton?": "boolean",
"evaluation?": { scale: "number.integer >= 2" },
},
each: {
type: "'each'",
component: "component",
},
fileInput: {
type: "'fileInput'",
variable: "string",
"multiple?": "boolean",
"label?": "component",
},
flexbox: {
type: "'flexbox'",
items: "component[]",
},
icon: {
type: "'icon'",
icon: "'add' | 'attachment' | 'checkmark' | 'chevronDown' | 'close' | 'delete' | 'documentEdit' | 'documentSparkle' | 'edit' | 'flash' | 'keyboard' | 'microphone' | 'pending' | 'pause' | 'send' | 'sparkle' | 'renew'",
},
input: {
type: "'input'",
"autofocus?": "boolean",
"placeholder?": "string",
variable: "string",
"label?": "component",
},
jsonata: {
type: "'jsonata'",
expression: "string",
},
micSelector: {
type: "'micSelector'",
deviceIdVariable: "string",
"label?": "component",
"permissionDeniedWarning?": {
content: "component",
"title?": "component",
},
},
pill: {
type: "'pill'",
"variant?":
"'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'highlight' | 'error'",
"size?": "'default' | 'sm' | 'lg'",
label: "component",
},
responseViewer: {
type: "'response-viewer'",
format: "'plaintext' | 'markdown' | 'json'",
"editable?": "boolean",
"evaluation?": { scale: "number.integer >= 2" },
messageHistoryVariable: "string",
latestResponseVariable: "string",
pendingResponseVariable: "string",
"header?": "component",
"footer?": "component",
},
"#selectOption": {
value: "string",
"label?": "string",
"selected?": "boolean",
},
select: {
type: "'select'",
"placeholder?": "string",
variable: "string",
options: "selectOption[]",
"label?": "component",
"description?": "component",
},
separator: {
type: "'separator'",
orientation: "'horizontal' | 'vertical'",
"decorative?": "boolean",
},
"#actionState": {
type: "'state'",
"idle?": "component",
"running?": "component",
},
"#recorderState": {
type: "'state'",
"idle?": "component",
"pending?": "component",
"recording?": "component",
"paused?": "component",
},
state: "actionState | recorderState",
"#tabsItem": {
id: "string",
title: "component",
content: "component",
},
tabs: {
type: "'tabs'",
"currentTabVariable?": "string",
items: "tabsItem[]",
},
text: {
type: "'text'",
"format?": "'plaintext' | 'markdown' | 'json'",
content: "string",
},
textarea: {
type: "'textarea'",
"autofocus?": "boolean",
"placeholder?": "string",
variable: "string",
"rows?": "number.integer",
"label?": "component",
},
tooltip: {
type: "'tooltip'",
label: "component",
content: "component",
},
"#singleComponent":
"string | accordion | alert | button | card | container | conversation | converter | dropdownMenu | each | editableText | fileInput | flexbox | icon | input | jsonata | micSelector | pill | responseViewer | select | separator | state | tabs | text | textarea | tooltip",
component: "singleComponent | singleComponent[]",
},
{
jitless: true,
},
);
const componentSchema = componentModule.component;
const componentType = componentSchema.infer;
TypeScript reports that the "Type instantiation is excessively deep and possibly infinite.":
If I remove the accordion type alias from the singleComponent union, the above error goes away, but now componentSchema.infer and componentModule.component errors:
If I also remove the alert type alias from the singleComponent union, only componentSchema.infer errors.
For componentSchema.infer to work, and actually be able to use the inferred type for anything, I must remove 17 type aliases from the singleComponent union, reducing it to just "string | responseViewer | select | separator | state | tabs | text | textarea | tooltip".
The inference here is a lot more expensive than I'd expect for a cyclic schema like this. E.g. compared to the cyclic scope benchmark with 500 cyclic types, this creates 20x more instantiations.
My guess is it has to do with the large unions being defined, but I will try and find some time to investigate further.
Thanks. Do you have any suggestions for workarounds? This is currently blocking me from moving to ArkType from Zod.