@beignet/react-hook-form 0.0.57 → 0.0.59

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Snapshot data containers without manufacturing invalid copies of opaque
3
+ * instances (which may depend on private fields or native internal slots).
4
+ * Opaque instances and functions remain references and must be treated as immutable.
5
+ */
6
+ export function snapshotInput(input) {
7
+ const seen = new WeakMap();
8
+ function copy(value) {
9
+ if (value === null || typeof value !== "object")
10
+ return value;
11
+ const previous = seen.get(value);
12
+ if (previous)
13
+ return previous;
14
+ const prototype = Object.getPrototypeOf(value);
15
+ let snapshot;
16
+ if (prototype === Date.prototype) {
17
+ snapshot = new Date(Date.prototype.getTime.call(value));
18
+ }
19
+ else if (Array.isArray(value) && prototype === Array.prototype) {
20
+ snapshot = new Array(value.length);
21
+ }
22
+ else if (prototype === Object.prototype || prototype === null) {
23
+ snapshot = Object.create(prototype);
24
+ }
25
+ else {
26
+ return value;
27
+ }
28
+ seen.set(value, snapshot);
29
+ for (const key of Reflect.ownKeys(value)) {
30
+ if (Array.isArray(value) && key === "length")
31
+ continue;
32
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
33
+ if (!descriptor)
34
+ continue;
35
+ Object.defineProperty(snapshot, key, {
36
+ value: copy(Reflect.get(value, key)),
37
+ enumerable: descriptor.enumerable,
38
+ configurable: true,
39
+ writable: true,
40
+ });
41
+ }
42
+ return snapshot;
43
+ }
44
+ return copy(input);
45
+ }
46
+ //# sourceMappingURL=input-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-snapshot.js","sourceRoot":"","sources":["../src/input-snapshot.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAI,KAAQ;IACvC,MAAM,IAAI,GAAG,IAAI,OAAO,EAAkB,CAAC;IAE3C,SAAS,IAAI,CAAC,KAAc;QAC1B,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAC/C,IAAI,QAAgB,CAAC;QACrB,IAAI,SAAS,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACjC,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,SAAS,KAAK,KAAK,CAAC,SAAS,EAAE,CAAC;YACjE,QAAQ,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACrC,CAAC;aAAM,IAAI,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YAChE,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAE1B,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,QAAQ;gBAAE,SAAS;YACvD,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC/D,IAAI,CAAC,UAAU;gBAAE,SAAS;YAC1B,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,GAAG,EAAE;gBACnC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;gBACpC,UAAU,EAAE,UAAU,CAAC,UAAU;gBACjC,YAAY,EAAE,IAAI;gBAClB,QAAQ,EAAE,IAAI;aACf,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,IAAI,CAAC,KAAK,CAAM,CAAC;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beignet/react-hook-form",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
4
4
  "type": "module",
5
5
  "description": "React Hook Form integration for Beignet with Standard Schema support",
6
6
  "main": "./dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "clean": "rm -rf dist coverage .turbo",
29
29
  "test": "bun test && bun run typecheck:types",
30
30
  "test:coverage": "bun test --coverage",
31
- "typecheck:types": "tsc -p tsconfig.type-tests.json",
31
+ "typecheck:types": "bun run build && tsc -p tsconfig.type-tests.json && tsc -p tsconfig.consumer-tests.json",
32
32
  "lint": "biome check ."
33
33
  },
34
34
  "keywords": [
@@ -69,10 +69,10 @@
69
69
  "@types/bun": "^1.4.0",
70
70
  "@types/node": "^22.0.0",
71
71
  "@types/react": "^18.2.0",
72
- "happy-dom": "^20.8.9",
72
+ "happy-dom": "^21.0.4",
73
73
  "react": "^18.2.0",
74
74
  "react-hook-form": "^7.48.0",
75
75
  "typescript": "^5.3.0",
76
- "zod": "^4.0.0"
76
+ "zod": "^4.6.6"
77
77
  }
78
78
  }
@@ -26,14 +26,15 @@ routes, providers, infra adapters, `server/`, or `app-context.ts`.
26
26
 
27
27
  ## Contract Requirements
28
28
 
29
- React Hook Form binds to the contract body schema. TypeScript rejects contracts
29
+ React Hook Form binds to the contract body schema by default. Pass
30
+ `rhf(contract, { schema })` for a separate form schema whose parsed output is
31
+ assignable to the contract body input. TypeScript rejects contracts
30
32
  without `.body(...)` because there are no form fields or body validation to
31
33
  infer; untyped JavaScript calls receive the same failure at runtime.
32
34
 
33
- The body schema input must be an object with named fields. TypeScript rejects
34
- scalar and array body inputs. Put scalar values or lists under a field, for
35
- example `.body(z.object({ tags: z.array(z.string()) }))`, when the endpoint is
36
- backed by a form.
35
+ The selected form schema input must be an object with named fields. Scalar or
36
+ array request bodies need an explicit object-input form schema that transforms
37
+ named fields into the required body shape.
37
38
 
38
39
  Keep shared form DTOs in `features/<feature>/schemas.ts`, then reuse them from
39
40
  contracts, use cases, tests, and components.
@@ -86,17 +87,36 @@ export function CreateTodoForm() {
86
87
  ```
87
88
 
88
89
  Use `formOptions(...)` when a component or wrapper wants options without
89
- calling the hook directly.
90
+ calling the hook directly. Returned options fit native `UseFormProps`, `useForm`,
91
+ and `createFormControl` with `exactOptionalPropertyTypes` enabled.
90
92
 
91
93
  ## Input And Output Types
92
94
 
93
- React Hook Form field state uses the schema input type. `handleSubmit` receives
94
- the schema output type after validation, coercion, defaults, and transforms.
95
-
96
- For plain, defaulted, or coerced schemas, submitting `values` as the contract
97
- body usually works. If a schema transform changes a field's type so the parsed
98
- output no longer matches the contract body input, send raw field values from
99
- `form.getValues()` after validation instead of `values`.
95
+ React Hook Form field state uses schema input; default `handleSubmit` values use
96
+ parsed output, including transforms, defaults, and removal of unknown keys.
97
+ Use `submitValues: "raw"` only when original validated input is needed. Use
98
+ `defaultValues` for visible defaults; parsing does not replace live field state.
99
+
100
+ Prefer numeric HTTP contracts (`z.number()`) and numeric field bindings
101
+ (`valueAsNumber`) when the API accepts numbers. Use a separate form schema when
102
+ UI values need conversion: `rhf(contract, { schema: formSchema })`. The form
103
+ schema owns validation; derive it from shared request validators to preserve
104
+ constraints. Its output must fit the contract's input. Keep schemas in feature
105
+ `schemas.ts` and reusable bindings in feature `client/forms.ts`.
106
+
107
+ Shared coercing schemas are appropriate when the API intentionally accepts
108
+ those inputs. Reuse transforms only when parsed output is accepted as input and
109
+ parsing again preserves its meaning. One-way conversions belong in a separate
110
+ form schema, or use raw mode to submit the original contract input. Raw mode
111
+ with a separate form schema may still need mapping into contract input.
112
+
113
+ Generated resolvers snapshot plain objects, arrays, and dates. Opaque instances
114
+ retain identity and must stay immutable during validation and submission.
115
+ Disabled resolvers submit input; dynamic enable flags return an input/output
116
+ union. Explicit parsed mode requires validation. Custom resolvers own output
117
+ and cannot combine with `submitValues`. Prefer adapter `useForm` for inference;
118
+ direct `useForm(formOptions(...))` may need explicit input, context, and output
119
+ generics with partial defaults.
100
120
 
101
121
  ## Error Mapping
102
122
 
@@ -122,11 +142,9 @@ codes declared on the contract with `.errors(...)`.
122
142
  params, headers, tenant IDs, and auth-derived values belong in mutation
123
143
  variables or app context, not form fields.
124
144
  - Contracts without `.body(...)` are not form contracts.
125
- - Scalar and array request bodies are not form contracts; wrap their values in
126
- an object with named fields.
127
- - `handleSubmit` returns parsed schema output. For transforms that change field
128
- types, submit `form.getValues()` after validation when the contract client
129
- expects body input.
145
+ - Scalar and array request bodies need a separate object-input form schema.
146
+ - `handleSubmit` returns parsed output by default. Submit callback values
147
+ directly rather than rereading `form.getValues()` after asynchronous validation.
130
148
  - Keep the root form helper in `client/forms.ts` unless the app already uses a
131
149
  different local convention.
132
150
  - Do not import use cases or server modules to reuse validation. Move shared
@@ -138,6 +156,15 @@ Leave the generated Standard Schema resolver enabled by default. Use
138
156
  `resolverEnabled: false` only for deliberate partial or multi-step flows, and
139
157
  keep final validation in use cases.
140
158
 
159
+ Forwarded `UseFormProps` and optional resolver settings are supported. A missing
160
+ custom resolver uses generated validation unless disabled; submit types include
161
+ the custom output and any possible parsed or unparsed fallback.
162
+ Wrappers may forward an `undefined` options object; this uses generated validation
163
+ and parsed output, which is included in the inferred submit union.
164
+ Returned props remain optional when the whole options object may be absent.
165
+ Conditional configurations can select a custom resolver or a generated submit
166
+ mode, with output inferred per branch. Do not set both in the same configuration.
167
+
141
168
  After form changes, run from the app root:
142
169
 
143
170
  ```bash
package/src/index.ts CHANGED
@@ -16,10 +16,12 @@ import {
16
16
  import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
17
17
  import {
18
18
  type FieldValues,
19
+ type Resolver,
19
20
  useForm as rhfUseForm,
20
21
  type UseFormProps,
21
22
  type UseFormReturn,
22
23
  } from "react-hook-form";
24
+ import { snapshotInput } from "./input-snapshot.js";
23
25
 
24
26
  /**
25
27
  * Keep only object-shaped React Hook Form field values. Arrays are objects in
@@ -32,95 +34,269 @@ type ObjectFieldValues<T> = [T] extends [FieldValues]
32
34
  : never
33
35
  : never;
34
36
 
35
- /**
36
- * Live form field values inferred from the contract body schema input.
37
- *
38
- * React Hook Form field values (`register`, `watch`, `setValue`,
39
- * `defaultValues`) hold pre-validation input, so coercing or transforming body
40
- * schemas type fields as what the user edits, not what the schema produces.
41
- */
42
- type FormInput<TContract extends HttpContractConfig> =
43
- TContract["body"] extends StandardSchemaV1
44
- ? ObjectFieldValues<InferInput<TContract["body"]>>
45
- : never;
37
+ /** Live fields and defaults use the selected schema's input. */
38
+ type FormInput<S extends StandardSchemaV1> = ObjectFieldValues<InferInput<S>>;
46
39
 
47
- /**
48
- * Validated submit values inferred from the contract body schema output.
49
- *
50
- * `handleSubmit` callbacks receive the resolver-parsed output.
51
- */
52
- type FormOutput<TContract extends HttpContractConfig> =
53
- TContract["body"] extends StandardSchemaV1
54
- ? InferOutput<TContract["body"]>
55
- : never;
40
+ /** Parsed submissions use the selected schema's output. */
41
+ type FormOutput<S extends StandardSchemaV1> = InferOutput<S>;
56
42
 
57
- /**
58
- * Contract input accepted by the React Hook Form adapter.
59
- */
60
- type ReactHookFormContract<TContractLike extends ContractLike> = [
61
- FormInput<ResolveContract<TContractLike>>,
43
+ type BodySchema<C extends HttpContractConfig> = Extract<
44
+ C["body"],
45
+ StandardSchemaV1
46
+ >;
47
+
48
+ type ReactHookFormContract<T extends ContractLike> = [
49
+ FormInput<BodySchema<ResolveContract<T>>>,
62
50
  ] extends [never]
63
51
  ? never
64
- : TContractLike;
52
+ : T;
65
53
 
66
- /**
67
- * React Hook Form options accepted by the Beignet adapter.
68
- */
69
- type RhfFormOptions<TContract extends HttpContractConfig> = Omit<
70
- UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>,
54
+ /** A form-specific schema produces the request input accepted by the contract. */
55
+ type CompatibleFormSchema<
56
+ C extends HttpContractConfig,
57
+ S extends StandardSchemaV1,
58
+ > = [BodySchema<C>] extends [never]
59
+ ? never
60
+ : [FormInput<S>] extends [never]
61
+ ? never
62
+ : [FormOutput<S>] extends [InferInput<BodySchema<C>>]
63
+ ? S
64
+ : never;
65
+
66
+ type RhfFormOptions<S extends StandardSchemaV1, Output> = Omit<
67
+ UseFormProps<FormInput<S>, unknown, Output>,
71
68
  "resolver"
69
+ >;
70
+
71
+ type ParsedFormOptions<S extends StandardSchemaV1> = RhfFormOptions<
72
+ S,
73
+ FormOutput<S>
72
74
  > & {
73
- /**
74
- * Optional override for the resolver. If supplied, this is used instead of
75
- * the default contract-based resolver.
76
- */
77
- resolver?: UseFormProps<
78
- FormInput<TContract>,
79
- unknown,
80
- FormOutput<TContract>
81
- >["resolver"];
82
-
83
- /**
84
- * Enables or disables automatic resolver generation from the contract body schema.
85
- * Defaults to true.
86
- */
75
+ /** Submit parsed schema output. Default: parsed. Requires the generated resolver. */
76
+ submitValues?: "parsed";
77
+ resolver?: undefined;
78
+ /** Generate the schema resolver. Default: true. */
79
+ resolverEnabled?: true;
80
+ };
81
+
82
+ type RawFormOptions<S extends StandardSchemaV1> = RhfFormOptions<
83
+ S,
84
+ FormInput<S>
85
+ > & {
86
+ /** Validate with the selected schema and submit original input. */
87
+ submitValues: "raw";
88
+ resolver?: undefined;
87
89
  resolverEnabled?: boolean;
88
90
  };
89
91
 
90
- /**
91
- * React Hook Form adapter methods for one compatible contract.
92
- */
93
- type ReactHookFormContractAdapterShape<TContract extends HttpContractConfig> = {
94
- /**
95
- * Generate `UseFormProps` for any React Hook Form usage.
96
- */
97
- formOptions: (
98
- props?: RhfFormOptions<TContract>,
99
- ) => UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>;
100
-
101
- /**
102
- * Convenience wrapper around `useForm(formOptions(props))`.
103
- */
104
- useForm: (
105
- props?: RhfFormOptions<TContract>,
106
- ) => UseFormReturn<FormInput<TContract>, unknown, FormOutput<TContract>>;
92
+ type DisabledFormOptions<S extends StandardSchemaV1> = RhfFormOptions<
93
+ S,
94
+ FormInput<S>
95
+ > & {
96
+ resolverEnabled: false;
97
+ resolver?: undefined;
98
+ submitValues?: "raw";
99
+ };
100
+
101
+ type VariableFormOptions<S extends StandardSchemaV1> = RhfFormOptions<
102
+ S,
103
+ FormInput<S> | FormOutput<S>
104
+ > & { resolver?: undefined } & (
105
+ | { submitValues?: "raw" | "parsed" | undefined; resolverEnabled?: true }
106
+ | { submitValues?: "raw"; resolverEnabled?: boolean | undefined }
107
+ );
108
+
109
+ type CustomFormOptions<S extends StandardSchemaV1, Output> = RhfFormOptions<
110
+ S,
111
+ Output
112
+ > & {
113
+ /** A custom resolver owns its submit output, independently of the schema. */
114
+ resolver: Resolver<FormInput<S>, unknown, Output>;
115
+ resolverEnabled?: boolean;
116
+ submitValues?: never;
117
+ };
118
+
119
+ type OptionalResolverFormOptions<
120
+ S extends StandardSchemaV1,
121
+ Output,
122
+ > = RhfFormOptions<S, Output> & {
123
+ resolver?: Resolver<FormInput<S>, unknown, Output> | undefined;
124
+ resolverEnabled?: boolean | undefined;
125
+ submitValues?: never;
126
+ };
127
+
128
+ type SuppliedResolverFormOptions<
129
+ S extends StandardSchemaV1,
130
+ Output,
131
+ > = OptionalResolverFormOptions<S, Output> & {
132
+ resolver: Resolver<FormInput<S>, unknown, Output> | undefined;
133
+ };
134
+
135
+ /** An absent override falls back to schema parsing unless generation is disabled. */
136
+ type ResolverFallbackOutput<S extends StandardSchemaV1, Props> = Props extends {
137
+ resolverEnabled: false;
138
+ }
139
+ ? FormInput<S>
140
+ : Props extends { resolverEnabled?: infer Enabled }
141
+ ? false extends Enabled
142
+ ? FormInput<S> | FormOutput<S>
143
+ : FormOutput<S>
144
+ : FormOutput<S>;
145
+
146
+ type ResolverSubmitOutput<
147
+ S extends StandardSchemaV1,
148
+ Output,
149
+ Props,
150
+ > = Props extends { resolver: undefined }
151
+ ? ResolverFallbackOutput<S, Props>
152
+ : Props extends { resolver: Resolver<FormInput<S>, unknown, Output> }
153
+ ? Output
154
+ : Output | ResolverFallbackOutput<S, Props>;
155
+
156
+ type ConditionalFormOptions<S extends StandardSchemaV1> =
157
+ | ParsedFormOptions<S>
158
+ | RawFormOptions<S>
159
+ | DisabledFormOptions<S>
160
+ | VariableFormOptions<S>
161
+ | OptionalResolverFormOptions<S, unknown>;
162
+
163
+ type GeneratedSubmitOutput<S extends StandardSchemaV1, Props> = Props extends
164
+ | { submitValues: "raw" }
165
+ | { resolverEnabled: false }
166
+ ? FormInput<S>
167
+ : Props extends { submitValues?: infer Mode }
168
+ ? "raw" extends Mode
169
+ ? FormInput<S> | ResolverFallbackOutput<S, Props>
170
+ : ResolverFallbackOutput<S, Props>
171
+ : ResolverFallbackOutput<S, Props>;
172
+
173
+ type CustomSubmitOutput<S extends StandardSchemaV1, R> =
174
+ R extends Resolver<FormInput<S>, unknown, infer Output> ? Output : never;
175
+
176
+ /** Distribute over configurations so each branch contributes only its own output. */
177
+ type ConditionalSubmitOutput<
178
+ S extends StandardSchemaV1,
179
+ Props,
180
+ > = Props extends undefined
181
+ ? FormOutput<S>
182
+ : Props extends { resolver: infer R }
183
+ ? undefined extends R
184
+ ? CustomSubmitOutput<S, R> | GeneratedSubmitOutput<S, Props>
185
+ : CustomSubmitOutput<S, R>
186
+ : Props extends { resolver?: infer R }
187
+ ? CustomSubmitOutput<S, R> | GeneratedSubmitOutput<S, Props>
188
+ : GeneratedSubmitOutput<S, Props>;
189
+
190
+ type SuppliedFormProps<Props> = undefined extends Props
191
+ ? Partial<Exclude<Props, undefined>>
192
+ : Props;
193
+
194
+ // Keep only supplied props so unused React Hook Form controls do not leak across peers.
195
+ type FormOptions<Input extends FieldValues, Output, Props> = Omit<
196
+ Props,
197
+ "resolver" | "resolverEnabled" | "submitValues"
198
+ > &
199
+ Pick<UseFormProps<Input, unknown, Output>, "resolver">;
200
+
201
+ /** React Hook Form methods for the selected form schema. */
202
+ type ReactHookFormContractAdapterShape<S extends StandardSchemaV1> = {
203
+ /** Generate options for React Hook Form or a wrapper. */
204
+ formOptions: {
205
+ (
206
+ props?: undefined,
207
+ ): FormOptions<FormInput<S>, FormOutput<S>, Record<never, never>>;
208
+ <Output, Props extends SuppliedResolverFormOptions<S, Output>>(
209
+ props: SuppliedResolverFormOptions<S, Output> & Props,
210
+ ): FormOptions<FormInput<S>, ResolverSubmitOutput<S, Output, Props>, Props>;
211
+ <Props extends RawFormOptions<S> | DisabledFormOptions<S>>(
212
+ props: Props,
213
+ ): FormOptions<FormInput<S>, FormInput<S>, Props>;
214
+ <Props extends ParsedFormOptions<S>>(
215
+ props: Props,
216
+ ): FormOptions<FormInput<S>, FormOutput<S>, Props>;
217
+ <Props extends VariableFormOptions<S>>(
218
+ props: Props,
219
+ ): FormOptions<FormInput<S>, FormInput<S> | FormOutput<S>, Props>;
220
+ <Output, Props extends OptionalResolverFormOptions<S, Output>>(
221
+ props: OptionalResolverFormOptions<S, Output> & Props,
222
+ ): FormOptions<
223
+ FormInput<S>,
224
+ Output | ResolverFallbackOutput<S, Props>,
225
+ Props
226
+ >;
227
+ <Props extends ParsedFormOptions<S>>(
228
+ props?: Props,
229
+ ): FormOptions<FormInput<S>, FormOutput<S>, Partial<Props>>;
230
+ <Props extends VariableFormOptions<S>>(
231
+ props: Props | undefined,
232
+ ): FormOptions<FormInput<S>, FormInput<S> | FormOutput<S>, Partial<Props>>;
233
+ <Output, Props extends OptionalResolverFormOptions<S, Output>>(
234
+ props: (OptionalResolverFormOptions<S, Output> & Props) | undefined,
235
+ ): FormOptions<
236
+ FormInput<S>,
237
+ ResolverSubmitOutput<S, Output, Props> | FormOutput<S>,
238
+ Partial<Props>
239
+ >;
240
+ <Props extends ConditionalFormOptions<S> | undefined>(
241
+ props: Props,
242
+ ): FormOptions<
243
+ FormInput<S>,
244
+ ConditionalSubmitOutput<S, Props>,
245
+ SuppliedFormProps<Props>
246
+ >;
247
+ };
248
+
249
+ /** Convenience wrapper around useForm(formOptions(props)). */
250
+ useForm: {
251
+ <Output>(
252
+ props: CustomFormOptions<S, Output>,
253
+ ): UseFormReturn<FormInput<S>, unknown, Output>;
254
+ (
255
+ props: RawFormOptions<S> | DisabledFormOptions<S>,
256
+ ): UseFormReturn<FormInput<S>, unknown, FormInput<S>>;
257
+ (
258
+ props?: ParsedFormOptions<S>,
259
+ ): UseFormReturn<FormInput<S>, unknown, FormOutput<S>>;
260
+ (
261
+ props?: VariableFormOptions<S>,
262
+ ): UseFormReturn<FormInput<S>, unknown, FormInput<S> | FormOutput<S>>;
263
+ <Output, Props extends OptionalResolverFormOptions<S, Output>>(
264
+ props: OptionalResolverFormOptions<S, Output> & Props,
265
+ ): UseFormReturn<
266
+ FormInput<S>,
267
+ unknown,
268
+ Output | ResolverFallbackOutput<S, Props>
269
+ >;
270
+ <Output, Props extends OptionalResolverFormOptions<S, Output>>(
271
+ props: (OptionalResolverFormOptions<S, Output> & Props) | undefined,
272
+ ): UseFormReturn<
273
+ FormInput<S>,
274
+ unknown,
275
+ ResolverSubmitOutput<S, Output, Props> | FormOutput<S>
276
+ >;
277
+ <Props extends ConditionalFormOptions<S> | undefined>(
278
+ props: Props,
279
+ ): UseFormReturn<FormInput<S>, unknown, ConditionalSubmitOutput<S, Props>>;
280
+ };
107
281
  };
108
282
 
109
283
  /**
110
- * React Hook Form adapter for one contract whose body schema input is
111
- * object-shaped.
284
+ * Adapter for a contract body, optionally using a separate form schema.
285
+ * The selected schema must have object-shaped input for named form fields.
112
286
  */
113
- export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
114
- [FormInput<TContract>] extends [never]
287
+ export type ReactHookFormContractAdapter<
288
+ TContract extends HttpContractConfig,
289
+ TSchema extends StandardSchemaV1 = BodySchema<TContract>,
290
+ > = [BodySchema<TContract>] extends [never]
291
+ ? never
292
+ : [FormInput<TSchema>] extends [never]
115
293
  ? never
116
- : ReactHookFormContractAdapterShape<TContract>;
117
-
118
- function createReactHookFormAdapter<TContractLike extends ContractLike>(
119
- contract: ReactHookFormContract<TContractLike>,
120
- ): ReactHookFormContractAdapter<ResolveContract<TContractLike>>;
121
- function createReactHookFormAdapter<TContractLike extends ContractLike>(
122
- contract: ReactHookFormContract<TContractLike>,
123
- ): ReactHookFormContractAdapterShape<ResolveContract<TContractLike>> {
294
+ : ReactHookFormContractAdapterShape<TSchema>;
295
+
296
+ function createReactHookFormAdapter(
297
+ contract: ContractLike,
298
+ schema?: StandardSchemaV1,
299
+ ): ReactHookFormContractAdapterShape<StandardSchemaV1<FieldValues, unknown>> {
124
300
  const resolvedContract = resolveContract(contract);
125
301
 
126
302
  if (!resolvedContract.body) {
@@ -131,57 +307,101 @@ function createReactHookFormAdapter<TContractLike extends ContractLike>(
131
307
  );
132
308
  }
133
309
 
134
- type Input = FormInput<ResolveContract<TContractLike>>;
135
- type Output = FormOutput<ResolveContract<TContractLike>>;
136
-
137
- function formOptions(
138
- props?: RhfFormOptions<ResolveContract<TContractLike>>,
139
- ): UseFormProps<Input, unknown, Output> {
140
- const bodySchema = resolvedContract.body as StandardSchemaV1<Input, Output>;
310
+ type Input = FieldValues;
311
+ type Schema = StandardSchemaV1<Input, unknown>;
312
+ type Options = RhfFormOptions<Schema, unknown> & {
313
+ resolver?: Resolver<Input, unknown, unknown>;
314
+ resolverEnabled?: boolean;
315
+ submitValues?: "raw" | "parsed";
316
+ };
317
+ const formSchema = (schema ?? resolvedContract.body) as Schema;
141
318
 
319
+ function formOptions(props?: Options): UseFormProps<Input, unknown, unknown> {
142
320
  const {
143
321
  resolverEnabled = true,
144
322
  resolver: resolverOverride,
323
+ submitValues = "parsed",
145
324
  ...rest
146
325
  } = props ?? {};
147
326
 
148
- const resolver =
149
- resolverOverride ??
150
- (resolverEnabled
151
- ? standardSchemaResolver<Input, unknown, Output>(bodySchema)
152
- : undefined);
327
+ if (resolverOverride && props?.submitValues !== undefined) {
328
+ throw new Error(
329
+ `rhf(${resolvedContract.name}): A custom resolver owns its submit values; omit submitValues.`,
330
+ );
331
+ }
332
+ if (
333
+ props?.submitValues === "parsed" &&
334
+ !resolverEnabled &&
335
+ !resolverOverride
336
+ ) {
337
+ throw new Error(
338
+ `rhf(${resolvedContract.name}): Parsed submit values require the generated resolver.`,
339
+ );
340
+ }
341
+
342
+ let resolver: Resolver<Input, unknown, unknown> | undefined =
343
+ resolverOverride;
344
+ if (!resolver && resolverEnabled) {
345
+ const generated =
346
+ submitValues === "parsed"
347
+ ? standardSchemaResolver<Input, unknown, unknown>(formSchema)
348
+ : standardSchemaResolver<Input, unknown, unknown>(
349
+ formSchema,
350
+ undefined,
351
+ { raw: true },
352
+ );
353
+ resolver = async (values, context, options) => {
354
+ // Capture data containers before asynchronous validation.
355
+ const snapshot = snapshotInput(values);
356
+ const result = await generated(
357
+ submitValues === "raw" ? snapshotInput(snapshot) : snapshot,
358
+ context,
359
+ options,
360
+ );
361
+ return submitValues === "raw" && Object.keys(result.errors).length === 0
362
+ ? { errors: {}, values: snapshot }
363
+ : result;
364
+ };
365
+ }
153
366
 
154
367
  return {
155
- ...(rest as UseFormProps<Input, unknown, Output>),
368
+ ...rest,
369
+ // RHF merges options on rerender; an explicit undefined clears an old resolver.
156
370
  resolver,
157
371
  };
158
372
  }
159
373
 
160
- function useForm(
161
- props?: RhfFormOptions<ResolveContract<TContractLike>>,
162
- ): UseFormReturn<Input, unknown, Output> {
374
+ function useForm(props?: Options): UseFormReturn<Input, unknown, unknown> {
163
375
  const options = formOptions(props);
164
- return rhfUseForm<Input, unknown, Output>(options);
376
+ return rhfUseForm<Input, unknown, unknown>(options);
165
377
  }
166
378
 
167
- return { formOptions, useForm };
379
+ return { formOptions, useForm } as ReactHookFormContractAdapterShape<Schema>;
168
380
  }
169
381
 
170
382
  /**
171
- * Create a React Hook Form adapter factory.
172
- *
173
- * Mirrors the React integration pattern used by `createReactQuery()` and `createNuqs()`:
174
- * create the adapter once, then bind individual contracts with `rhf(contract)`.
175
- * Contracts must declare a body schema whose input is object-shaped. After
176
- * binding, callers may override or disable the generated resolver through
177
- * `formOptions(...)` or `useForm(...)`.
383
+ * Create a React Hook Form adapter factory. Bind a contract with rhf(contract),
384
+ * or pass { schema } when the form's input differs from the request body.
385
+ * A separate schema must have object-shaped input and output assignable to the
386
+ * contract body input. Parsed submissions are the default; raw mode is opt-in.
178
387
  */
179
388
  export function createReactHookForm() {
180
- return function rhf<TContractLike extends ContractLike>(
389
+ function rhf<TContractLike extends ContractLike>(
181
390
  contract: ReactHookFormContract<TContractLike>,
182
- ): ReactHookFormContractAdapter<ResolveContract<TContractLike>> {
183
- return createReactHookFormAdapter(contract);
184
- };
391
+ ): ReactHookFormContractAdapter<ResolveContract<TContractLike>>;
392
+ function rhf<
393
+ TContractLike extends ContractLike,
394
+ TSchema extends StandardSchemaV1,
395
+ >(
396
+ contract: TContractLike,
397
+ options: {
398
+ schema: CompatibleFormSchema<ResolveContract<TContractLike>, TSchema>;
399
+ },
400
+ ): ReactHookFormContractAdapter<ResolveContract<TContractLike>, TSchema>;
401
+ function rhf(contract: ContractLike, options?: { schema: StandardSchemaV1 }) {
402
+ return createReactHookFormAdapter(contract, options?.schema);
403
+ }
404
+ return rhf;
185
405
  }
186
406
 
187
407
  /**