@ic-reactor/candid 3.0.12-beta.0 → 3.0.13-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,20 +21,119 @@ export type ArgumentFieldType =
21
21
  | "unknown"
22
22
 
23
23
  // ════════════════════════════════════════════════════════════════════════════
24
- // UI Hints for Form Rendering
24
+ // Component Type Hints
25
25
  // ════════════════════════════════════════════════════════════════════════════
26
26
 
27
- export interface FieldUIHints {
28
- /** Placeholder text for the input */
29
- placeholder?: string
30
- /** Description or help text for the field */
27
+ /**
28
+ * Suggested component type for rendering the field.
29
+ * This eliminates the need for switch statements in the frontend.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * const componentMap = {
34
+ * 'text-input': TextField,
35
+ * 'number-input': NumberField,
36
+ * 'boolean-checkbox': BooleanField,
37
+ * // ...
38
+ * }
39
+ * const Component = componentMap[field.component]
40
+ * return <Component field={field} />
41
+ * ```
42
+ */
43
+ export type FieldComponentType =
44
+ | "record-container"
45
+ | "tuple-container"
46
+ | "variant-select"
47
+ | "optional-toggle"
48
+ | "vector-list"
49
+ | "blob-upload"
50
+ | "principal-input"
51
+ | "text-input"
52
+ | "number-input"
53
+ | "boolean-checkbox"
54
+ | "null-hidden"
55
+ | "recursive-lazy"
56
+ | "unknown-fallback"
57
+
58
+ // ════════════════════════════════════════════════════════════════════════════
59
+ // Render Hints for UI Rendering Strategy
60
+ // ════════════════════════════════════════════════════════════════════════════
61
+
62
+ /**
63
+ * Input type hints for HTML input elements.
64
+ * Used by primitive fields to suggest the appropriate input type.
65
+ */
66
+ export type InputType =
67
+ | "text"
68
+ | "number"
69
+ | "checkbox"
70
+ | "select"
71
+ | "file"
72
+ | "textarea"
73
+
74
+ /**
75
+ * Rendering hints for the UI.
76
+ * Eliminates the need for frontend to maintain COMPLEX_TYPES arrays.
77
+ *
78
+ * @example
79
+ * ```tsx
80
+ * // Frontend no longer needs:
81
+ * // const COMPLEX_TYPES = ["record", "tuple", "variant", "vector", "optional"]
82
+ *
83
+ * // Instead use:
84
+ * if (field.renderHint.isCompound) {
85
+ * return <CompoundFieldRenderer field={field} />
86
+ * }
87
+ * return <PrimitiveInput field={field} />
88
+ * ```
89
+ */
90
+ export interface RenderHint {
91
+ /** Whether this field has its own container/card styling (compound types) */
92
+ isCompound: boolean
93
+ /** Whether this is a leaf input (primitive types) */
94
+ isPrimitive: boolean
95
+ /** Suggested input type for HTML input elements */
96
+ inputType?: InputType
97
+ /** Description or help text for the field (derived from Candid) */
31
98
  description?: string
32
- /** Whether the field is required */
33
- required?: boolean
34
- /** Whether the field should be disabled */
35
- disabled?: boolean
36
- /** Additional CSS class names */
37
- className?: string
99
+ }
100
+
101
+ // ════════════════════════════════════════════════════════════════════════════
102
+ // Primitive Input Props
103
+ // ════════════════════════════════════════════════════════════════════════════
104
+
105
+ /**
106
+ * Pre-computed HTML input props for primitive fields.
107
+ * Can be spread directly onto an input element.
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * <input {...field.inputProps} value={value} onChange={handleChange} />
112
+ * ```
113
+ */
114
+ export interface PrimitiveInputProps {
115
+ /** HTML input type */
116
+ type?: "text" | "number" | "checkbox"
117
+ /** Placeholder text */
118
+ placeholder?: string
119
+ /** Minimum value for number inputs */
120
+ min?: string | number
121
+ /** Maximum value for number inputs */
122
+ max?: string | number
123
+ /** Step value for number inputs */
124
+ step?: string | number
125
+ /** Pattern for text inputs */
126
+ pattern?: string
127
+ /** Input mode for virtual keyboards */
128
+ inputMode?: "text" | "numeric" | "decimal"
129
+ /** Autocomplete hint */
130
+ autoComplete?: string
131
+ /** Whether to check spelling */
132
+ spellCheck?: boolean
133
+ /** Minimum length for text inputs */
134
+ minLength?: number
135
+ /** Maximum length for text inputs */
136
+ maxLength?: number
38
137
  }
39
138
 
40
139
  // ════════════════════════════════════════════════════════════════════════════
@@ -44,8 +143,18 @@ export interface FieldUIHints {
44
143
  export interface FieldBase<TValue = unknown> {
45
144
  /** The field type */
46
145
  type: ArgumentFieldType
47
- /** Human-readable label from Candid */
146
+ /** Raw label from Candid: "__arg0", "_0_" */
48
147
  label: string
148
+ /**
149
+ * Pre-formatted display label for UI rendering.
150
+ * Transforms raw labels into human-readable format.
151
+ *
152
+ * @example
153
+ * "__arg0" => "Arg 0"
154
+ * "_0_" => "Item 0"
155
+ * "created_at_time" => "Created At Time"
156
+ */
157
+ displayLabel: string
49
158
  /**
50
159
  * Form field name path for binding.
51
160
  * Uses bracket notation for array indices: `[0]`, `args[0].owner`, `tags[1]`
@@ -59,14 +168,22 @@ export interface FieldBase<TValue = unknown> {
59
168
  * ```
60
169
  */
61
170
  name: string
171
+ /**
172
+ * Suggested component type for rendering this field.
173
+ * Eliminates the need for switch statements in the frontend.
174
+ */
175
+ component: FieldComponentType
176
+ /**
177
+ * Rendering hints for UI strategy.
178
+ * Use this to determine if the field needs a container or is a simple input.
179
+ */
180
+ renderHint: RenderHint
62
181
  /** Zod schema for field validation */
63
182
  schema: z.ZodTypeAny
64
183
  /** Default value for the field */
65
184
  defaultValue: TValue
66
185
  /** Original Candid type name for reference */
67
186
  candidType?: string
68
- /** UI rendering hints */
69
- ui?: FieldUIHints
70
187
  }
71
188
 
72
189
  // ════════════════════════════════════════════════════════════════════════════
@@ -104,6 +221,43 @@ export interface VariantField extends FieldBase<Record<string, unknown>> {
104
221
  * ```
105
222
  */
106
223
  getOptionDefault: (option: string) => Record<string, unknown>
224
+ /**
225
+ * Get the field for a specific option.
226
+ *
227
+ * @example
228
+ * ```tsx
229
+ * const transferField = field.getField("Transfer")
230
+ * ```
231
+ */
232
+ getField: (option: string) => Field
233
+ /**
234
+ * Get the currently selected option from a value.
235
+ * Returns the first valid key found, or the default option.
236
+ *
237
+ * @example
238
+ * ```tsx
239
+ * const selectedOption = field.getSelectedOption(currentValue)
240
+ * // { Transfer: {...} } => "Transfer"
241
+ * ```
242
+ */
243
+ getSelectedOption: (value: Record<string, unknown>) => string
244
+ /**
245
+ * Get the selected field from a value.
246
+ * Combines getSelectedOption and getField for convenience.
247
+ *
248
+ * @example
249
+ * ```tsx
250
+ * // Current (verbose):
251
+ * const validKeys = Object.keys(currentValue).filter(k => field.options.includes(k))
252
+ * const selected = validKeys[0] ?? field.options[0]
253
+ * const selectedIndex = Math.max(0, field.options.indexOf(selected))
254
+ * const selectedField = field.fields[selectedIndex]
255
+ *
256
+ * // Proposed (simple):
257
+ * const selectedField = field.getSelectedField(currentValue)
258
+ * ```
259
+ */
260
+ getSelectedField: (value: Record<string, unknown>) => Field
107
261
  }
108
262
 
109
263
  export interface TupleField extends FieldBase<unknown[]> {
@@ -132,6 +286,20 @@ export interface OptionalField extends FieldBase<null> {
132
286
  * ```
133
287
  */
134
288
  getInnerDefault: () => unknown
289
+ /**
290
+ * Check if a value represents an enabled optional.
291
+ * Returns true if the value is not null or undefined.
292
+ *
293
+ * @example
294
+ * ```tsx
295
+ * // Current:
296
+ * const enabled = fieldApi.state.value !== null && typeof fieldApi.state.value !== "undefined"
297
+ *
298
+ * // Proposed:
299
+ * const enabled = field.isEnabled(fieldApi.state.value)
300
+ * ```
301
+ */
302
+ isEnabled: (value: unknown) => boolean
135
303
  }
136
304
 
137
305
  export interface VectorField extends FieldBase<unknown[]> {
@@ -150,6 +318,47 @@ export interface VectorField extends FieldBase<unknown[]> {
150
318
  * ```
151
319
  */
152
320
  getItemDefault: () => unknown
321
+ /**
322
+ * Create a properly configured item field for a specific index.
323
+ * Handles name path and label generation.
324
+ *
325
+ * @example
326
+ * ```tsx
327
+ * // Current:
328
+ * renderField({
329
+ * ...field.itemField,
330
+ * label: itemLabel,
331
+ * name: itemFieldName
332
+ * })
333
+ *
334
+ * // Proposed:
335
+ * const itemField = field.createItemField(index, { label: itemLabel })
336
+ * renderField(itemField)
337
+ * ```
338
+ */
339
+ createItemField: (index: number, overrides?: { label?: string }) => Field
340
+ }
341
+
342
+ /**
343
+ * Blob field size limits.
344
+ */
345
+ export interface BlobLimits {
346
+ /** Maximum bytes when entering as hex (e.g., 512 bytes) */
347
+ maxHexBytes: number
348
+ /** Maximum file size in bytes (e.g., 2MB ICP limit) */
349
+ maxFileBytes: number
350
+ /** Maximum hex display length before truncation */
351
+ maxHexDisplayLength: number
352
+ }
353
+
354
+ /**
355
+ * Validation result for blob input.
356
+ */
357
+ export interface BlobValidationResult {
358
+ /** Whether the input is valid */
359
+ valid: boolean
360
+ /** Error message if invalid */
361
+ error?: string
153
362
  }
154
363
 
155
364
  export interface BlobField extends FieldBase<string> {
@@ -158,6 +367,30 @@ export interface BlobField extends FieldBase<string> {
158
367
  itemField: Field
159
368
  /** Accepted input formats */
160
369
  acceptedFormats: ("hex" | "base64" | "file")[]
370
+ /** Size limits for blob input */
371
+ limits: BlobLimits
372
+ /**
373
+ * Normalize hex input (remove 0x prefix, lowercase, etc.)
374
+ *
375
+ * @example
376
+ * ```tsx
377
+ * const normalized = field.normalizeHex("0xDEADBEEF")
378
+ * // => "deadbeef"
379
+ * ```
380
+ */
381
+ normalizeHex: (input: string) => string
382
+ /**
383
+ * Validate blob input value.
384
+ *
385
+ * @example
386
+ * ```tsx
387
+ * const result = field.validateInput(value)
388
+ * if (!result.valid) {
389
+ * setError(result.error)
390
+ * }
391
+ * ```
392
+ */
393
+ validateInput: (value: string | Uint8Array) => BlobValidationResult
161
394
  }
162
395
 
163
396
  export interface RecursiveField extends FieldBase<undefined> {
@@ -181,6 +414,14 @@ export interface PrincipalField extends FieldBase<string> {
181
414
  type: "principal"
182
415
  maxLength: number
183
416
  minLength: number
417
+ /**
418
+ * Pre-computed HTML input props for direct spreading.
419
+ * @example
420
+ * ```tsx
421
+ * <input {...field.inputProps} value={value} onChange={handleChange} />
422
+ * ```
423
+ */
424
+ inputProps: PrimitiveInputProps
184
425
  }
185
426
 
186
427
  export interface NumberField extends FieldBase<string> {
@@ -199,6 +440,14 @@ export interface NumberField extends FieldBase<string> {
199
440
  min?: string
200
441
  /** Maximum value constraint (for bounded types) */
201
442
  max?: string
443
+ /**
444
+ * Pre-computed HTML input props for direct spreading.
445
+ * @example
446
+ * ```tsx
447
+ * <input {...field.inputProps} value={value} onChange={handleChange} />
448
+ * ```
449
+ */
450
+ inputProps: PrimitiveInputProps
202
451
  }
203
452
 
204
453
  export interface TextField extends FieldBase<string> {
@@ -209,10 +458,26 @@ export interface TextField extends FieldBase<string> {
209
458
  maxLength?: number
210
459
  /** Whether to render as multiline textarea */
211
460
  multiline?: boolean
461
+ /**
462
+ * Pre-computed HTML input props for direct spreading.
463
+ * @example
464
+ * ```tsx
465
+ * <input {...field.inputProps} value={value} onChange={handleChange} />
466
+ * ```
467
+ */
468
+ inputProps: PrimitiveInputProps
212
469
  }
213
470
 
214
471
  export interface BooleanField extends FieldBase<boolean> {
215
472
  type: "boolean"
473
+ /**
474
+ * Pre-computed HTML input props for direct spreading.
475
+ * @example
476
+ * ```tsx
477
+ * <input {...field.inputProps} checked={value} onChange={handleChange} />
478
+ * ```
479
+ */
480
+ inputProps: PrimitiveInputProps
216
481
  }
217
482
 
218
483
  export interface NullField extends FieldBase<null> {
@@ -327,6 +592,28 @@ export type FieldByType<T extends ArgumentFieldType> = Extract<
327
592
  { type: T }
328
593
  >
329
594
 
595
+ /**
596
+ * Props type helper for field components.
597
+ * Use this to type your field components for better DX.
598
+ *
599
+ * @example
600
+ * ```tsx
601
+ * const VariantField: React.FC<FieldProps<'variant'>> = ({ field, renderField }) => {
602
+ * // field is properly typed as VariantField
603
+ * return (
604
+ * <div>
605
+ * <select>{field.options.map(opt => ...)}</select>
606
+ * {renderField?.(field.getSelectedField(currentValue))}
607
+ * </div>
608
+ * )
609
+ * }
610
+ * ```
611
+ */
612
+ export type FieldProps<T extends ArgumentFieldType> = {
613
+ field: FieldByType<T>
614
+ renderField?: (child: Field) => React.ReactNode
615
+ }
616
+
330
617
  /** Compound field types that contain other fields */
331
618
  export type CompoundField =
332
619
  | RecordField
@@ -345,50 +632,29 @@ export type PrimitiveField =
345
632
  | NullField
346
633
 
347
634
  /**
348
- * Type guard for checking specific field types.
635
+ * A complete mapping of component types to React components.
636
+ * Use this type when defining your component map.
349
637
  *
350
638
  * @example
351
639
  * ```tsx
352
- * function FieldInput({ field }: { field: Field }) {
353
- * if (isFieldType(field, 'record')) {
354
- * // field is now typed as RecordField
355
- * return <RecordInput field={field} />
356
- * }
357
- * if (isFieldType(field, 'text')) {
358
- * // field is now typed as TextField
359
- * return <TextInput field={field} />
360
- * }
640
+ * const componentMap: ComponentMap<typeof MyTextInput, typeof MyNumberInput, ...> = {
641
+ * 'text-input': MyTextInput,
642
+ * 'number-input': MyNumberInput,
361
643
  * // ...
362
644
  * }
363
645
  * ```
364
646
  */
365
- export function isFieldType<T extends ArgumentFieldType>(
366
- field: Field,
367
- type: T
368
- ): field is FieldByType<T> {
369
- return field.type === type
370
- }
371
-
372
- /** Check if a field is a compound type (contains other fields) */
373
- export function isCompoundField(field: Field): field is CompoundField {
374
- return [
375
- "record",
376
- "variant",
377
- "tuple",
378
- "optional",
379
- "vector",
380
- "recursive",
381
- ].includes(field.type)
382
- }
383
-
384
- /** Check if a field is a primitive type */
385
- export function isPrimitiveField(field: Field): field is PrimitiveField {
386
- return ["principal", "number", "text", "boolean", "null"].includes(field.type)
647
+ export type ComponentMap<
648
+ TComponents extends Record<FieldComponentType, unknown>,
649
+ > = {
650
+ [K in FieldComponentType]: TComponents[K]
387
651
  }
388
652
 
389
- /** Check if a field has children (for iteration) */
390
- export function hasChildFields(
391
- field: Field
392
- ): field is RecordField | VariantField | TupleField {
393
- return "fields" in field && Array.isArray((field as RecordField).fields)
394
- }
653
+ /**
654
+ * Get the component type for a given field component type.
655
+ * Useful for typing dynamic component lookups.
656
+ */
657
+ export type GetComponentType<
658
+ TMap extends Partial<Record<FieldComponentType, unknown>>,
659
+ TKey extends FieldComponentType,
660
+ > = TKey extends keyof TMap ? TMap[TKey] : never
@@ -1,230 +0,0 @@
1
- # Argument Field Visitor
2
-
3
- The `ArgumentFieldVisitor` traverses Candid IDL types to generate two things:
4
-
5
- 1. **Field Metadata**: Structure, labels, names (for form binding), and default values for rendering form fields.
6
- 2. **Validation Schema**: A Zod schema for validating form inputs.
7
-
8
- ## Usage
9
-
10
- ### 1. Initialize the Visitor
11
-
12
- ```typescript
13
- import { ArgumentFieldVisitor } from "@ic-reactor/candid"
14
- import { IDL } from "@icp-sdk/core/candid"
15
-
16
- const visitor = new ArgumentFieldVisitor()
17
- ```
18
-
19
- ### 2. Generate Metadata & Schema
20
-
21
- You can visit a single function or an entire service.
22
-
23
- #### For a Service
24
-
25
- ```typescript
26
- const serviceMeta = visitor.visitService(idlFactory({ IDL }))
27
- const transferMeta = serviceMeta["icrc1_transfer"]
28
-
29
- console.log(transferMeta)
30
- // Output:
31
- // {
32
- // functionName: "icrc1_transfer",
33
- // functionType: "update",
34
- // fields: [...], // Field definitions for rendering
35
- // defaultValues: [...], // Default values for the form (array of argument defaults)
36
- // schema: ZodSchema, // Zod schema for validation
37
- // argCount: 1, // Number of arguments
38
- // isNoArgs: false // Whether the function takes no arguments
39
- // }
40
- ```
41
-
42
- #### For a Single Function
43
-
44
- ```typescript
45
- const funcType = IDL.Func([IDL.Text, IDL.Nat], [], [])
46
- const meta = visitor.visitFunc(funcType, "myMethod")
47
- ```
48
-
49
- ### 3. Field Properties
50
-
51
- Each field in `meta.fields` has the following properties:
52
-
53
- ```typescript
54
- {
55
- type: "text" | "number" | "boolean" | "principal" | "record" | "variant" | ...,
56
- label: "fieldName", // Human-readable label
57
- name: "[0].field.nested", // TanStack Form compatible path
58
- defaultValue: ..., // Default value for this field
59
- schema: ZodSchema, // Zod schema for this field
60
- candidType: "text", // Original Candid type
61
- ui: { // Optional UI hints
62
- placeholder: "e.g. 100",
63
- },
64
- // Type-specific properties:
65
- // - For "number" fields (Nat8, Int32, Float): min, max, unsigned, isFloat, bits
66
- // - For "text" fields (Nat, Int, Nat64): (handled as text for BigInt support)
67
- // - For variants: options, optionMap, getOptionDefault()
68
- // - For vectors: itemField, getItemDefault()
69
- // - For optionals: innerField, getInnerDefault()
70
- // - For records: fields, fieldMap
71
- }
72
- ```
73
-
74
- ### 4. Special Handling & Validation
75
-
76
- #### BigInts as Text
77
-
78
- Large integer types (`Nat`, `Int`, `Nat64`, `Int64`, `Nat32` > 32-bit representations) are generated with `type: "text"`.
79
-
80
- - **Reason**: Standard JavaScript numbers lose precision for values > `2^53 - 1`. HTML number inputs can be unreliable for large integers.
81
- - **Validation**: The Zod schema strictly validates these as **strings containing only digits** (or sign for signed types).
82
- - **Label**: They retain their `candidType` (e.g. `nat`) for reference.
83
-
84
- #### Strict Validation
85
-
86
- - **Required Fields**: Text and Number fields include `.min(1, "Required")`. Empty strings are rejected.
87
- - **Integers**: Regex validation ensures only digits (no decimals).
88
- - **Floats**: Float32/Float64 allow decimal points (e.g., `123.1`) and are validated using standard `!isNaN(Number(val))`.
89
- - **Principals**: Validated using `Principal.fromText()`. Empty strings are rejected.
90
-
91
- #### Optional Fields
92
-
93
- - **Behavior**: Optional fields (`Opt`) wrap the inner schema.
94
- - **Empty Handling**: An empty string input (`""`) is automatically transformed to `null` (Candid `null` / `None`), ensuring optional fields can be cleared.
95
-
96
- ### 5. Integration with TanStack Form
97
-
98
- The visitor is optimized for standard form libraries like TanStack Form.
99
-
100
- ```tsx
101
- import { useForm } from "@tanstack/react-form"
102
-
103
- function MethodForm({ meta }) {
104
- const form = useForm({
105
- defaultValues: meta.defaultValues,
106
- validators: {
107
- onChange: meta.schema, // Use generated Zod schema for validation
108
- },
109
- onSubmit: async ({ value }) => {
110
- console.log("Structured Data:", value)
111
- // value is ready to be passed to strict Candid adapters
112
- },
113
- })
114
-
115
- return (
116
- <form
117
- onSubmit={(e) => {
118
- e.preventDefault()
119
- e.stopPropagation()
120
- form.handleSubmit()
121
- }}
122
- >
123
- {meta.fields.map((field) => (
124
- <form.Field key={field.name} name={field.name}>
125
- {(fieldApi) => (
126
- <div>
127
- <label>{field.label}</label>
128
- <input
129
- type={
130
- field.type === "text" || field.type === "principal"
131
- ? "text"
132
- : field.type
133
- }
134
- value={fieldApi.state.value}
135
- onChange={(e) => fieldApi.handleChange(e.target.value)}
136
- placeholder={field.ui?.placeholder}
137
- />
138
- {fieldApi.state.meta.errors.map((err) => (
139
- <span key={err} className="error">
140
- {err}
141
- </span>
142
- ))}
143
- </div>
144
- )}
145
- </form.Field>
146
- ))}
147
- <button type="submit">Submit</button>
148
- </form>
149
- )
150
- }
151
- ```
152
-
153
- ### 6. Dynamic Fields
154
-
155
- For **Vectors** and **Variants**, you can access helper paths dynamically:
156
-
157
- - **Vector**: Field name `items` -> Item name `items[0]`, `items[1]`.
158
- - **Record**: Field name `user` -> Nested `user.name`.
159
-
160
- The `name` property in the metadata is pre-calculated to match this structure (e.g., `[0].args.user.name` if it's the first argument).
161
-
162
- ### 7. Working with Vectors (Arrays)
163
-
164
- Use helper methods like `getItemDefault()` to manage array items.
165
-
166
- ```tsx
167
- function VectorField({ field, form }) {
168
- return (
169
- <form.Field name={field.name} mode="array">
170
- {(arrayFieldApi) => (
171
- <div>
172
- {arrayFieldApi.state.value.map((_, index) => (
173
- /* Render items using field.name + [index] */
174
- ))}
175
- <button
176
- onClick={() => arrayFieldApi.pushValue(field.getItemDefault())}
177
- >
178
- Add Item
179
- </button>
180
- </div>
181
- )}
182
- </form.Field>
183
- )
184
- }
185
- ```
186
-
187
- ### 8. Working with Variants
188
-
189
- Use `optionMap` for lookup and `getOptionDefault()` for switching types.
190
-
191
- ```tsx
192
- <select
193
- onChange={(e) => {
194
- // Switch variant type and default value
195
- const newValue = field.getOptionDefault(e.target.value)
196
- form.setFieldValue(field.name, newValue)
197
- }}
198
- >
199
- {field.options.map((opt) => (
200
- <option key={opt}>{opt}</option>
201
- ))}
202
- </select>
203
- ```
204
-
205
- ### 9. Type Guards
206
-
207
- The library exports type guard utilities for safer type narrowing:
208
-
209
- ```typescript
210
- import {
211
- isFieldType,
212
- isCompoundField,
213
- isPrimitiveField,
214
- } from "@ic-reactor/candid"
215
-
216
- if (isFieldType(field, "record")) {
217
- // field is RecordArgumentField
218
- }
219
- ```
220
-
221
- ### 10. Recursive Types
222
-
223
- Recursive types (like linked lists) use `z.lazy()` schemas. Use `field.extract()` to get the inner definition when rendering.
224
-
225
- ```tsx
226
- function RecursiveField({ field }) {
227
- const innerField = useMemo(() => field.extract(), [field])
228
- return <DynamicField field={innerField} ... />
229
- }
230
- ```