@zod-utils/react-hook-form 0.11.0 → 0.12.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.
package/README.md CHANGED
@@ -63,6 +63,9 @@ npm install @zod-utils/react-hook-form zod react react-hook-form @hookform/resol
63
63
  ## Features
64
64
 
65
65
  - 🎣 **useZodForm** - Automatic type transformation for form inputs (nullable/undefined) while preserving Zod schema validation
66
+ - 📋 **FormSchemaProvider** - React Context for providing schema to form components
67
+ - ✅ **useIsRequiredField** - Hook to check if a field requires valid input
68
+ - 🔄 **Discriminated Union Support** - Full type-safe support for discriminated unions
66
69
  - 📦 **All core utilities** - Re-exports everything from `@zod-utils/core`
67
70
  - ⚛️ **React-optimized** - Built specifically for React applications
68
71
 
@@ -273,6 +276,116 @@ const form2 = useZodForm<
273
276
 
274
277
  ---
275
278
 
279
+ ## Form Schema Context
280
+
281
+ The Form Schema Context system allows you to provide Zod schema context to deeply nested form components without prop drilling.
282
+
283
+ ### `FormSchemaProvider`
284
+
285
+ Provides schema context to all child components. Use this to wrap your form.
286
+
287
+ ```tsx
288
+ import { FormSchemaProvider } from "@zod-utils/react-hook-form";
289
+ import { z } from "zod";
290
+
291
+ const schema = z.object({
292
+ username: z.string().min(3).max(20),
293
+ email: z.string().email(),
294
+ });
295
+
296
+ function MyForm() {
297
+ return (
298
+ <FormSchemaProvider schema={schema}>
299
+ <form>
300
+ <UsernameField />
301
+ <EmailField />
302
+ </form>
303
+ </FormSchemaProvider>
304
+ );
305
+ }
306
+ ```
307
+
308
+ #### With Discriminated Union
309
+
310
+ For discriminated unions, pass the discriminator to enable type-safe field access:
311
+
312
+ ```tsx
313
+ const schema = z.discriminatedUnion("mode", [
314
+ z.object({ mode: z.literal("create"), name: z.string().min(1) }),
315
+ z.object({ mode: z.literal("edit"), id: z.number() }),
316
+ ]);
317
+
318
+ function CreateModeForm() {
319
+ return (
320
+ <FormSchemaProvider
321
+ schema={schema}
322
+ discriminator={{ key: "mode", value: "create" }}
323
+ >
324
+ <NameField /> {/* Only fields from 'create' variant are available */}
325
+ </FormSchemaProvider>
326
+ );
327
+ }
328
+ ```
329
+
330
+ ### `useFormSchema()`
331
+
332
+ Access the schema context from child components:
333
+
334
+ ```tsx
335
+ import { useFormSchema } from "@zod-utils/react-hook-form";
336
+
337
+ function FieldComponent() {
338
+ const context = useFormSchema();
339
+ if (!context) return null;
340
+
341
+ const { schema, discriminator } = context;
342
+ // Use schema for field-level logic
343
+ }
344
+ ```
345
+
346
+ ### `useIsRequiredField({ schema, fieldName, discriminator? })`
347
+
348
+ Hook to check if a field requires valid input (shows validation errors on submit).
349
+ The schema parameter is used for type inference only - the actual schema is retrieved from context.
350
+
351
+ ```tsx
352
+ import { useIsRequiredField } from "@zod-utils/react-hook-form";
353
+
354
+ function FormLabel({ name, schema }: { name: string; schema: z.ZodType }) {
355
+ const isRequired = useIsRequiredField({ schema, fieldName: name });
356
+
357
+ return (
358
+ <label>
359
+ {name}
360
+ {isRequired && <span className="text-red-500">*</span>}
361
+ </label>
362
+ );
363
+ }
364
+ ```
365
+
366
+ ### `isRequiredField({ schema, fieldName, discriminator? })`
367
+
368
+ Standalone function to check if a field requires valid input:
369
+
370
+ ```tsx
371
+ import { isRequiredField } from "@zod-utils/react-hook-form";
372
+ import { z } from "zod";
373
+
374
+ const schema = z.object({
375
+ username: z.string().min(1), // Required - min(1) rejects empty
376
+ email: z.string(), // Not required - accepts empty string
377
+ age: z.number(), // Required - numbers reject empty input
378
+ bio: z.string().optional(), // Not required - optional
379
+ });
380
+
381
+ isRequiredField({ schema, fieldName: "username" }); // true
382
+ isRequiredField({ schema, fieldName: "email" }); // false
383
+ isRequiredField({ schema, fieldName: "age" }); // true
384
+ isRequiredField({ schema, fieldName: "bio" }); // false
385
+ ```
386
+
387
+ ---
388
+
276
389
  ## Core Utilities (Re-exported)
277
390
 
278
391
  All utilities from `@zod-utils/core` are re-exported for convenience:
@@ -286,10 +399,23 @@ import {
286
399
  removeDefault,
287
400
  extractDefaultValue,
288
401
  type Simplify,
402
+ type ZodUnionCheck,
403
+
404
+ // Form schema context
405
+ FormSchemaContext,
406
+ FormSchemaProvider,
407
+ useFormSchema,
408
+ useIsRequiredField,
409
+ isRequiredField,
289
410
 
290
- // Type utilities (react-hook-form specific)
411
+ // Type utilities
291
412
  type PartialWithNullableObjects,
292
413
  type PartialWithAllNullables,
414
+ type Discriminator,
415
+ type DiscriminatorKey,
416
+ type DiscriminatorValue,
417
+ type InferredFieldValues,
418
+ type ValidFieldName,
293
419
  } from "@zod-utils/react-hook-form";
294
420
  ```
295
421
 
package/dist/index.d.mts CHANGED
@@ -1,9 +1,185 @@
1
- import { Simplify } from '@zod-utils/core';
1
+ import { DiscriminatorKey, DiscriminatorValue, Discriminator, Simplify } from '@zod-utils/core';
2
2
  export * from '@zod-utils/core';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { Context, ReactNode } from 'react';
5
+ import { z } from 'zod';
3
6
  import * as react_hook_form from 'react-hook-form';
4
- import { FieldValues, UseFormProps } from 'react-hook-form';
7
+ import { FieldValues, Path, UseFormProps } from 'react-hook-form';
5
8
  import { zodResolver } from '@hookform/resolvers/zod';
6
- import { z } from 'zod';
9
+
10
+ /**
11
+ * Type for the FormSchemaContext with full generic support.
12
+ * @internal
13
+ */
14
+ type FormSchemaContextType<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>> = Context<{
15
+ schema: TSchema;
16
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
17
+ } | null>;
18
+ /**
19
+ * Context value type for FormSchemaContext.
20
+ */
21
+ type FormSchemaContextValue<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>> = {
22
+ schema: TSchema;
23
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
24
+ } | null;
25
+ /**
26
+ * React Context for providing Zod schema to form components.
27
+ *
28
+ * Use with {@link FormSchemaProvider} to provide schema context, and
29
+ * {@link useFormSchema} to consume it in child components.
30
+ */
31
+ declare const FormSchemaContext: Context<{
32
+ schema: z.ZodType;
33
+ discriminator?: {
34
+ key: unknown;
35
+ value: unknown;
36
+ };
37
+ } | null>;
38
+ /**
39
+ * Hook to access the form schema from context.
40
+ *
41
+ * The optional `_params` argument is used for TypeScript type inference only.
42
+ * Pass your schema to get proper type narrowing of the context value.
43
+ *
44
+ * @param _params - Optional params for type inference (not used at runtime)
45
+ * @returns The schema context value or null if not within a provider
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * // Without type params (returns generic context)
50
+ * function MyFormField() {
51
+ * const context = useFormSchema();
52
+ * if (!context) return null;
53
+ *
54
+ * const { schema, discriminator } = context;
55
+ * // Use schema for validation or field extraction
56
+ * }
57
+ *
58
+ * // With type params (for type-safe schema access)
59
+ * function TypedFormField() {
60
+ * const context = useFormSchema({ schema: mySchema });
61
+ * // context.schema is now typed as typeof mySchema
62
+ * }
63
+ * ```
64
+ */
65
+ declare function useFormSchema<TSchema extends z.ZodType = z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema> = DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey> = DiscriminatorValue<TSchema, TDiscriminatorKey>>(_params?: {
66
+ schema: TSchema;
67
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
68
+ }): FormSchemaContextValue<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
69
+ /**
70
+ * Provider component that makes Zod schema available to all child components.
71
+ *
72
+ * Use this to wrap your form and provide schema context to nested components
73
+ * like field labels and validation indicators.
74
+ *
75
+ * @example
76
+ * Basic usage with ZodObject
77
+ * ```tsx
78
+ * const schema = z.object({
79
+ * name: z.string(),
80
+ * email: z.string().email().optional()
81
+ * });
82
+ *
83
+ * <FormSchemaProvider schema={schema}>
84
+ * <YourFormComponents />
85
+ * </FormSchemaProvider>
86
+ * ```
87
+ *
88
+ * @example
89
+ * Usage with discriminated union
90
+ * ```tsx
91
+ * const schema = z.discriminatedUnion('mode', [
92
+ * z.object({ mode: z.literal('create'), name: z.string() }),
93
+ * z.object({ mode: z.literal('edit'), id: z.number() })
94
+ * ]);
95
+ *
96
+ * <FormSchemaProvider
97
+ * schema={schema}
98
+ * discriminator={{ key: 'mode', value: 'create' }}
99
+ * >
100
+ * <YourFormComponents />
101
+ * </FormSchemaProvider>
102
+ * ```
103
+ */
104
+ declare function FormSchemaProvider<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ schema, discriminator, children, }: {
105
+ schema: TSchema;
106
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
107
+ children: ReactNode;
108
+ }): react_jsx_runtime.JSX.Element;
109
+ /**
110
+ * Hook to check if a field requires valid input based on the Zod schema.
111
+ *
112
+ * Uses the schema from {@link FormSchemaContext} to determine if a field
113
+ * will show validation errors when submitted with empty/invalid input.
114
+ *
115
+ * @param params - Schema, field name, and optional discriminator (schema used for type inference)
116
+ * @returns true if the field requires valid input, false otherwise
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * function MyFieldLabel({ name, schema }: { name: string; schema: z.ZodType }) {
121
+ * const isRequired = useIsRequiredField({ schema, fieldName: name });
122
+ *
123
+ * return (
124
+ * <label>
125
+ * {name}
126
+ * {isRequired && <span className="text-red-500">*</span>}
127
+ * </label>
128
+ * );
129
+ * }
130
+ * ```
131
+ */
132
+ declare function useIsRequiredField<TSchema extends z.ZodType, TName extends keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>>, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ fieldName, ...props }: {
133
+ schema: TSchema;
134
+ fieldName: TName;
135
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
136
+ }): boolean;
137
+ /**
138
+ * Determines if a field requires valid input (will show validation errors on empty/invalid input).
139
+ *
140
+ * Uses `requiresValidInput` from `@zod-utils/core` which checks the underlying field after
141
+ * removing defaults. This tells you if the field will error when user submits empty input.
142
+ *
143
+ * Returns false if the underlying field accepts:
144
+ * - `undefined` (via `.optional()`)
145
+ * - `null` (via `.nullable()`)
146
+ * - Empty strings (plain `z.string()` without `.min(1)`)
147
+ * - Empty arrays (plain `z.array()` without `.min(1)`)
148
+ *
149
+ * @param options - Schema, field name, and optional discriminator
150
+ * @returns true if the field requires valid input, false otherwise
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const schema = z.object({
155
+ * name: z.string().min(1),
156
+ * bio: z.string().optional(),
157
+ * });
158
+ *
159
+ * isRequiredField({ schema, fieldName: 'name' }); // true
160
+ * isRequiredField({ schema, fieldName: 'bio' }); // false
161
+ * ```
162
+ *
163
+ * @example
164
+ * With discriminated union
165
+ * ```typescript
166
+ * const schema = z.discriminatedUnion('mode', [
167
+ * z.object({ mode: z.literal('create'), name: z.string().min(1) }),
168
+ * z.object({ mode: z.literal('edit'), id: z.number() }),
169
+ * ]);
170
+ *
171
+ * isRequiredField({
172
+ * schema,
173
+ * fieldName: 'name',
174
+ * discriminator: { key: 'mode', value: 'create' },
175
+ * }); // true
176
+ * ```
177
+ */
178
+ declare function isRequiredField<TSchema extends z.ZodType, TName extends keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>>, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ schema, fieldName, discriminator, }: {
179
+ schema: TSchema;
180
+ fieldName: TName;
181
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
182
+ }): boolean;
7
183
 
8
184
  /**
9
185
  * Helper type that adds `null` to object-type fields only (excludes arrays).
@@ -52,6 +228,38 @@ type PartialWithNullableObjects<T> = Simplify<Partial<AddNullToObjects<T>>>;
52
228
  type PartialWithAllNullables<T> = {
53
229
  [K in keyof T]?: T[K] | null;
54
230
  };
231
+ /**
232
+ * Infers field values from a Zod schema compatible with React Hook Form's FieldValues.
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * const schema = z.object({ name: z.string() });
237
+ * type Values = InferredFieldValues<typeof schema>;
238
+ * // { name: string } & FieldValues
239
+ * ```
240
+ */
241
+ type InferredFieldValues<TSchema extends z.ZodType> = z.input<TSchema> & FieldValues;
242
+ /**
243
+ * Type-safe field names for a specific discriminator value.
244
+ *
245
+ * Narrows field names to only those that exist for the given discriminator value
246
+ * in a discriminated union schema.
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const schema = z.discriminatedUnion('mode', [
251
+ * z.object({ mode: z.literal('create'), name: z.string() }),
252
+ * z.object({ mode: z.literal('edit'), id: z.number() }),
253
+ * ]);
254
+ *
255
+ * type CreateFields = ValidFieldName<typeof schema, 'mode', 'create'>;
256
+ * // "mode" | "name"
257
+ *
258
+ * type EditFields = ValidFieldName<typeof schema, 'mode', 'edit'>;
259
+ * // "mode" | "id"
260
+ * ```
261
+ */
262
+ type ValidFieldName<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>, TFieldValues extends InferredFieldValues<TSchema> = InferredFieldValues<TSchema>> = keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>> & Path<TFieldValues>;
55
263
 
56
264
  /**
57
265
  * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.
@@ -223,4 +431,4 @@ declare const useZodForm: <TInput extends FieldValues, TOutput extends FieldValu
223
431
  zodResolverOptions?: Parameters<typeof zodResolver>[1];
224
432
  } & Omit<UseFormProps<TFormInput, unknown, TOutput>, "resolver" | "defaultValues">) => react_hook_form.UseFormReturn<TFormInput, unknown, TOutput>;
225
433
 
226
- export { type PartialWithAllNullables, type PartialWithNullableObjects, useZodForm };
434
+ export { FormSchemaContext, type FormSchemaContextType, type FormSchemaContextValue, FormSchemaProvider, type InferredFieldValues, type PartialWithAllNullables, type PartialWithNullableObjects, type ValidFieldName, isRequiredField, useFormSchema, useIsRequiredField, useZodForm };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,185 @@
1
- import { Simplify } from '@zod-utils/core';
1
+ import { DiscriminatorKey, DiscriminatorValue, Discriminator, Simplify } from '@zod-utils/core';
2
2
  export * from '@zod-utils/core';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { Context, ReactNode } from 'react';
5
+ import { z } from 'zod';
3
6
  import * as react_hook_form from 'react-hook-form';
4
- import { FieldValues, UseFormProps } from 'react-hook-form';
7
+ import { FieldValues, Path, UseFormProps } from 'react-hook-form';
5
8
  import { zodResolver } from '@hookform/resolvers/zod';
6
- import { z } from 'zod';
9
+
10
+ /**
11
+ * Type for the FormSchemaContext with full generic support.
12
+ * @internal
13
+ */
14
+ type FormSchemaContextType<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>> = Context<{
15
+ schema: TSchema;
16
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
17
+ } | null>;
18
+ /**
19
+ * Context value type for FormSchemaContext.
20
+ */
21
+ type FormSchemaContextValue<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>> = {
22
+ schema: TSchema;
23
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
24
+ } | null;
25
+ /**
26
+ * React Context for providing Zod schema to form components.
27
+ *
28
+ * Use with {@link FormSchemaProvider} to provide schema context, and
29
+ * {@link useFormSchema} to consume it in child components.
30
+ */
31
+ declare const FormSchemaContext: Context<{
32
+ schema: z.ZodType;
33
+ discriminator?: {
34
+ key: unknown;
35
+ value: unknown;
36
+ };
37
+ } | null>;
38
+ /**
39
+ * Hook to access the form schema from context.
40
+ *
41
+ * The optional `_params` argument is used for TypeScript type inference only.
42
+ * Pass your schema to get proper type narrowing of the context value.
43
+ *
44
+ * @param _params - Optional params for type inference (not used at runtime)
45
+ * @returns The schema context value or null if not within a provider
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * // Without type params (returns generic context)
50
+ * function MyFormField() {
51
+ * const context = useFormSchema();
52
+ * if (!context) return null;
53
+ *
54
+ * const { schema, discriminator } = context;
55
+ * // Use schema for validation or field extraction
56
+ * }
57
+ *
58
+ * // With type params (for type-safe schema access)
59
+ * function TypedFormField() {
60
+ * const context = useFormSchema({ schema: mySchema });
61
+ * // context.schema is now typed as typeof mySchema
62
+ * }
63
+ * ```
64
+ */
65
+ declare function useFormSchema<TSchema extends z.ZodType = z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema> = DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey> = DiscriminatorValue<TSchema, TDiscriminatorKey>>(_params?: {
66
+ schema: TSchema;
67
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
68
+ }): FormSchemaContextValue<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
69
+ /**
70
+ * Provider component that makes Zod schema available to all child components.
71
+ *
72
+ * Use this to wrap your form and provide schema context to nested components
73
+ * like field labels and validation indicators.
74
+ *
75
+ * @example
76
+ * Basic usage with ZodObject
77
+ * ```tsx
78
+ * const schema = z.object({
79
+ * name: z.string(),
80
+ * email: z.string().email().optional()
81
+ * });
82
+ *
83
+ * <FormSchemaProvider schema={schema}>
84
+ * <YourFormComponents />
85
+ * </FormSchemaProvider>
86
+ * ```
87
+ *
88
+ * @example
89
+ * Usage with discriminated union
90
+ * ```tsx
91
+ * const schema = z.discriminatedUnion('mode', [
92
+ * z.object({ mode: z.literal('create'), name: z.string() }),
93
+ * z.object({ mode: z.literal('edit'), id: z.number() })
94
+ * ]);
95
+ *
96
+ * <FormSchemaProvider
97
+ * schema={schema}
98
+ * discriminator={{ key: 'mode', value: 'create' }}
99
+ * >
100
+ * <YourFormComponents />
101
+ * </FormSchemaProvider>
102
+ * ```
103
+ */
104
+ declare function FormSchemaProvider<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ schema, discriminator, children, }: {
105
+ schema: TSchema;
106
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
107
+ children: ReactNode;
108
+ }): react_jsx_runtime.JSX.Element;
109
+ /**
110
+ * Hook to check if a field requires valid input based on the Zod schema.
111
+ *
112
+ * Uses the schema from {@link FormSchemaContext} to determine if a field
113
+ * will show validation errors when submitted with empty/invalid input.
114
+ *
115
+ * @param params - Schema, field name, and optional discriminator (schema used for type inference)
116
+ * @returns true if the field requires valid input, false otherwise
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * function MyFieldLabel({ name, schema }: { name: string; schema: z.ZodType }) {
121
+ * const isRequired = useIsRequiredField({ schema, fieldName: name });
122
+ *
123
+ * return (
124
+ * <label>
125
+ * {name}
126
+ * {isRequired && <span className="text-red-500">*</span>}
127
+ * </label>
128
+ * );
129
+ * }
130
+ * ```
131
+ */
132
+ declare function useIsRequiredField<TSchema extends z.ZodType, TName extends keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>>, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ fieldName, ...props }: {
133
+ schema: TSchema;
134
+ fieldName: TName;
135
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
136
+ }): boolean;
137
+ /**
138
+ * Determines if a field requires valid input (will show validation errors on empty/invalid input).
139
+ *
140
+ * Uses `requiresValidInput` from `@zod-utils/core` which checks the underlying field after
141
+ * removing defaults. This tells you if the field will error when user submits empty input.
142
+ *
143
+ * Returns false if the underlying field accepts:
144
+ * - `undefined` (via `.optional()`)
145
+ * - `null` (via `.nullable()`)
146
+ * - Empty strings (plain `z.string()` without `.min(1)`)
147
+ * - Empty arrays (plain `z.array()` without `.min(1)`)
148
+ *
149
+ * @param options - Schema, field name, and optional discriminator
150
+ * @returns true if the field requires valid input, false otherwise
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const schema = z.object({
155
+ * name: z.string().min(1),
156
+ * bio: z.string().optional(),
157
+ * });
158
+ *
159
+ * isRequiredField({ schema, fieldName: 'name' }); // true
160
+ * isRequiredField({ schema, fieldName: 'bio' }); // false
161
+ * ```
162
+ *
163
+ * @example
164
+ * With discriminated union
165
+ * ```typescript
166
+ * const schema = z.discriminatedUnion('mode', [
167
+ * z.object({ mode: z.literal('create'), name: z.string().min(1) }),
168
+ * z.object({ mode: z.literal('edit'), id: z.number() }),
169
+ * ]);
170
+ *
171
+ * isRequiredField({
172
+ * schema,
173
+ * fieldName: 'name',
174
+ * discriminator: { key: 'mode', value: 'create' },
175
+ * }); // true
176
+ * ```
177
+ */
178
+ declare function isRequiredField<TSchema extends z.ZodType, TName extends keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>>, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>>({ schema, fieldName, discriminator, }: {
179
+ schema: TSchema;
180
+ fieldName: TName;
181
+ discriminator?: Discriminator<TSchema, TDiscriminatorKey, TDiscriminatorValue>;
182
+ }): boolean;
7
183
 
8
184
  /**
9
185
  * Helper type that adds `null` to object-type fields only (excludes arrays).
@@ -52,6 +228,38 @@ type PartialWithNullableObjects<T> = Simplify<Partial<AddNullToObjects<T>>>;
52
228
  type PartialWithAllNullables<T> = {
53
229
  [K in keyof T]?: T[K] | null;
54
230
  };
231
+ /**
232
+ * Infers field values from a Zod schema compatible with React Hook Form's FieldValues.
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * const schema = z.object({ name: z.string() });
237
+ * type Values = InferredFieldValues<typeof schema>;
238
+ * // { name: string } & FieldValues
239
+ * ```
240
+ */
241
+ type InferredFieldValues<TSchema extends z.ZodType> = z.input<TSchema> & FieldValues;
242
+ /**
243
+ * Type-safe field names for a specific discriminator value.
244
+ *
245
+ * Narrows field names to only those that exist for the given discriminator value
246
+ * in a discriminated union schema.
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const schema = z.discriminatedUnion('mode', [
251
+ * z.object({ mode: z.literal('create'), name: z.string() }),
252
+ * z.object({ mode: z.literal('edit'), id: z.number() }),
253
+ * ]);
254
+ *
255
+ * type CreateFields = ValidFieldName<typeof schema, 'mode', 'create'>;
256
+ * // "mode" | "name"
257
+ *
258
+ * type EditFields = ValidFieldName<typeof schema, 'mode', 'edit'>;
259
+ * // "mode" | "id"
260
+ * ```
261
+ */
262
+ type ValidFieldName<TSchema extends z.ZodType, TDiscriminatorKey extends DiscriminatorKey<TSchema>, TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>, TFieldValues extends InferredFieldValues<TSchema> = InferredFieldValues<TSchema>> = keyof Extract<Required<z.input<TSchema>>, Record<TDiscriminatorKey, TDiscriminatorValue>> & Path<TFieldValues>;
55
263
 
56
264
  /**
57
265
  * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.
@@ -223,4 +431,4 @@ declare const useZodForm: <TInput extends FieldValues, TOutput extends FieldValu
223
431
  zodResolverOptions?: Parameters<typeof zodResolver>[1];
224
432
  } & Omit<UseFormProps<TFormInput, unknown, TOutput>, "resolver" | "defaultValues">) => react_hook_form.UseFormReturn<TFormInput, unknown, TOutput>;
225
433
 
226
- export { type PartialWithAllNullables, type PartialWithNullableObjects, useZodForm };
434
+ export { FormSchemaContext, type FormSchemaContextType, type FormSchemaContextValue, FormSchemaProvider, type InferredFieldValues, type PartialWithAllNullables, type PartialWithNullableObjects, type ValidFieldName, isRequiredField, useFormSchema, useIsRequiredField, useZodForm };
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@zod-utils/core');
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
4
6
  var zod = require('@hookform/resolvers/zod');
5
7
  var reactHookForm = require('react-hook-form');
6
8
 
@@ -32,6 +34,51 @@ var __objRest = (source, exclude) => {
32
34
  }
33
35
  return target;
34
36
  };
37
+ var FormSchemaContext = react.createContext(null);
38
+ function useFormSchema(_params) {
39
+ return react.useContext(
40
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
41
+ FormSchemaContext
42
+ );
43
+ }
44
+ function FormSchemaProvider({
45
+ schema,
46
+ discriminator,
47
+ children
48
+ }) {
49
+ return /* @__PURE__ */ jsxRuntime.jsx(FormSchemaContext.Provider, { value: { schema, discriminator }, children });
50
+ }
51
+ function useIsRequiredField(_a) {
52
+ var _b = _a, {
53
+ fieldName
54
+ } = _b; __objRest(_b, [
55
+ "fieldName"
56
+ ]);
57
+ const context = useFormSchema();
58
+ if (!context) {
59
+ return false;
60
+ }
61
+ return isRequiredField({
62
+ schema: context.schema,
63
+ fieldName,
64
+ discriminator: context.discriminator
65
+ });
66
+ }
67
+ function isRequiredField({
68
+ schema,
69
+ fieldName,
70
+ discriminator
71
+ }) {
72
+ const field = core.extractFieldFromSchema({
73
+ schema,
74
+ fieldName,
75
+ discriminator
76
+ });
77
+ if (!field) {
78
+ return false;
79
+ }
80
+ return core.requiresValidInput(field);
81
+ }
35
82
  var useZodForm = (_a) => {
36
83
  var _b = _a, {
37
84
  schema,
@@ -46,6 +93,11 @@ var useZodForm = (_a) => {
46
93
  }, formOptions));
47
94
  };
48
95
 
96
+ exports.FormSchemaContext = FormSchemaContext;
97
+ exports.FormSchemaProvider = FormSchemaProvider;
98
+ exports.isRequiredField = isRequiredField;
99
+ exports.useFormSchema = useFormSchema;
100
+ exports.useIsRequiredField = useIsRequiredField;
49
101
  exports.useZodForm = useZodForm;
50
102
  Object.keys(core).forEach(function (k) {
51
103
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/use-zod-form.ts"],"names":["zodResolver","useForm"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4KO,IAAM,UAAA,GAAa,CAKxB,EAAA,KAWI;AAXJ,EAAA,IAAA,EAAA,GAAA,EAAA,EACA;AAAA,IAAA,MAAA;AAAA,IACA;AAAA,GAnLF,GAiLE,EAAA,EAGG,WAAA,GAAA,SAAA,CAHH,EAAA,EAGG;AAAA,IAFH,QAAA;AAAA,IACA;AAAA,GAAA,CAAA;AAUA,EAAA,MAAM,QAAA,GAAWA,eAAA,CAAY,MAAA,EAAQ,kBAAkB,CAAA;AAGvD,EAAA,OAAOC,qBAAA,CAAQ,cAAA,CAAA;AAAA,IACb;AAAA,GAAA,EACG,WAAA,CACqD,CAAA;AAC5D","file":"index.js","sourcesContent":["import { zodResolver } from '@hookform/resolvers/zod';\nimport { type FieldValues, type UseFormProps, useForm } from 'react-hook-form';\nimport type { z } from 'zod';\nimport type {\n PartialWithAllNullables,\n PartialWithNullableObjects,\n} from './types';\n\n/**\n * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.\n *\n * This hook eliminates the TypeScript friction between React Hook Form's nullable field values\n * and Zod's strict output types. It uses a two-type schema pattern where:\n * - **Input type** (`PartialWithNullableObjects<TOutput>`): Form fields accept `null | undefined` during editing\n * - **Output type** (`TOutput`): Validated data matches exact schema type (no `null | undefined`)\n *\n * **Key Benefits:**\n * - ✅ No more \"Type 'null' is not assignable to...\" TypeScript errors\n * - ✅ Use `form.setValue()` and `form.reset()` with `null` values freely\n * - ✅ Validated output is still type-safe with exact Zod schema types\n * - ✅ Automatic zodResolver setup - no manual configuration needed\n *\n * @template TOutput - The Zod schema output type (extends FieldValues)\n * @template TInput - The Zod schema input type (accepts nullable/undefined values during form editing)\n *\n * @param options - Configuration object\n * @param options.schema - Zod schema with two-type signature `z.ZodType<TOutput, TInput>`\n * @param options.defaultValues - Default form values (shallow partial - nested objects must be complete if provided)\n * @param options.zodResolverOptions - Optional zodResolver configuration\n * @param options....formOptions - All other react-hook-form useForm options\n *\n * @returns React Hook Form instance with type-safe methods\n *\n * @example\n * Basic usage with required fields\n * ```typescript\n * import { useZodForm } from '@zod-utils/react-hook-form';\n * import { z } from 'zod';\n *\n * const schema = z.object({\n * name: z.string().min(1), // Required field\n * age: z.number().min(0),\n * }) satisfies z.ZodType<{ name: string; age: number }, any>;\n *\n * function MyForm() {\n * const form = useZodForm({ schema });\n *\n * // ✅ These work without type errors:\n * form.setValue('name', null); // Accepts null during editing\n * form.reset({ name: null, age: null }); // Reset with null\n *\n * const onSubmit = (data: { name: string; age: number }) => {\n * // ✅ data is exact type - no null | undefined\n * console.log(data.name.toUpperCase()); // Safe to use string methods\n * };\n *\n * return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;\n * }\n * ```\n *\n * @example\n * With default values\n * ```typescript\n * const schema = z.object({\n * username: z.string(),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * }) satisfies z.ZodType<{\n * username: string;\n * email: string;\n * notifications: boolean;\n * }, any>;\n *\n * const form = useZodForm({\n * schema,\n * defaultValues: {\n * username: '',\n * email: '',\n * // notifications gets default from schema\n * },\n * });\n * ```\n *\n * @example\n * Without default values (all fields are optional during editing)\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * email: z.string().email(),\n * age: z.number(),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * // ✅ No defaultValues needed - fields are optional during editing\n * const form = useZodForm({ schema });\n *\n * // Form fields can be set individually as user types\n * form.setValue('name', 'John');\n * form.setValue('email', 'john@example.com');\n * form.setValue('age', 25);\n *\n * // All fields must be valid on submit (per schema validation)\n * ```\n *\n * @example\n * With optional and nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string(),\n * description: z.string().optional(), // Optional in output\n * tags: z.array(z.string()).nullable(), // Nullable in output\n * }) satisfies z.ZodType<{\n * title: string;\n * description?: string;\n * tags: string[] | null;\n * }, any>;\n *\n * const form = useZodForm({ schema });\n *\n * // All fields accept null/undefined during editing\n * form.setValue('title', null);\n * form.setValue('description', undefined);\n * form.setValue('tags', null);\n * ```\n *\n * @example\n * With zodResolver options\n * ```typescript\n * const form = useZodForm({\n * schema,\n * zodResolverOptions: {\n * async: true, // Enable async validation\n * errorMap: customErrorMap, // Custom error messages\n * },\n * });\n * ```\n *\n * @example\n * Complete form example\n * ```typescript\n * const userSchema = z.object({\n * name: z.string().min(1, 'Name is required'),\n * email: z.string().email('Invalid email'),\n * age: z.number().min(18, 'Must be 18+'),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * function UserForm() {\n * const form = useZodForm({\n * schema: userSchema,\n * defaultValues: { name: '', email: '', age: null },\n * });\n *\n * const onSubmit = (data: { name: string; email: string; age: number }) => {\n * // Type-safe: data has exact types, no null/undefined\n * console.log(`${data.name} is ${data.age} years old`);\n * };\n *\n * return (\n * <form onSubmit={form.handleSubmit(onSubmit)}>\n * <input {...form.register('name')} />\n * <input {...form.register('email')} type=\"email\" />\n * <input {...form.register('age', { valueAsNumber: true })} type=\"number\" />\n * <button type=\"submit\">Submit</button>\n * </form>\n * );\n * }\n * ```\n *\n * @see {@link PartialWithNullableObjects} for the type transformation utility\n * @see https://react-hook-form.com/docs/useform for React Hook Form documentation\n * @see https://zod.dev for Zod schema documentation\n * @since 0.1.0\n */\nexport const useZodForm = <\n TInput extends FieldValues,\n TOutput extends FieldValues,\n TFormInput extends\n PartialWithAllNullables<TInput> = PartialWithNullableObjects<TInput>,\n>({\n schema,\n zodResolverOptions,\n ...formOptions\n}: {\n schema: z.ZodType<TOutput, TInput>;\n defaultValues?: TFormInput;\n zodResolverOptions?: Parameters<typeof zodResolver>[1];\n} & Omit<\n UseFormProps<TFormInput, unknown, TOutput>,\n 'resolver' | 'defaultValues'\n>) => {\n const resolver = zodResolver(schema, zodResolverOptions);\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return useForm({\n resolver,\n ...formOptions,\n } as unknown as UseFormProps<TFormInput, unknown, TOutput>);\n};\n"]}
1
+ {"version":3,"sources":["../src/context.tsx","../src/use-zod-form.ts"],"names":["createContext","useContext","jsx","extractFieldFromSchema","requiresValidInput","zodResolver","useForm"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDO,IAAM,iBAAA,GAAoBA,oBAMvB,IAAI;AA6BP,SAAS,cAUd,OAAA,EAQyE;AACzE,EAAA,OAAOC,gBAAA;AAAA;AAAA,IAEL;AAAA,GAQF;AACF;AAqCO,SAAS,kBAAA,CAId;AAAA,EACA,MAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAA,EAQG;AACD,EAAA,uBACEC,cAAA,CAAC,kBAAkB,QAAA,EAAlB,EAA2B,OAAO,EAAE,MAAA,EAAQ,aAAA,EAAc,EACxD,QAAA,EACH,CAAA;AAEJ;AAyBO,SAAS,mBAQd,EAAA,EAWU;AAXV,EAAA,IAAA,EAAA,GAAA,EAAA,CAAA,CACA;AAAA,IAAA;AAAA,GAjNF,GAgNE,EAAA,CAAA,CAEG,SAAA,CAFH,EAAA,EAEG;AAAA,IADH;AAAA,GAAA;AAWA,EAAA,MAAM,OAAA,GAAU,cAIT,CAAA;AAEP,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,eAAA,CAAgB;AAAA,IACrB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,SAAA;AAAA,IACA,eAAe,OAAA,CAAQ;AAAA,GACxB,CAAA;AACH;AA2CO,SAAS,eAAA,CAQd;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAQY;AACV,EAAA,MAAM,QAAQC,2BAAA,CAAuB;AAAA,IACnC,MAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AAED,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAOC,wBAAmB,KAAK,CAAA;AACjC;AC1IO,IAAM,UAAA,GAAa,CAKxB,EAAA,KAWI;AAXJ,EAAA,IAAA,EAAA,GAAA,EAAA,EACA;AAAA,IAAA,MAAA;AAAA,IACA;AAAA,GAnLF,GAiLE,EAAA,EAGG,WAAA,GAAA,SAAA,CAHH,EAAA,EAGG;AAAA,IAFH,QAAA;AAAA,IACA;AAAA,GAAA,CAAA;AAUA,EAAA,MAAM,QAAA,GAAWC,eAAA,CAAY,MAAA,EAAQ,kBAAkB,CAAA;AAGvD,EAAA,OAAOC,qBAAA,CAAQ,cAAA,CAAA;AAAA,IACb;AAAA,GAAA,EACG,WAAA,CACqD,CAAA;AAC5D","file":"index.js","sourcesContent":["'use client';\n\nimport {\n type Discriminator,\n type DiscriminatorKey,\n type DiscriminatorValue,\n extractFieldFromSchema,\n requiresValidInput,\n} from '@zod-utils/core';\nimport { type Context, createContext, type ReactNode, useContext } from 'react';\nimport type { z } from 'zod';\n\n/**\n * Type for the FormSchemaContext with full generic support.\n * @internal\n */\nexport type FormSchemaContextType<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n> = Context<{\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n} | null>;\n\n/**\n * Context value type for FormSchemaContext.\n */\nexport type FormSchemaContextValue<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n> = {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n} | null;\n\n/**\n * React Context for providing Zod schema to form components.\n *\n * Use with {@link FormSchemaProvider} to provide schema context, and\n * {@link useFormSchema} to consume it in child components.\n */\nexport const FormSchemaContext = createContext<{\n schema: z.ZodType;\n discriminator?: {\n key: unknown;\n value: unknown;\n };\n} | null>(null);\n\n/**\n * Hook to access the form schema from context.\n *\n * The optional `_params` argument is used for TypeScript type inference only.\n * Pass your schema to get proper type narrowing of the context value.\n *\n * @param _params - Optional params for type inference (not used at runtime)\n * @returns The schema context value or null if not within a provider\n *\n * @example\n * ```tsx\n * // Without type params (returns generic context)\n * function MyFormField() {\n * const context = useFormSchema();\n * if (!context) return null;\n *\n * const { schema, discriminator } = context;\n * // Use schema for validation or field extraction\n * }\n *\n * // With type params (for type-safe schema access)\n * function TypedFormField() {\n * const context = useFormSchema({ schema: mySchema });\n * // context.schema is now typed as typeof mySchema\n * }\n * ```\n */\nexport function useFormSchema<\n TSchema extends z.ZodType = z.ZodType,\n TDiscriminatorKey extends\n DiscriminatorKey<TSchema> = DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<\n TSchema,\n TDiscriminatorKey\n > = DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>(\n // Parameter used for type inference only, not at runtime\n _params?: {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n },\n): FormSchemaContextValue<TSchema, TDiscriminatorKey, TDiscriminatorValue> {\n return useContext(\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n FormSchemaContext as Context<{\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n } | null>,\n );\n}\n\n/**\n * Provider component that makes Zod schema available to all child components.\n *\n * Use this to wrap your form and provide schema context to nested components\n * like field labels and validation indicators.\n *\n * @example\n * Basic usage with ZodObject\n * ```tsx\n * const schema = z.object({\n * name: z.string(),\n * email: z.string().email().optional()\n * });\n *\n * <FormSchemaProvider schema={schema}>\n * <YourFormComponents />\n * </FormSchemaProvider>\n * ```\n *\n * @example\n * Usage with discriminated union\n * ```tsx\n * const schema = z.discriminatedUnion('mode', [\n * z.object({ mode: z.literal('create'), name: z.string() }),\n * z.object({ mode: z.literal('edit'), id: z.number() })\n * ]);\n *\n * <FormSchemaProvider\n * schema={schema}\n * discriminator={{ key: 'mode', value: 'create' }}\n * >\n * <YourFormComponents />\n * </FormSchemaProvider>\n * ```\n */\nexport function FormSchemaProvider<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n schema,\n discriminator,\n children,\n}: {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n children: ReactNode;\n}) {\n return (\n <FormSchemaContext.Provider value={{ schema, discriminator }}>\n {children}\n </FormSchemaContext.Provider>\n );\n}\n\n/**\n * Hook to check if a field requires valid input based on the Zod schema.\n *\n * Uses the schema from {@link FormSchemaContext} to determine if a field\n * will show validation errors when submitted with empty/invalid input.\n *\n * @param params - Schema, field name, and optional discriminator (schema used for type inference)\n * @returns true if the field requires valid input, false otherwise\n *\n * @example\n * ```tsx\n * function MyFieldLabel({ name, schema }: { name: string; schema: z.ZodType }) {\n * const isRequired = useIsRequiredField({ schema, fieldName: name });\n *\n * return (\n * <label>\n * {name}\n * {isRequired && <span className=\"text-red-500\">*</span>}\n * </label>\n * );\n * }\n * ```\n */\nexport function useIsRequiredField<\n TSchema extends z.ZodType,\n TName extends keyof Extract<\n Required<z.input<TSchema>>,\n Record<TDiscriminatorKey, TDiscriminatorValue>\n >,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n fieldName,\n ...props\n}: {\n schema: TSchema;\n fieldName: TName;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n}): boolean {\n const context = useFormSchema<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >(props);\n\n if (!context) {\n return false;\n }\n\n return isRequiredField({\n schema: context.schema,\n fieldName,\n discriminator: context.discriminator,\n });\n}\n\n/**\n * Determines if a field requires valid input (will show validation errors on empty/invalid input).\n *\n * Uses `requiresValidInput` from `@zod-utils/core` which checks the underlying field after\n * removing defaults. This tells you if the field will error when user submits empty input.\n *\n * Returns false if the underlying field accepts:\n * - `undefined` (via `.optional()`)\n * - `null` (via `.nullable()`)\n * - Empty strings (plain `z.string()` without `.min(1)`)\n * - Empty arrays (plain `z.array()` without `.min(1)`)\n *\n * @param options - Schema, field name, and optional discriminator\n * @returns true if the field requires valid input, false otherwise\n *\n * @example\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * bio: z.string().optional(),\n * });\n *\n * isRequiredField({ schema, fieldName: 'name' }); // true\n * isRequiredField({ schema, fieldName: 'bio' }); // false\n * ```\n *\n * @example\n * With discriminated union\n * ```typescript\n * const schema = z.discriminatedUnion('mode', [\n * z.object({ mode: z.literal('create'), name: z.string().min(1) }),\n * z.object({ mode: z.literal('edit'), id: z.number() }),\n * ]);\n *\n * isRequiredField({\n * schema,\n * fieldName: 'name',\n * discriminator: { key: 'mode', value: 'create' },\n * }); // true\n * ```\n */\nexport function isRequiredField<\n TSchema extends z.ZodType,\n TName extends keyof Extract<\n Required<z.input<TSchema>>,\n Record<TDiscriminatorKey, TDiscriminatorValue>\n >,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n schema,\n fieldName,\n discriminator,\n}: {\n schema: TSchema;\n fieldName: TName;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n}): boolean {\n const field = extractFieldFromSchema({\n schema,\n fieldName,\n discriminator,\n });\n\n if (!field) {\n return false;\n }\n\n return requiresValidInput(field);\n}\n","import { zodResolver } from '@hookform/resolvers/zod';\nimport { type FieldValues, type UseFormProps, useForm } from 'react-hook-form';\nimport type { z } from 'zod';\nimport type {\n PartialWithAllNullables,\n PartialWithNullableObjects,\n} from './types';\n\n/**\n * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.\n *\n * This hook eliminates the TypeScript friction between React Hook Form's nullable field values\n * and Zod's strict output types. It uses a two-type schema pattern where:\n * - **Input type** (`PartialWithNullableObjects<TOutput>`): Form fields accept `null | undefined` during editing\n * - **Output type** (`TOutput`): Validated data matches exact schema type (no `null | undefined`)\n *\n * **Key Benefits:**\n * - ✅ No more \"Type 'null' is not assignable to...\" TypeScript errors\n * - ✅ Use `form.setValue()` and `form.reset()` with `null` values freely\n * - ✅ Validated output is still type-safe with exact Zod schema types\n * - ✅ Automatic zodResolver setup - no manual configuration needed\n *\n * @template TOutput - The Zod schema output type (extends FieldValues)\n * @template TInput - The Zod schema input type (accepts nullable/undefined values during form editing)\n *\n * @param options - Configuration object\n * @param options.schema - Zod schema with two-type signature `z.ZodType<TOutput, TInput>`\n * @param options.defaultValues - Default form values (shallow partial - nested objects must be complete if provided)\n * @param options.zodResolverOptions - Optional zodResolver configuration\n * @param options....formOptions - All other react-hook-form useForm options\n *\n * @returns React Hook Form instance with type-safe methods\n *\n * @example\n * Basic usage with required fields\n * ```typescript\n * import { useZodForm } from '@zod-utils/react-hook-form';\n * import { z } from 'zod';\n *\n * const schema = z.object({\n * name: z.string().min(1), // Required field\n * age: z.number().min(0),\n * }) satisfies z.ZodType<{ name: string; age: number }, any>;\n *\n * function MyForm() {\n * const form = useZodForm({ schema });\n *\n * // ✅ These work without type errors:\n * form.setValue('name', null); // Accepts null during editing\n * form.reset({ name: null, age: null }); // Reset with null\n *\n * const onSubmit = (data: { name: string; age: number }) => {\n * // ✅ data is exact type - no null | undefined\n * console.log(data.name.toUpperCase()); // Safe to use string methods\n * };\n *\n * return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;\n * }\n * ```\n *\n * @example\n * With default values\n * ```typescript\n * const schema = z.object({\n * username: z.string(),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * }) satisfies z.ZodType<{\n * username: string;\n * email: string;\n * notifications: boolean;\n * }, any>;\n *\n * const form = useZodForm({\n * schema,\n * defaultValues: {\n * username: '',\n * email: '',\n * // notifications gets default from schema\n * },\n * });\n * ```\n *\n * @example\n * Without default values (all fields are optional during editing)\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * email: z.string().email(),\n * age: z.number(),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * // ✅ No defaultValues needed - fields are optional during editing\n * const form = useZodForm({ schema });\n *\n * // Form fields can be set individually as user types\n * form.setValue('name', 'John');\n * form.setValue('email', 'john@example.com');\n * form.setValue('age', 25);\n *\n * // All fields must be valid on submit (per schema validation)\n * ```\n *\n * @example\n * With optional and nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string(),\n * description: z.string().optional(), // Optional in output\n * tags: z.array(z.string()).nullable(), // Nullable in output\n * }) satisfies z.ZodType<{\n * title: string;\n * description?: string;\n * tags: string[] | null;\n * }, any>;\n *\n * const form = useZodForm({ schema });\n *\n * // All fields accept null/undefined during editing\n * form.setValue('title', null);\n * form.setValue('description', undefined);\n * form.setValue('tags', null);\n * ```\n *\n * @example\n * With zodResolver options\n * ```typescript\n * const form = useZodForm({\n * schema,\n * zodResolverOptions: {\n * async: true, // Enable async validation\n * errorMap: customErrorMap, // Custom error messages\n * },\n * });\n * ```\n *\n * @example\n * Complete form example\n * ```typescript\n * const userSchema = z.object({\n * name: z.string().min(1, 'Name is required'),\n * email: z.string().email('Invalid email'),\n * age: z.number().min(18, 'Must be 18+'),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * function UserForm() {\n * const form = useZodForm({\n * schema: userSchema,\n * defaultValues: { name: '', email: '', age: null },\n * });\n *\n * const onSubmit = (data: { name: string; email: string; age: number }) => {\n * // Type-safe: data has exact types, no null/undefined\n * console.log(`${data.name} is ${data.age} years old`);\n * };\n *\n * return (\n * <form onSubmit={form.handleSubmit(onSubmit)}>\n * <input {...form.register('name')} />\n * <input {...form.register('email')} type=\"email\" />\n * <input {...form.register('age', { valueAsNumber: true })} type=\"number\" />\n * <button type=\"submit\">Submit</button>\n * </form>\n * );\n * }\n * ```\n *\n * @see {@link PartialWithNullableObjects} for the type transformation utility\n * @see https://react-hook-form.com/docs/useform for React Hook Form documentation\n * @see https://zod.dev for Zod schema documentation\n * @since 0.1.0\n */\nexport const useZodForm = <\n TInput extends FieldValues,\n TOutput extends FieldValues,\n TFormInput extends\n PartialWithAllNullables<TInput> = PartialWithNullableObjects<TInput>,\n>({\n schema,\n zodResolverOptions,\n ...formOptions\n}: {\n schema: z.ZodType<TOutput, TInput>;\n defaultValues?: TFormInput;\n zodResolverOptions?: Parameters<typeof zodResolver>[1];\n} & Omit<\n UseFormProps<TFormInput, unknown, TOutput>,\n 'resolver' | 'defaultValues'\n>) => {\n const resolver = zodResolver(schema, zodResolverOptions);\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return useForm({\n resolver,\n ...formOptions,\n } as unknown as UseFormProps<TFormInput, unknown, TOutput>);\n};\n"]}
package/dist/index.mjs CHANGED
@@ -1,4 +1,7 @@
1
+ import { extractFieldFromSchema, requiresValidInput } from '@zod-utils/core';
1
2
  export * from '@zod-utils/core';
3
+ import { createContext, useContext } from 'react';
4
+ import { jsx } from 'react/jsx-runtime';
2
5
  import { zodResolver } from '@hookform/resolvers/zod';
3
6
  import { useForm } from 'react-hook-form';
4
7
 
@@ -30,6 +33,51 @@ var __objRest = (source, exclude) => {
30
33
  }
31
34
  return target;
32
35
  };
36
+ var FormSchemaContext = createContext(null);
37
+ function useFormSchema(_params) {
38
+ return useContext(
39
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
40
+ FormSchemaContext
41
+ );
42
+ }
43
+ function FormSchemaProvider({
44
+ schema,
45
+ discriminator,
46
+ children
47
+ }) {
48
+ return /* @__PURE__ */ jsx(FormSchemaContext.Provider, { value: { schema, discriminator }, children });
49
+ }
50
+ function useIsRequiredField(_a) {
51
+ var _b = _a, {
52
+ fieldName
53
+ } = _b; __objRest(_b, [
54
+ "fieldName"
55
+ ]);
56
+ const context = useFormSchema();
57
+ if (!context) {
58
+ return false;
59
+ }
60
+ return isRequiredField({
61
+ schema: context.schema,
62
+ fieldName,
63
+ discriminator: context.discriminator
64
+ });
65
+ }
66
+ function isRequiredField({
67
+ schema,
68
+ fieldName,
69
+ discriminator
70
+ }) {
71
+ const field = extractFieldFromSchema({
72
+ schema,
73
+ fieldName,
74
+ discriminator
75
+ });
76
+ if (!field) {
77
+ return false;
78
+ }
79
+ return requiresValidInput(field);
80
+ }
33
81
  var useZodForm = (_a) => {
34
82
  var _b = _a, {
35
83
  schema,
@@ -44,6 +92,6 @@ var useZodForm = (_a) => {
44
92
  }, formOptions));
45
93
  };
46
94
 
47
- export { useZodForm };
95
+ export { FormSchemaContext, FormSchemaProvider, isRequiredField, useFormSchema, useIsRequiredField, useZodForm };
48
96
  //# sourceMappingURL=index.mjs.map
49
97
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/use-zod-form.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4KO,IAAM,UAAA,GAAa,CAKxB,EAAA,KAWI;AAXJ,EAAA,IAAA,EAAA,GAAA,EAAA,EACA;AAAA,IAAA,MAAA;AAAA,IACA;AAAA,GAnLF,GAiLE,EAAA,EAGG,WAAA,GAAA,SAAA,CAHH,EAAA,EAGG;AAAA,IAFH,QAAA;AAAA,IACA;AAAA,GAAA,CAAA;AAUA,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,MAAA,EAAQ,kBAAkB,CAAA;AAGvD,EAAA,OAAO,OAAA,CAAQ,cAAA,CAAA;AAAA,IACb;AAAA,GAAA,EACG,WAAA,CACqD,CAAA;AAC5D","file":"index.mjs","sourcesContent":["import { zodResolver } from '@hookform/resolvers/zod';\nimport { type FieldValues, type UseFormProps, useForm } from 'react-hook-form';\nimport type { z } from 'zod';\nimport type {\n PartialWithAllNullables,\n PartialWithNullableObjects,\n} from './types';\n\n/**\n * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.\n *\n * This hook eliminates the TypeScript friction between React Hook Form's nullable field values\n * and Zod's strict output types. It uses a two-type schema pattern where:\n * - **Input type** (`PartialWithNullableObjects<TOutput>`): Form fields accept `null | undefined` during editing\n * - **Output type** (`TOutput`): Validated data matches exact schema type (no `null | undefined`)\n *\n * **Key Benefits:**\n * - ✅ No more \"Type 'null' is not assignable to...\" TypeScript errors\n * - ✅ Use `form.setValue()` and `form.reset()` with `null` values freely\n * - ✅ Validated output is still type-safe with exact Zod schema types\n * - ✅ Automatic zodResolver setup - no manual configuration needed\n *\n * @template TOutput - The Zod schema output type (extends FieldValues)\n * @template TInput - The Zod schema input type (accepts nullable/undefined values during form editing)\n *\n * @param options - Configuration object\n * @param options.schema - Zod schema with two-type signature `z.ZodType<TOutput, TInput>`\n * @param options.defaultValues - Default form values (shallow partial - nested objects must be complete if provided)\n * @param options.zodResolverOptions - Optional zodResolver configuration\n * @param options....formOptions - All other react-hook-form useForm options\n *\n * @returns React Hook Form instance with type-safe methods\n *\n * @example\n * Basic usage with required fields\n * ```typescript\n * import { useZodForm } from '@zod-utils/react-hook-form';\n * import { z } from 'zod';\n *\n * const schema = z.object({\n * name: z.string().min(1), // Required field\n * age: z.number().min(0),\n * }) satisfies z.ZodType<{ name: string; age: number }, any>;\n *\n * function MyForm() {\n * const form = useZodForm({ schema });\n *\n * // ✅ These work without type errors:\n * form.setValue('name', null); // Accepts null during editing\n * form.reset({ name: null, age: null }); // Reset with null\n *\n * const onSubmit = (data: { name: string; age: number }) => {\n * // ✅ data is exact type - no null | undefined\n * console.log(data.name.toUpperCase()); // Safe to use string methods\n * };\n *\n * return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;\n * }\n * ```\n *\n * @example\n * With default values\n * ```typescript\n * const schema = z.object({\n * username: z.string(),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * }) satisfies z.ZodType<{\n * username: string;\n * email: string;\n * notifications: boolean;\n * }, any>;\n *\n * const form = useZodForm({\n * schema,\n * defaultValues: {\n * username: '',\n * email: '',\n * // notifications gets default from schema\n * },\n * });\n * ```\n *\n * @example\n * Without default values (all fields are optional during editing)\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * email: z.string().email(),\n * age: z.number(),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * // ✅ No defaultValues needed - fields are optional during editing\n * const form = useZodForm({ schema });\n *\n * // Form fields can be set individually as user types\n * form.setValue('name', 'John');\n * form.setValue('email', 'john@example.com');\n * form.setValue('age', 25);\n *\n * // All fields must be valid on submit (per schema validation)\n * ```\n *\n * @example\n * With optional and nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string(),\n * description: z.string().optional(), // Optional in output\n * tags: z.array(z.string()).nullable(), // Nullable in output\n * }) satisfies z.ZodType<{\n * title: string;\n * description?: string;\n * tags: string[] | null;\n * }, any>;\n *\n * const form = useZodForm({ schema });\n *\n * // All fields accept null/undefined during editing\n * form.setValue('title', null);\n * form.setValue('description', undefined);\n * form.setValue('tags', null);\n * ```\n *\n * @example\n * With zodResolver options\n * ```typescript\n * const form = useZodForm({\n * schema,\n * zodResolverOptions: {\n * async: true, // Enable async validation\n * errorMap: customErrorMap, // Custom error messages\n * },\n * });\n * ```\n *\n * @example\n * Complete form example\n * ```typescript\n * const userSchema = z.object({\n * name: z.string().min(1, 'Name is required'),\n * email: z.string().email('Invalid email'),\n * age: z.number().min(18, 'Must be 18+'),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * function UserForm() {\n * const form = useZodForm({\n * schema: userSchema,\n * defaultValues: { name: '', email: '', age: null },\n * });\n *\n * const onSubmit = (data: { name: string; email: string; age: number }) => {\n * // Type-safe: data has exact types, no null/undefined\n * console.log(`${data.name} is ${data.age} years old`);\n * };\n *\n * return (\n * <form onSubmit={form.handleSubmit(onSubmit)}>\n * <input {...form.register('name')} />\n * <input {...form.register('email')} type=\"email\" />\n * <input {...form.register('age', { valueAsNumber: true })} type=\"number\" />\n * <button type=\"submit\">Submit</button>\n * </form>\n * );\n * }\n * ```\n *\n * @see {@link PartialWithNullableObjects} for the type transformation utility\n * @see https://react-hook-form.com/docs/useform for React Hook Form documentation\n * @see https://zod.dev for Zod schema documentation\n * @since 0.1.0\n */\nexport const useZodForm = <\n TInput extends FieldValues,\n TOutput extends FieldValues,\n TFormInput extends\n PartialWithAllNullables<TInput> = PartialWithNullableObjects<TInput>,\n>({\n schema,\n zodResolverOptions,\n ...formOptions\n}: {\n schema: z.ZodType<TOutput, TInput>;\n defaultValues?: TFormInput;\n zodResolverOptions?: Parameters<typeof zodResolver>[1];\n} & Omit<\n UseFormProps<TFormInput, unknown, TOutput>,\n 'resolver' | 'defaultValues'\n>) => {\n const resolver = zodResolver(schema, zodResolverOptions);\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return useForm({\n resolver,\n ...formOptions,\n } as unknown as UseFormProps<TFormInput, unknown, TOutput>);\n};\n"]}
1
+ {"version":3,"sources":["../src/context.tsx","../src/use-zod-form.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDO,IAAM,iBAAA,GAAoB,cAMvB,IAAI;AA6BP,SAAS,cAUd,OAAA,EAQyE;AACzE,EAAA,OAAO,UAAA;AAAA;AAAA,IAEL;AAAA,GAQF;AACF;AAqCO,SAAS,kBAAA,CAId;AAAA,EACA,MAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAA,EAQG;AACD,EAAA,uBACE,GAAA,CAAC,kBAAkB,QAAA,EAAlB,EAA2B,OAAO,EAAE,MAAA,EAAQ,aAAA,EAAc,EACxD,QAAA,EACH,CAAA;AAEJ;AAyBO,SAAS,mBAQd,EAAA,EAWU;AAXV,EAAA,IAAA,EAAA,GAAA,EAAA,CAAA,CACA;AAAA,IAAA;AAAA,GAjNF,GAgNE,EAAA,CAAA,CAEG,SAAA,CAFH,EAAA,EAEG;AAAA,IADH;AAAA,GAAA;AAWA,EAAA,MAAM,OAAA,GAAU,cAIT,CAAA;AAEP,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,eAAA,CAAgB;AAAA,IACrB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,SAAA;AAAA,IACA,eAAe,OAAA,CAAQ;AAAA,GACxB,CAAA;AACH;AA2CO,SAAS,eAAA,CAQd;AAAA,EACA,MAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAQY;AACV,EAAA,MAAM,QAAQ,sBAAA,CAAuB;AAAA,IACnC,MAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACD,CAAA;AAED,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,OAAO,mBAAmB,KAAK,CAAA;AACjC;AC1IO,IAAM,UAAA,GAAa,CAKxB,EAAA,KAWI;AAXJ,EAAA,IAAA,EAAA,GAAA,EAAA,EACA;AAAA,IAAA,MAAA;AAAA,IACA;AAAA,GAnLF,GAiLE,EAAA,EAGG,WAAA,GAAA,SAAA,CAHH,EAAA,EAGG;AAAA,IAFH,QAAA;AAAA,IACA;AAAA,GAAA,CAAA;AAUA,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,MAAA,EAAQ,kBAAkB,CAAA;AAGvD,EAAA,OAAO,OAAA,CAAQ,cAAA,CAAA;AAAA,IACb;AAAA,GAAA,EACG,WAAA,CACqD,CAAA;AAC5D","file":"index.mjs","sourcesContent":["'use client';\n\nimport {\n type Discriminator,\n type DiscriminatorKey,\n type DiscriminatorValue,\n extractFieldFromSchema,\n requiresValidInput,\n} from '@zod-utils/core';\nimport { type Context, createContext, type ReactNode, useContext } from 'react';\nimport type { z } from 'zod';\n\n/**\n * Type for the FormSchemaContext with full generic support.\n * @internal\n */\nexport type FormSchemaContextType<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n> = Context<{\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n} | null>;\n\n/**\n * Context value type for FormSchemaContext.\n */\nexport type FormSchemaContextValue<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n> = {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n} | null;\n\n/**\n * React Context for providing Zod schema to form components.\n *\n * Use with {@link FormSchemaProvider} to provide schema context, and\n * {@link useFormSchema} to consume it in child components.\n */\nexport const FormSchemaContext = createContext<{\n schema: z.ZodType;\n discriminator?: {\n key: unknown;\n value: unknown;\n };\n} | null>(null);\n\n/**\n * Hook to access the form schema from context.\n *\n * The optional `_params` argument is used for TypeScript type inference only.\n * Pass your schema to get proper type narrowing of the context value.\n *\n * @param _params - Optional params for type inference (not used at runtime)\n * @returns The schema context value or null if not within a provider\n *\n * @example\n * ```tsx\n * // Without type params (returns generic context)\n * function MyFormField() {\n * const context = useFormSchema();\n * if (!context) return null;\n *\n * const { schema, discriminator } = context;\n * // Use schema for validation or field extraction\n * }\n *\n * // With type params (for type-safe schema access)\n * function TypedFormField() {\n * const context = useFormSchema({ schema: mySchema });\n * // context.schema is now typed as typeof mySchema\n * }\n * ```\n */\nexport function useFormSchema<\n TSchema extends z.ZodType = z.ZodType,\n TDiscriminatorKey extends\n DiscriminatorKey<TSchema> = DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<\n TSchema,\n TDiscriminatorKey\n > = DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>(\n // Parameter used for type inference only, not at runtime\n _params?: {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n },\n): FormSchemaContextValue<TSchema, TDiscriminatorKey, TDiscriminatorValue> {\n return useContext(\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n FormSchemaContext as Context<{\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n } | null>,\n );\n}\n\n/**\n * Provider component that makes Zod schema available to all child components.\n *\n * Use this to wrap your form and provide schema context to nested components\n * like field labels and validation indicators.\n *\n * @example\n * Basic usage with ZodObject\n * ```tsx\n * const schema = z.object({\n * name: z.string(),\n * email: z.string().email().optional()\n * });\n *\n * <FormSchemaProvider schema={schema}>\n * <YourFormComponents />\n * </FormSchemaProvider>\n * ```\n *\n * @example\n * Usage with discriminated union\n * ```tsx\n * const schema = z.discriminatedUnion('mode', [\n * z.object({ mode: z.literal('create'), name: z.string() }),\n * z.object({ mode: z.literal('edit'), id: z.number() })\n * ]);\n *\n * <FormSchemaProvider\n * schema={schema}\n * discriminator={{ key: 'mode', value: 'create' }}\n * >\n * <YourFormComponents />\n * </FormSchemaProvider>\n * ```\n */\nexport function FormSchemaProvider<\n TSchema extends z.ZodType,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n schema,\n discriminator,\n children,\n}: {\n schema: TSchema;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n children: ReactNode;\n}) {\n return (\n <FormSchemaContext.Provider value={{ schema, discriminator }}>\n {children}\n </FormSchemaContext.Provider>\n );\n}\n\n/**\n * Hook to check if a field requires valid input based on the Zod schema.\n *\n * Uses the schema from {@link FormSchemaContext} to determine if a field\n * will show validation errors when submitted with empty/invalid input.\n *\n * @param params - Schema, field name, and optional discriminator (schema used for type inference)\n * @returns true if the field requires valid input, false otherwise\n *\n * @example\n * ```tsx\n * function MyFieldLabel({ name, schema }: { name: string; schema: z.ZodType }) {\n * const isRequired = useIsRequiredField({ schema, fieldName: name });\n *\n * return (\n * <label>\n * {name}\n * {isRequired && <span className=\"text-red-500\">*</span>}\n * </label>\n * );\n * }\n * ```\n */\nexport function useIsRequiredField<\n TSchema extends z.ZodType,\n TName extends keyof Extract<\n Required<z.input<TSchema>>,\n Record<TDiscriminatorKey, TDiscriminatorValue>\n >,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n fieldName,\n ...props\n}: {\n schema: TSchema;\n fieldName: TName;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n}): boolean {\n const context = useFormSchema<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >(props);\n\n if (!context) {\n return false;\n }\n\n return isRequiredField({\n schema: context.schema,\n fieldName,\n discriminator: context.discriminator,\n });\n}\n\n/**\n * Determines if a field requires valid input (will show validation errors on empty/invalid input).\n *\n * Uses `requiresValidInput` from `@zod-utils/core` which checks the underlying field after\n * removing defaults. This tells you if the field will error when user submits empty input.\n *\n * Returns false if the underlying field accepts:\n * - `undefined` (via `.optional()`)\n * - `null` (via `.nullable()`)\n * - Empty strings (plain `z.string()` without `.min(1)`)\n * - Empty arrays (plain `z.array()` without `.min(1)`)\n *\n * @param options - Schema, field name, and optional discriminator\n * @returns true if the field requires valid input, false otherwise\n *\n * @example\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * bio: z.string().optional(),\n * });\n *\n * isRequiredField({ schema, fieldName: 'name' }); // true\n * isRequiredField({ schema, fieldName: 'bio' }); // false\n * ```\n *\n * @example\n * With discriminated union\n * ```typescript\n * const schema = z.discriminatedUnion('mode', [\n * z.object({ mode: z.literal('create'), name: z.string().min(1) }),\n * z.object({ mode: z.literal('edit'), id: z.number() }),\n * ]);\n *\n * isRequiredField({\n * schema,\n * fieldName: 'name',\n * discriminator: { key: 'mode', value: 'create' },\n * }); // true\n * ```\n */\nexport function isRequiredField<\n TSchema extends z.ZodType,\n TName extends keyof Extract<\n Required<z.input<TSchema>>,\n Record<TDiscriminatorKey, TDiscriminatorValue>\n >,\n TDiscriminatorKey extends DiscriminatorKey<TSchema>,\n TDiscriminatorValue extends DiscriminatorValue<TSchema, TDiscriminatorKey>,\n>({\n schema,\n fieldName,\n discriminator,\n}: {\n schema: TSchema;\n fieldName: TName;\n discriminator?: Discriminator<\n TSchema,\n TDiscriminatorKey,\n TDiscriminatorValue\n >;\n}): boolean {\n const field = extractFieldFromSchema({\n schema,\n fieldName,\n discriminator,\n });\n\n if (!field) {\n return false;\n }\n\n return requiresValidInput(field);\n}\n","import { zodResolver } from '@hookform/resolvers/zod';\nimport { type FieldValues, type UseFormProps, useForm } from 'react-hook-form';\nimport type { z } from 'zod';\nimport type {\n PartialWithAllNullables,\n PartialWithNullableObjects,\n} from './types';\n\n/**\n * Type-safe React Hook Form wrapper with automatic Zod v4 schema validation and type transformation.\n *\n * This hook eliminates the TypeScript friction between React Hook Form's nullable field values\n * and Zod's strict output types. It uses a two-type schema pattern where:\n * - **Input type** (`PartialWithNullableObjects<TOutput>`): Form fields accept `null | undefined` during editing\n * - **Output type** (`TOutput`): Validated data matches exact schema type (no `null | undefined`)\n *\n * **Key Benefits:**\n * - ✅ No more \"Type 'null' is not assignable to...\" TypeScript errors\n * - ✅ Use `form.setValue()` and `form.reset()` with `null` values freely\n * - ✅ Validated output is still type-safe with exact Zod schema types\n * - ✅ Automatic zodResolver setup - no manual configuration needed\n *\n * @template TOutput - The Zod schema output type (extends FieldValues)\n * @template TInput - The Zod schema input type (accepts nullable/undefined values during form editing)\n *\n * @param options - Configuration object\n * @param options.schema - Zod schema with two-type signature `z.ZodType<TOutput, TInput>`\n * @param options.defaultValues - Default form values (shallow partial - nested objects must be complete if provided)\n * @param options.zodResolverOptions - Optional zodResolver configuration\n * @param options....formOptions - All other react-hook-form useForm options\n *\n * @returns React Hook Form instance with type-safe methods\n *\n * @example\n * Basic usage with required fields\n * ```typescript\n * import { useZodForm } from '@zod-utils/react-hook-form';\n * import { z } from 'zod';\n *\n * const schema = z.object({\n * name: z.string().min(1), // Required field\n * age: z.number().min(0),\n * }) satisfies z.ZodType<{ name: string; age: number }, any>;\n *\n * function MyForm() {\n * const form = useZodForm({ schema });\n *\n * // ✅ These work without type errors:\n * form.setValue('name', null); // Accepts null during editing\n * form.reset({ name: null, age: null }); // Reset with null\n *\n * const onSubmit = (data: { name: string; age: number }) => {\n * // ✅ data is exact type - no null | undefined\n * console.log(data.name.toUpperCase()); // Safe to use string methods\n * };\n *\n * return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>;\n * }\n * ```\n *\n * @example\n * With default values\n * ```typescript\n * const schema = z.object({\n * username: z.string(),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * }) satisfies z.ZodType<{\n * username: string;\n * email: string;\n * notifications: boolean;\n * }, any>;\n *\n * const form = useZodForm({\n * schema,\n * defaultValues: {\n * username: '',\n * email: '',\n * // notifications gets default from schema\n * },\n * });\n * ```\n *\n * @example\n * Without default values (all fields are optional during editing)\n * ```typescript\n * const schema = z.object({\n * name: z.string().min(1),\n * email: z.string().email(),\n * age: z.number(),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * // ✅ No defaultValues needed - fields are optional during editing\n * const form = useZodForm({ schema });\n *\n * // Form fields can be set individually as user types\n * form.setValue('name', 'John');\n * form.setValue('email', 'john@example.com');\n * form.setValue('age', 25);\n *\n * // All fields must be valid on submit (per schema validation)\n * ```\n *\n * @example\n * With optional and nullable fields\n * ```typescript\n * const schema = z.object({\n * title: z.string(),\n * description: z.string().optional(), // Optional in output\n * tags: z.array(z.string()).nullable(), // Nullable in output\n * }) satisfies z.ZodType<{\n * title: string;\n * description?: string;\n * tags: string[] | null;\n * }, any>;\n *\n * const form = useZodForm({ schema });\n *\n * // All fields accept null/undefined during editing\n * form.setValue('title', null);\n * form.setValue('description', undefined);\n * form.setValue('tags', null);\n * ```\n *\n * @example\n * With zodResolver options\n * ```typescript\n * const form = useZodForm({\n * schema,\n * zodResolverOptions: {\n * async: true, // Enable async validation\n * errorMap: customErrorMap, // Custom error messages\n * },\n * });\n * ```\n *\n * @example\n * Complete form example\n * ```typescript\n * const userSchema = z.object({\n * name: z.string().min(1, 'Name is required'),\n * email: z.string().email('Invalid email'),\n * age: z.number().min(18, 'Must be 18+'),\n * }) satisfies z.ZodType<{ name: string; email: string; age: number }, any>;\n *\n * function UserForm() {\n * const form = useZodForm({\n * schema: userSchema,\n * defaultValues: { name: '', email: '', age: null },\n * });\n *\n * const onSubmit = (data: { name: string; email: string; age: number }) => {\n * // Type-safe: data has exact types, no null/undefined\n * console.log(`${data.name} is ${data.age} years old`);\n * };\n *\n * return (\n * <form onSubmit={form.handleSubmit(onSubmit)}>\n * <input {...form.register('name')} />\n * <input {...form.register('email')} type=\"email\" />\n * <input {...form.register('age', { valueAsNumber: true })} type=\"number\" />\n * <button type=\"submit\">Submit</button>\n * </form>\n * );\n * }\n * ```\n *\n * @see {@link PartialWithNullableObjects} for the type transformation utility\n * @see https://react-hook-form.com/docs/useform for React Hook Form documentation\n * @see https://zod.dev for Zod schema documentation\n * @since 0.1.0\n */\nexport const useZodForm = <\n TInput extends FieldValues,\n TOutput extends FieldValues,\n TFormInput extends\n PartialWithAllNullables<TInput> = PartialWithNullableObjects<TInput>,\n>({\n schema,\n zodResolverOptions,\n ...formOptions\n}: {\n schema: z.ZodType<TOutput, TInput>;\n defaultValues?: TFormInput;\n zodResolverOptions?: Parameters<typeof zodResolver>[1];\n} & Omit<\n UseFormProps<TFormInput, unknown, TOutput>,\n 'resolver' | 'defaultValues'\n>) => {\n const resolver = zodResolver(schema, zodResolverOptions);\n\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n return useForm({\n resolver,\n ...formOptions,\n } as unknown as UseFormProps<TFormInput, unknown, TOutput>);\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zod-utils/react-hook-form",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "React Hook Form integration and utilities for Zod schemas",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",