@reformer/core 11.0.0-beta.8 → 11.0.0-beta.9

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.
Files changed (224) hide show
  1. package/README.md +91 -73
  2. package/dist/behaviors-value-HA_Q03hc.js +108 -0
  3. package/dist/behaviors.js +370 -242
  4. package/dist/{create-model-BBRHhHwK.js → create-model-BvcbNPgL.js} +6 -6
  5. package/dist/date-utils-90-qZbse.js +43 -0
  6. package/dist/{en-CjAqn7h0.js → en-HP8XEych.js} +6 -3
  7. package/dist/form/aggregate-signals.d.ts +21 -63
  8. package/dist/form/behaviors/collections.d.ts +28 -7
  9. package/dist/form/behaviors/context.d.ts +19 -2
  10. package/dist/form/behaviors/index.d.ts +3 -3
  11. package/dist/form/behaviors/internals.d.ts +9 -2
  12. package/dist/form/behaviors/node.d.ts +25 -2
  13. package/dist/form/behaviors/operators.d.ts +17 -9
  14. package/dist/form/behaviors/schema-operators.d.ts +6 -4
  15. package/dist/form/behaviors/types.d.ts +12 -4
  16. package/dist/form/create-form.d.ts +47 -35
  17. package/dist/form/flow/flow.d.ts +87 -0
  18. package/dist/form/form-bundle.d.ts +45 -28
  19. package/dist/form/form-config-base.d.ts +46 -0
  20. package/dist/form/form-level.d.ts +121 -0
  21. package/dist/form/form-proxy-builder.d.ts +6 -22
  22. package/dist/form/index.d.ts +16 -21
  23. package/dist/form/nodes/form-array-node.d.ts +100 -0
  24. package/dist/form/nodes/form-control-node.d.ts +73 -0
  25. package/dist/form/nodes/form-group-node.d.ts +66 -0
  26. package/dist/form/nodes/node-kind.d.ts +29 -0
  27. package/dist/form/nodes/state/availability.d.ts +6 -0
  28. package/dist/form/nodes/state/component.d.ts +6 -0
  29. package/dist/form/nodes/state/index.d.ts +12 -0
  30. package/dist/form/nodes/state/interaction.d.ts +13 -0
  31. package/dist/form/nodes/state/issues.d.ts +9 -0
  32. package/dist/form/nodes/state/pending.d.ts +6 -0
  33. package/dist/form/schema/compile.d.ts +59 -0
  34. package/dist/form/schema/compiled.d.ts +143 -0
  35. package/dist/form/schema/row-key.d.ts +11 -0
  36. package/dist/form/schema-controller.d.ts +30 -14
  37. package/dist/form/signal-node-registry.d.ts +6 -7
  38. package/dist/form/submission.d.ts +93 -0
  39. package/dist/form/type-guards.d.ts +14 -73
  40. package/dist/form/types/contracts.d.ts +19 -92
  41. package/dist/form/types/field-config.d.ts +27 -0
  42. package/dist/form/types/index.d.ts +5 -57
  43. package/dist/form/types/nodes.d.ts +159 -0
  44. package/dist/form/types/schema-node.d.ts +139 -63
  45. package/dist/form/types/validation-schema.d.ts +23 -27
  46. package/dist/form/validation/context.d.ts +42 -7
  47. package/dist/form/validation/index.d.ts +9 -8
  48. package/dist/form/validation/operators.d.ts +22 -30
  49. package/dist/form/validation/run.d.ts +93 -18
  50. package/dist/form/validation/strategy.d.ts +41 -13
  51. package/dist/form/validation/types.d.ts +74 -20
  52. package/dist/form/validators/date-utils.d.ts +13 -1
  53. package/dist/form/validators/email.d.ts +8 -7
  54. package/dist/form/validators/file-type.d.ts +8 -7
  55. package/dist/form/validators/file-utils.d.ts +2 -1
  56. package/dist/form/validators/future-date.d.ts +8 -7
  57. package/dist/form/validators/index.d.ts +1 -1
  58. package/dist/form/validators/integer.d.ts +8 -7
  59. package/dist/form/validators/is-date.d.ts +11 -9
  60. package/dist/form/validators/is-number.d.ts +11 -9
  61. package/dist/form/validators/max-age.d.ts +13 -9
  62. package/dist/form/validators/max-date.d.ts +8 -7
  63. package/dist/form/validators/max-file-size.d.ts +8 -7
  64. package/dist/form/validators/max-files.d.ts +8 -7
  65. package/dist/form/validators/max-length.d.ts +14 -7
  66. package/dist/form/validators/max-total-file-size.d.ts +8 -7
  67. package/dist/form/validators/max.d.ts +10 -8
  68. package/dist/form/validators/min-age.d.ts +12 -9
  69. package/dist/form/validators/min-date.d.ts +8 -7
  70. package/dist/form/validators/min-file-size.d.ts +8 -7
  71. package/dist/form/validators/min-files.d.ts +11 -9
  72. package/dist/form/validators/min-length.d.ts +14 -7
  73. package/dist/form/validators/min.d.ts +10 -8
  74. package/dist/form/validators/multiple-of.d.ts +10 -8
  75. package/dist/form/validators/non-negative.d.ts +8 -7
  76. package/dist/form/validators/non-zero.d.ts +8 -7
  77. package/dist/form/validators/past-date.d.ts +8 -7
  78. package/dist/form/validators/pattern.d.ts +9 -8
  79. package/dist/form/validators/phone.d.ts +8 -7
  80. package/dist/form/validators/required.d.ts +11 -9
  81. package/dist/form/validators/url.d.ts +20 -13
  82. package/dist/form/validators/validation-issue.d.ts +8 -0
  83. package/dist/i18n/descriptor.d.ts +1 -1
  84. package/dist/i18n/en.json.d.ts +5 -1
  85. package/dist/i18n/locale-store.d.ts +7 -0
  86. package/dist/i18n/message-format.d.ts +24 -4
  87. package/dist/i18n/ru.json.d.ts +5 -1
  88. package/dist/i18n/translator.d.ts +11 -0
  89. package/dist/i18n/validation-message.d.ts +42 -7
  90. package/dist/i18n.d.ts +7 -12
  91. package/dist/i18n.js +66 -283
  92. package/dist/index.d.ts +16 -7
  93. package/dist/index.js +924 -2855
  94. package/dist/loader-BWkigpVe.js +52 -0
  95. package/dist/locale/en.js +1 -1
  96. package/dist/locale/en.json +4 -1
  97. package/dist/locale/ru.js +4 -1
  98. package/dist/locale/ru.json +4 -1
  99. package/dist/locale-CG54AU2Q.js +219 -0
  100. package/dist/locale.js +1 -1
  101. package/dist/model/behaviors-value.d.ts +24 -6
  102. package/dist/model/create-model.d.ts +2 -2
  103. package/dist/model/derived-registry.d.ts +1 -1
  104. package/dist/model/index.d.ts +11 -5
  105. package/dist/model/model-nodes.d.ts +79 -12
  106. package/dist/model/model-signals-proxy.d.ts +5 -6
  107. package/dist/model/model-value-proxy.d.ts +20 -15
  108. package/dist/model/safe-effect.d.ts +33 -1
  109. package/dist/model/types.d.ts +63 -13
  110. package/dist/model.js +20 -18
  111. package/dist/node-kind-C-r6ZMdo.js +15 -0
  112. package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
  113. package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
  114. package/dist/platforms/react/form-bundle-context.d.ts +2 -2
  115. package/dist/platforms/react/hooks/types.d.ts +57 -38
  116. package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
  117. package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
  118. package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
  119. package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
  120. package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
  121. package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
  122. package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
  123. package/dist/platforms/react/i18n/context.d.ts +6 -1
  124. package/dist/platforms/react/i18n/hooks.d.ts +16 -10
  125. package/dist/platforms/react/i18n/provider.d.ts +9 -2
  126. package/dist/platforms/react/index.d.ts +6 -4
  127. package/dist/react.d.ts +22 -0
  128. package/dist/react.js +505 -0
  129. package/dist/runtime-token.d.ts +1 -2
  130. package/dist/safe-effect-BbtX3KuN.js +643 -0
  131. package/dist/schema-controller-CpiO2JoK.js +143 -0
  132. package/dist/signals.d.ts +15 -9
  133. package/dist/signals.js +11 -6
  134. package/dist/strategy-CoFSemuP.js +375 -0
  135. package/dist/type-guards-D-N_5Ddh.js +19 -0
  136. package/dist/validation-issue-B9mjI8jz.js +7 -0
  137. package/dist/validation-message-Ced9MGxs.js +194 -0
  138. package/dist/validation.js +9 -9
  139. package/dist/validators/email.js +5 -5
  140. package/dist/validators/future-date.js +8 -8
  141. package/dist/validators/integer.js +3 -3
  142. package/dist/validators/is-date.js +4 -4
  143. package/dist/validators/is-number.js +4 -4
  144. package/dist/validators/max-age.js +8 -9
  145. package/dist/validators/max-date.js +8 -8
  146. package/dist/validators/max-length.js +7 -7
  147. package/dist/validators/max.js +4 -4
  148. package/dist/validators/min-age.js +10 -8
  149. package/dist/validators/min-date.js +3 -3
  150. package/dist/validators/min-length.js +5 -5
  151. package/dist/validators/min.js +4 -4
  152. package/dist/validators/multiple-of.js +11 -7
  153. package/dist/validators/non-negative.js +3 -3
  154. package/dist/validators/non-zero.js +3 -3
  155. package/dist/validators/past-date.js +8 -8
  156. package/dist/validators/pattern.js +5 -5
  157. package/dist/validators/phone.js +11 -9
  158. package/dist/validators/required.js +2 -2
  159. package/dist/validators/url.js +48 -12
  160. package/dist/validators.js +17 -17
  161. package/docs/llms/01-api-reference.md +60 -34
  162. package/docs/llms/02-quick-start.md +111 -80
  163. package/docs/llms/03-api-signatures.md +368 -153
  164. package/docs/llms/04-common-patterns.md +58 -56
  165. package/docs/llms/05-common-mistakes.md +69 -44
  166. package/docs/llms/06-troubleshooting.md +14 -9
  167. package/docs/llms/07-complete-import.md +74 -43
  168. package/docs/llms/08-form-types.md +11 -4
  169. package/docs/llms/09-formschema.md +129 -81
  170. package/docs/llms/10-arrays.md +106 -75
  171. package/docs/llms/11-async-watchfield.md +2 -2
  172. package/docs/llms/13-multi-step.md +134 -63
  173. package/docs/llms/14-extended-mistakes.md +20 -17
  174. package/docs/llms/15-project-structure.md +221 -74
  175. package/docs/llms/16-ui-components.md +55 -42
  176. package/docs/llms/17-nonexistent-api.md +199 -32
  177. package/docs/llms/18-conditional-fields.md +66 -32
  178. package/docs/llms/19-reading-values.md +15 -3
  179. package/docs/llms/20-compute-vs-watch.md +16 -10
  180. package/docs/llms/21-array-operations.md +51 -27
  181. package/docs/llms/22-cycle-detection.md +1 -1
  182. package/docs/llms/23-copy-from.md +2 -2
  183. package/docs/llms/24-sync-fields.md +6 -5
  184. package/docs/llms/25-reset-when.md +14 -7
  185. package/docs/llms/26-transform-value.md +4 -4
  186. package/docs/llms/27-revalidate-when.md +16 -15
  187. package/docs/llms/28-submit-and-reset.md +276 -128
  188. package/docs/llms/29-async-preload.md +39 -29
  189. package/docs/llms/30-type-safety-recipes.md +105 -51
  190. package/docs/llms/31-async-validator-debounce.md +96 -53
  191. package/docs/llms/33-validation-strategy.md +177 -65
  192. package/docs/llms/34-i18n.md +69 -43
  193. package/docs/llms/35-unified-contract.md +100 -39
  194. package/docs/llms/36-issue-messages.md +284 -0
  195. package/docs/llms/37-compiled-schema.md +332 -0
  196. package/docs/llms/38-form-flow.md +397 -0
  197. package/llms-index.json +1 -1
  198. package/llms.txt +6084 -3420
  199. package/package.json +23 -4
  200. package/dist/behaviors-value-C7dSEhCc.js +0 -134
  201. package/dist/date-utils-dTT_x_Be.js +0 -31
  202. package/dist/form/create-core-form.d.ts +0 -72
  203. package/dist/form/factories/index.d.ts +0 -6
  204. package/dist/form/factories/node-factory.d.ts +0 -189
  205. package/dist/form/form-submitter.d.ts +0 -121
  206. package/dist/form/nodes/array-node.d.ts +0 -276
  207. package/dist/form/nodes/field-node.d.ts +0 -297
  208. package/dist/form/nodes/form-node.d.ts +0 -326
  209. package/dist/form/nodes/group-node.d.ts +0 -284
  210. package/dist/form/nodes/model-array-node.d.ts +0 -126
  211. package/dist/form/nodes/subscription-manager.d.ts +0 -180
  212. package/dist/form/status-machine.d.ts +0 -115
  213. package/dist/form/types/deep-schema.d.ts +0 -107
  214. package/dist/form/types/form-proxy.d.ts +0 -141
  215. package/dist/form/unique-id.d.ts +0 -45
  216. package/dist/form/validation/config.d.ts +0 -73
  217. package/dist/form/validation/error-handler.d.ts +0 -166
  218. package/dist/form/validators/validation-error.d.ts +0 -8
  219. package/dist/index-BrNkQj1y.js +0 -105
  220. package/dist/loader-Dv_Hl7JC.js +0 -216
  221. package/dist/model-value-proxy-caTmAiUd.js +0 -419
  222. package/dist/node-BeV_lV5i.js +0 -100
  223. package/dist/strategy-DgzELWno.js +0 -204
  224. package/dist/validation-error-B-U3RsV0.js +0 -11
@@ -1,21 +1,13 @@
1
1
  ## 3. COMMON PATTERNS
2
2
 
3
- Все паттерны — на архитектуре M1: значения в модели (`model.$.field`), поведение (`defineFormBehavior`)
4
- на живых сигналах, валидация — отдельной ambient-схемой (`defineValidationSchema`, прогон по требованию
5
- через `validateModel`). Слои раздельны: layout НЕ несёт validators.
3
+ Значения живут в модели (`model.$.field`), поведение (`defineFormBehavior`) работает на живых
4
+ сигналах, валидация — отдельная схема (`defineValidation`, прогон по требованию через
5
+ раннер). Слои раздельны: схема формы правил НЕ несёт.
6
6
 
7
7
  ### Conditional Fields with Auto-Reset
8
8
 
9
- ```typescript
10
- import { enableWhen } from '@reformer/core';
11
-
12
- // поле включается по условию; при выключении — сброс к initial
13
- enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', {
14
- resetOnDisable: true,
15
- });
16
- ```
17
-
18
- Или декларативно внутри `defineFormBehavior`:
9
+ `enableWhen` — оператор поведения: поле включается по условию, при выключении сбрасывается к
10
+ initial. Вызывается внутри `defineFormBehavior`:
19
11
 
20
12
  ```typescript
21
13
  import { defineFormBehavior, enableWhen } from '@reformer/core/behaviors';
@@ -77,14 +69,14 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
77
69
  });
78
70
  ```
79
71
 
80
- > Низкоуровневый аналог — примитив `watchField(model.$.region, cb)` из `@reformer/core` (без debounce/AbortSignal;
72
+ > Низкоуровневый аналог — примитив `watchField(model.$.region, cb)` из `@reformer/core/model` (без debounce/AbortSignal;
81
73
  > для сети предпочтителен `onChange`). См. `20-compute-vs-watch.md`, `32-async-options-loading.md`.
82
74
 
83
75
  ### Reading values
84
76
 
85
77
  ```typescript
86
- // В React-компоненте — хуки
87
- const { value, errors, disabled } = useFormControl(form.email);
78
+ // В React-компоненте — хуки (@reformer/core/react)
79
+ const { value, issues, disabled } = useFormControl(form.email);
88
80
  const loanType = useFormControlValue(form.loanType); // значение напрямую
89
81
 
90
82
  // Вне React — из модели
@@ -96,50 +88,56 @@ model.get(); // весь объект-снимок
96
88
 
97
89
  ### Submit + validation
98
90
 
99
- Валидация — отдельная `ValidationSchema<T>` (`defineValidationSchema`), а не часть layout. Раннер
100
- `validateModel(model, schema)` сам роутит ошибки в ноды формы и возвращает `Promise<boolean>`
101
- (`false` = есть блокирующая ошибка; `severity: 'warning'` не блокирует).
91
+ Валидация — отдельная `ValidationDefinition<T>` (`defineValidation`), а не часть layout; в сборку
92
+ она уходит полем `validation`. `bundle.submission.submit(handler)` прогоняет все правила один раз,
93
+ разносит замечания по нодам и зовёт обработчик только при статусе `valid` (`severity: 'warning'` не
94
+ блокирует). Результат — `SubmitResult`.
102
95
 
103
96
  ```typescript
104
- import { validateModel } from '@reformer/core/validation';
105
-
106
- async function handleSubmit(e: React.FormEvent) {
107
- e.preventDefault();
108
- const ok = await validateModel(model, schema); // ошибки сами доезжают до нод формы (UI подсветит)
109
- if (!ok) return; // false → есть блокирующая ошибка
110
- await api.send(model.get());
111
- model.reset(); // к initial-снимку
97
+ async function handleSubmit(event: React.FormEvent) {
98
+ event.preventDefault();
99
+ const outcome = await bundle.submission.submit(async (values) => {
100
+ await api.send(values);
101
+ bundle.model.reset(); // к initial-снимку — только после успеха
102
+ });
103
+ if (outcome.status === 'invalid') showHint('Проверьте форму'); // поля уже подсвечены
112
104
  }
113
105
  ```
114
106
 
115
- Multi-step: держи отдельные под-схемы на шаг и вызывай `validateModel(model, stepSchema)`. Для wizard'а —
116
- `makeValidationConfig(model)` → `{ validateStep, validateAll }` (`validateAll` прогоняет полную
117
- `apply(...STEP_SCHEMAS, extras)`). См. `13-multi-step.md`, `28-submit-and-reset.md`.
107
+ Без сборки — раннер `validateModel(model, schema)`: сам разносит замечания по нодам и отвечает
108
+ `Promise<boolean>`.
109
+
110
+ Multi-step: шаги и их правила — поток сборки `createForm({ flow: { steps: [{ id, rules }] } })`
111
+ (`id` — `selector` шага), правила вне шагов — `validation: { extras }`. Сборка отдаёт
112
+ `bundle.validateStep(id)`, `bundle.validate()` и поток `bundle.flow` (`next` / `previous` / `goTo`);
113
+ визард принимает поток пропом `flow`. См. `13-multi-step.md`, `28-submit-and-reset.md`,
114
+ `38-form-flow.md`.
118
115
 
119
116
  ### Cross-field validation — через `cross`
120
117
 
121
- Cross-field правило — обычная функция над **снапшотом** модели (`fn` получает `model.get()`),
122
- навешивается оператором `cross(sig, fn)` на поле-носитель ошибки внутри схемы:
118
+ Cross-field правило — обычная функция над **снимком** модели. Оператор `cross` берётся из
119
+ аргумента схемы и вешает правило на поле-носитель ошибки; тип снимка выведен из схемы:
123
120
 
124
121
  ```typescript
125
- import { defineValidationSchema, validate, cross } from '@reformer/core/validation';
122
+ import { defineValidation, validate } from '@reformer/core/validation';
126
123
  import { required, min } from '@reformer/core/validators';
127
- import type { ValidationError } from '@reformer/core';
124
+ import type { ValidationIssue } from '@reformer/core';
128
125
 
129
- // снапшот формы читается напрямую — без каста, соседние поля доступны как поля объекта
130
- const initialPaymentVsProperty = (f: MyForm): ValidationError | null =>
131
- f.initialPayment && f.propertyValue && f.initialPayment > f.propertyValue
132
- ? { code: 'tooHigh', message: 'Взнос не может превышать стоимость' }
126
+ // снимок формы читается напрямую — без каста, соседние поля доступны как поля объекта
127
+ const initialPaymentVsProperty = (form: MyForm): ValidationIssue | null =>
128
+ form.initialPayment && form.propertyValue && form.initialPayment > form.propertyValue
129
+ ? { code: 'initialPaymentTooHigh' } // текст кода — в словаре формы (defineIssueMessages)
133
130
  : null;
134
131
 
135
- const schema = defineValidationSchema<MyForm>(({ model }) => {
132
+ const schema = defineValidation<MyForm>(({ model, cross }) => {
136
133
  validate(model.$.initialPayment, [required(), min(0)]);
137
134
  cross(model.$.initialPayment, initialPaymentVsProperty); // ошибка сядет на initialPayment
138
135
  });
139
136
  ```
140
137
 
141
- > Для элемента массива / под-модели захвати нужный снапшот в замыкание (`const item = im.get();
142
- > cross(im.$.x, () => rule(item))`) — `fn` всегда получает модель ТЕКУЩЕГО scope, а не под-модель.
138
+ > Правила строки массива и подформы — отдельная схема над их типом, подключённая через
139
+ > `applyEach(model.$.items, rowRules)` / `apply(model.$.group, groupRules)`. У такой схемы своя
140
+ > область: её `cross` получает снимок строки или группы.
143
141
 
144
142
  ### Conditional validation — `validateWhen`
145
143
 
@@ -148,21 +146,25 @@ const schema = defineValidationSchema<MyForm>(({ model }) => {
148
146
  включение/выключение самих проверок:
149
147
 
150
148
  ```typescript
151
- import { validateWhen, validate, cross } from '@reformer/core/validation';
152
-
153
- validateWhen(
154
- () => model.loanType === 'mortgage',
155
- () => {
156
- validate(model.$.propertyValue, [required(), min(1000000)]);
157
- cross(model.$.initialPayment, initialPaymentVsProperty);
158
- }
159
- );
149
+ import { defineValidation, validateWhen, validate } from '@reformer/core/validation';
150
+
151
+ const loanRules = defineValidation<MyForm>(({ model, cross }) => {
152
+ validateWhen(
153
+ () => model.loanType === 'mortgage',
154
+ () => {
155
+ validate(model.$.propertyValue, [required(), min(1000000)]);
156
+ cross(model.$.initialPayment, initialPaymentVsProperty);
157
+ }
158
+ );
159
+ });
160
160
  ```
161
161
 
162
162
  ### Перезапуск прогона — `revalidateWhen` (мост поведение → валидация)
163
163
 
164
- Схема прогоняется по требованию (submit/шаг). Чтобы перезапустить её при изменении зависимости —
165
- `revalidateWhen` из **поведения** (единственный мост поведение → валидация):
164
+ Схема прогоняется по требованию (submit/шаг) либо живой стратегией сборки
165
+ (`validation: { strategy: 'change' }`, см. `33-validation-strategy.md`). Чтобы перезапустить
166
+ отдельную схему при изменении зависимости — `revalidateWhen` из **поведения** (мост
167
+ поведение → валидация):
166
168
 
167
169
  ```typescript
168
170
  import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
@@ -176,11 +178,11 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
176
178
  ### Extracting named rules
177
179
 
178
180
  Когда тело правила растёт — выноси в именованную функцию. Field-правило типизируй `Rule<T>`
179
- (`(value) => ValidationError | null`), cross-field — обычной `(f: Root) => ValidationError | null`.
181
+ (`(value) => ValidationIssue | null`), cross-field — обычной `(f: Root) => ValidationIssue | null`.
180
182
  Схема остаётся плоской и читается как оглавление:
181
183
 
182
184
  ```typescript
183
- import { defineValidationSchema, validate } from '@reformer/core/validation';
185
+ import { defineValidation, validate } from '@reformer/core/validation';
184
186
  import { required } from '@reformer/core/validators';
185
187
  import type { Rule } from '@reformer/core/validation';
186
188
 
@@ -188,10 +190,10 @@ import type { Rule } from '@reformer/core/validation';
188
190
  const validateAdultAge: Rule<string> = (value) => {
189
191
  if (!value) return null;
190
192
  const age = new Date().getFullYear() - new Date(value).getFullYear();
191
- return age < 18 ? { code: 'tooYoung', message: 'Минимум 18 лет' } : null;
193
+ return age < 18 ? { code: 'tooYoung' } : null;
192
194
  };
193
195
 
194
- const schema = defineValidationSchema<MyForm>(({ model }) => {
196
+ const schema = defineValidation<MyForm>(({ model }) => {
195
197
  validate(model.$.birthDate, [required(), validateAdultAge]);
196
198
  });
197
199
  ```
@@ -2,9 +2,11 @@
2
2
 
3
3
  ### Imports rule (#1 cause of cascading errors — read first)
4
4
 
5
- - Модель/форма/хуки/типы (`FormModel`, `ValidationError`)/**примитивы behaviors** — из `@reformer/core`.
6
- - Схема валидации (`defineValidationSchema` + операторы `validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`) и раннер `validateModel` — из `@reformer/core/validation`.
5
+ - Модель/форма/типы (`createForm`, `FormModel`, `FormSchemaNode`, `ValidationIssue`, `FormFlow`) — из `@reformer/core`.
6
+ - Хуки (`useFormBundle`, `useFormControl`, `useFormFlow`, `useIssueMessage`, …) и `I18nProvider` — из `@reformer/core/react`.
7
+ - Схема валидации (`defineValidation` + операторы `validate`/`validateAsync`/`validateWhen`/`apply`/`applyEach`) и раннер `validateModel` — из `@reformer/core/validation`. `cross` берётся из аргумента схемы.
7
8
  - Чистые фабрики валидаторов (`required`/`min`/`email`/…) — из `@reformer/core/validators`.
9
+ - Словарь текстов замечаний (`defineIssueMessages`, `resolveIssueMessage`) — из `@reformer/core/i18n`.
8
10
  - Декларативный DSL поведения (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
9
11
 
10
12
  ```typescript
@@ -12,40 +14,42 @@
12
14
  import {
13
15
  createModel,
14
16
  createForm,
15
- useFormControl,
16
- useFormControlValue,
17
- type FormProxy,
18
- type FieldConfig,
17
+ type FormGroup,
18
+ type FormSchemaNode,
19
19
  type FormModel,
20
- type ValidationError,
20
+ type ValidationIssue,
21
21
  } from '@reformer/core';
22
+ import { useFormBundle, useFormControl, useFormControlValue } from '@reformer/core/react';
23
+ import { defineIssueMessages } from '@reformer/core/i18n';
22
24
  // схема валидации + раннер — отдельный слой:
23
25
  import {
24
- defineValidationSchema,
26
+ defineValidation,
25
27
  validate,
26
28
  validateAsync,
27
29
  validateWhen,
28
- cross,
29
- each,
30
30
  apply,
31
+ applyEach,
31
32
  validateModel,
32
33
  type Rule,
33
34
  type AsyncRule,
34
- type ValidationSchema,
35
+ type ValidationDefinition,
35
36
  } from '@reformer/core/validation';
36
37
  import { required, min, max, email } from '@reformer/core/validators';
37
- // либо примитивы behaviors из основного пакета:
38
- import { computeFrom, enableWhen, copyFrom } from '@reformer/core';
39
- // либо декларативный DSL поведения:
40
- import { defineFormBehavior, compute, onChange } from '@reformer/core/behaviors';
38
+ // поведение формы — декларативный DSL:
39
+ import { defineFormBehavior, compute, enableWhen, copyFrom, onChange } from '@reformer/core/behaviors';
40
+ // примитивы над сигналами (вне формы, возвращают cleanup) — отдельный сабпат:
41
+ import { computeFrom, watchField } from '@reformer/core/model';
41
42
  ```
42
43
 
43
- > **watchField живёт в `@reformer/core`** (низкоуровневый примитив), НЕ в `@reformer/core/behaviors`.
44
+ > **В корне `@reformer/core` операторов поведения нет.** `computeFrom`, `copyFrom`, `watchField`,
45
+ > `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen` — примитивы сабпата
46
+ > `@reformer/core/model`; `enableWhen` / `disableWhen` — только операторы `@reformer/core/behaviors`.
44
47
  > В DSL для реакции на изменения используется `onChange`.
45
48
 
46
49
  ### Значения — в модели, не в форме
47
50
 
48
- Под M1 источник истины значений — модель. Форма (ноды) отражает их.
51
+ Источник истины значений — модель. Форма (ноды) отражает их; `form.x.setValue(v)` пишет в ту же
52
+ модель.
49
53
 
50
54
  ```typescript
51
55
  // ✅ читаем/пишем значение
@@ -68,7 +72,7 @@ const { value: loanType } = useFormControlValue(control.loanType);
68
72
  const loanType = useFormControlValue(control.loanType);
69
73
 
70
74
  // ALSO CORRECT - useFormControl returns object
71
- const { value, errors } = useFormControl(control.loanType);
75
+ const { value, issues } = useFormControl(control.loanType);
72
76
  ```
73
77
 
74
78
  ### Behaviors принимают СИГНАЛЫ, а не пути/форму
@@ -83,33 +87,41 @@ enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true })
83
87
  computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);
84
88
  ```
85
89
 
86
- ### Валидаторы — оператор `validate`, а не layout-нода
90
+ ### Валидаторы — оператор `validate`, а не узел схемы
87
91
 
88
- Валидация — **отдельный слой** (`@reformer/core/validation`), а не поле layout-ноды. Layout
89
- (схема `createForm` / JSON-DSL) больше **не несёт** `validators`: правила живут в `ValidationSchema`,
90
- а прогоняет их внешний раннер `validateModel`. ⚠️ `form.validate()`/`submit()` schema-валидацию
91
- **НЕ запускают** — прогон только через `validateModel(model, schema)`.
92
+ Валидация — **отдельный слой** (`@reformer/core/validation`), а не ключ узла. Схема формы
93
+ (билдер `createForm` / JSON-DSL) правил **не несёт**: они живут в `ValidationDefinition` и уходят в
94
+ сборку полем `validation` (шаги — `flow`). Запуск — уровнем формы:
95
+ `bundle.submission.submit(handler)`, `bundle.validate()`, `bundle.validateStep(id)`; без сборки —
96
+ `validateModel(model, schema)`. У нод методов `validate()` / `submit()` нет.
92
97
 
93
98
  ```typescript
94
- // ❌ WRONG - дерево { value, validators } и позиционная строка удалены
99
+ // ❌ WRONG - ключей value и validators у узла нет, как и записи «имя → узел»
95
100
  const schema = {
96
101
  email: { value: model.$.email, component: Input, validators: [required(), email()] },
97
102
  };
98
103
  validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
104
+ required({ message: 'Email is required' }); // опции message нет — текст живёт в словаре формы
99
105
 
100
- // ✅ CORRECT - операторы validate/validateAsync внутри defineValidationSchema; фабрики с options
101
- const emailSchema = defineValidationSchema<MyForm>(({ model }) => {
102
- validate(model.$.email, [required({ message: 'Email is required' }), email()]);
106
+ // ✅ CORRECT - операторы validate/validateAsync внутри defineValidation; у правила — код
107
+ const emailSchema = defineValidation<MyForm>(({ model }) => {
108
+ validate(model.$.email, [required({ code: 'email.required' }), email()]);
103
109
  validateAsync(model.$.email, [
104
110
  async (value, { signal }) => {
105
111
  const res = await fetch(`/api/free?email=${value}`, { signal });
106
- return (await res.json()).free ? null : { code: 'taken', message: 'Email занят' };
112
+ return (await res.json()).free ? null : { code: 'email.taken' };
107
113
  },
108
114
  ]);
109
115
  });
116
+ // тексты кодов — словарь формы
117
+ export const formMessages = defineIssueMessages({
118
+ 'email.required': 'Укажите email',
119
+ 'email.taken': 'Email уже занят',
120
+ });
110
121
 
111
- // прогон по требованию (submit/шаг): ошибки сами доезжают до нод формы, warnings не блокируют
112
- const ok = await validateModel(model, emailSchema);
122
+ // прогон — уровнем формы: замечания сами доезжают до нод, warnings не блокируют.
123
+ // Отклонённое async-правило — сбой: статус `error`, на поле замечание `ruleFailed`.
124
+ const outcome = await bundle.submission.submit(api.send); // createForm({ …, validation: emailSchema })
113
125
  ```
114
126
 
115
127
  ### Cross-field — оператор `cross`, а не `ctx.form`
@@ -124,44 +136,57 @@ watchField(path.amount, (amount, ctx) => {
124
136
  // ✅ CORRECT - compute (поведение) на сигналах; cross-field (валидация) через оператор cross
125
137
  compute(model.$.total, () => (model.amount ?? 0) * (model.rate ?? 0));
126
138
 
127
- // cross-field правило — обычная функция над снапшотом Root (`model.get()`), навешивается через cross(sig, fn):
128
- const amountVsMax = (f: MyForm): ValidationError | null =>
129
- f.amount != null && f.maxAmount != null && f.amount > f.maxAmount
130
- ? { code: 'tooBig', message: 'Превышает лимит' }
139
+ // cross-field правило — обычная функция над снимком модели; `cross` берётся из аргумента схемы:
140
+ const amountVsMax = (form: MyForm): ValidationIssue | null =>
141
+ form.amount != null && form.maxAmount != null && form.amount > form.maxAmount
142
+ ? { code: 'amount.tooBig', params: { max: form.maxAmount } }
131
143
  : null;
132
144
 
133
- const amountSchema = defineValidationSchema<MyForm>(({ model }) => {
134
- cross(model.$.amount, amountVsMax); // fn получает model.get(), вешает ошибку на model.$.amount
145
+ const amountSchema = defineValidation<MyForm>(({ model, cross }) => {
146
+ cross(model.$.amount, amountVsMax); // check получает снимок модели, ошибка — на model.$.amount
135
147
  });
136
148
  ```
137
149
 
138
- ### Form-shape types — `type` over `interface`
150
+ ### Form-shape types — `interface` или `type`
151
+
152
+ Форму данных можно объявлять и `interface`, и `type` — оба работают с `createModel<T>`,
153
+ `createForm<T>`, `FormGroup<T>` и `FormArray<T>`. Группу от поля `FormGroup` отличает по виду
154
+ значения (обычный объект, а не массив, дата, файл или функция), а не по index signature. Эталонная
155
+ форма (`CreditApplication`) объявлена через `interface`.
139
156
 
140
157
  ```typescript
141
- // ❌ interface лишён неявной index signature; конструкции ArrayNode<T> его отвергают
158
+ // ✅ interface
142
159
  export interface PropertyItem {
143
160
  type: PropertyType;
144
161
  description: string;
145
162
  }
146
163
 
147
- // ✅ type alias структурно совместим с Record<string, FormValue>
164
+ // ✅ type alias — то же самое
148
165
  export type PropertyItem = {
149
166
  type: PropertyType;
150
167
  description: string;
151
168
  };
152
169
  ```
153
170
 
171
+ Единственная оговорка — свои обобщённые хелперы: `interface` не присваивается
172
+ `Record<string, unknown>` (у него нет неявной index signature). Ограничение пиши через
173
+ `T extends object`, а не `T extends Record<string, unknown>`.
174
+
154
175
  `number | null` — конвенциональный тип для очищенного поля; встроенные валидаторы
155
176
  (`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`, `maxAge`) пропускают
156
177
  пустые значения внутри.
157
178
 
158
- ### Proxy не проходит instanceof
179
+ ### Вид ноды — гардами, а не instanceof
180
+
181
+ `FormControl`, `FormGroup`, `FormArray` — интерфейсы: классов нод в публичном API нет, проверка
182
+ `instanceof` не скомпилируется. Вид ноды различают гарды по метке вида:
159
183
 
160
184
  ```typescript
161
- // ❌ instanceof на Proxy не работает
162
- if (node instanceof FieldNode) { ... }
185
+ // ❌ FormControl — тип, а не класс
186
+ if (node instanceof FormControl) { ... }
163
187
 
164
188
  // ✅ type guards
165
- import { isFieldNode, isGroupNode, isArrayNode } from '@reformer/core';
166
- if (isFieldNode(node)) { ... }
189
+ import { isFormNode, isFormControl, isFormGroup, isFormArray } from '@reformer/core';
190
+ if (isFormControl(node)) node.updateComponentProps({ placeholder: 'Email' });
191
+ if (isFormArray(node)) node.push();
167
192
  ```
@@ -1,11 +1,16 @@
1
1
  ## 5. TROUBLESHOOTING
2
2
 
3
- | Error | Cause | Solution |
4
- | ------------------------------------------------------ | -------------------------------------------------------- | ------------------------------------------------- |
5
- | `'string' is not assignable to '{ message?: string }'` | Валидатору передали строку вместо options | Используй `required({ message: 'text' })` |
6
- | `Module has no exported member` | Неверный источник импорта | `watchField`/примитивы — из `@reformer/core`; DSL — из `@reformer/core/behaviors`; фабрики — из `@reformer/core/validators` |
7
- | `undefined` из `useFormControlValue` | Деструктурировали хук | `const v = useFormControlValue(...)` — без деструктуризации |
8
- | `enableWhen`/`disableWhen` не срабатывает | Поле не материализовано в форме (элемент массива) | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |
9
- | `Cycle detected` | Взаимные `compute`/`computeFrom` без стабилизации | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |
10
- | Значение поля не пишется | Пишем в форму вместо модели | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` |
11
- | Ошибки не появляются после submit | Не вызвали `validateModel` (`form.submit()` НЕ гоняет schema-валидацию) | `await validateModel(model, schema)` из `@reformer/core/validation` — он роутит ошибки в ноды |
3
+ | Error | Cause | Solution |
4
+ | -------------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5
+ | `'message' does not exist in type 'RuleOptions'` | Текст передан в правило — у правил текста нет | `required({ code: 'phone.required' })` + запись `'phone.required'` в `defineIssueMessages`; см. 36-issue-messages.md |
6
+ | `'string' is not assignable to 'RuleOptions'` | Валидатору передали строку вместо options | `required({ code: 'field.required' })` |
7
+ | `Module has no exported member` | Неверный источник импорта | хуки и `I18nProvider` — из `@reformer/core/react`; `watchField`/примитивы — из `@reformer/core/model`; DSL — из `@reformer/core/behaviors`; фабрики — из `@reformer/core/validators`; удалённое — 17-nonexistent-api.md |
8
+ | `undefined` из `useFormControlValue` | Деструктурировали хук | `const v = useFormControlValue(...)` — без деструктуризации |
9
+ | `enableWhen`/`disableWhen` не срабатывает | Поле не материализовано в форме (элемент массива) | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |
10
+ | `Cycle detected` | Взаимные `compute`/`computeFrom` без стабилизации | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |
11
+ | Значение поля не пишется | Пишем в форму вместо модели | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` / `form.field.setValue(...)` |
12
+ | Замечания не появляются после submit | Отправка в обход проверки (`api.send(model.get())`) | `await bundle.submission.submit(handler)` — один прогон правил, при отказе вся форма помечается тронутой; без сборки — `await validateModel(model, schema)` |
13
+ | На поле текст вида `loanAmount.required` | Свой код правила без записи в словаре формы | Добавь код в `defineIssueMessages` и подключи `ValidationMessagesProvider messages` (cdk) |
14
+ | `compileSchema: в области «#» селектор «X» повторяется` | Два узла с одним `selector` в одной области схемы | Разные селекторы; см. 37-compiled-schema.md |
15
+ | `createForm: шага «X» нет в потоке` / `FormFlow: шага «X» нет` | `validateStep(id)` / `flow.goTo(id)` с неизвестным шагом | `id` — из `flow.steps`; см. 38-form-flow.md |
16
+ | `flow.goTo(id)` вернул `reason: 'locked'` | Прыжок вперёд через непройденный шаг | Вперёд — `flow.next()`; `goTo` — назад и на открытые шаги |
@@ -1,63 +1,82 @@
1
1
  ## Import Patterns
2
2
 
3
3
  ```typescript
4
- // Модель, форма, хуки, типы, примитивы behaviors — из @reformer/core
4
+ // Модель, форма, уровень формы, типы — из @reformer/core. React здесь нет: корень работает и на сервере
5
5
  import {
6
- // фабрики
7
6
  createModel,
8
7
  createForm,
9
- // хуки
8
+ createFormFromModel,
9
+ disposeForm, // снять форму из createFormFromModel; сборку снимает bundle.dispose()
10
+ createSubmission, // отправка без сборки (визард в разметке без createForm)
11
+ arrayOf, // массив модели с шаблоном новой строки
12
+ compileSchema, // авторское дерево → CompiledSchema (обычно не нужен: createForm компилирует сам)
13
+ createFormFlow, // поток шагов без сборки
14
+ isFormControl,
15
+ isFormGroup,
16
+ isFormArray,
17
+ } from '@reformer/core';
18
+
19
+ // Привязка к React — хуки, контекст сборки, связка поля с контролом, провайдер локали — из /react
20
+ import {
21
+ useFormBundle,
10
22
  useFormControl,
11
23
  useFormControlValue,
12
24
  useArrayLength,
13
- // примитивы behaviors (принимают сигналы, возвращают cleanup)
14
- computeFrom,
15
- copyFrom,
16
- watchField,
17
- enableWhen,
18
- disableWhen,
19
- transformValue,
20
- resetWhen,
21
- syncFields,
22
- revalidateWhen,
23
- } from '@reformer/core';
25
+ useFormFlow, // снимок потока шагов для рендера
26
+ useFormValidation, // стратегия без сборки: submit() — попытка отправки, validate() — проверка
27
+ useIssueMessage, // текст замечания: словарь формы → локаль → английский → код
28
+ I18nProvider,
29
+ } from '@reformer/core/react';
30
+ import type { FieldControlState } from '@reformer/core/react';
24
31
 
25
32
  // Типы
26
33
  import type {
27
- FormModel, // реактивная модель данных
28
- FormProxy, // тип формы для props компонентов
29
- FieldNode, // узел одного поля
30
- GroupNode, // узел группы
31
- ArrayNode, // узел массива
32
- ModelArray, // реактивный массив модели (push/removeAt/at/map/length)
33
- ModelSignals, // дерево сигналов ($)
34
- PathAwareSignal, // сигнал, знающий свой путь
35
- ValidationError,
36
- FieldConfig, // { value, component, componentProps?, ... } — layout, БЕЗ валидаторов
37
- FormSchema,
38
- FieldControlState,
34
+ FormModel, // реактивная модель данных
35
+ FormBundle, // результат createForm: { model, form, render, flow, validate, validateStep, submission, dispose, … }
36
+ FormNode, // общий контракт ноды
37
+ FormControl, // нода одного поля
38
+ FormGroup, // нода группы — тип формы для props компонентов
39
+ FormArray, // нода массива под-форм
40
+ ModelArray, // реактивный массив модели (push/removeAt/at/map/length)
41
+ ModelSignals, // дерево ручек ($)
42
+ PathAwareSignal, // ручка, знающая свой путь
43
+ ValidationIssue, // { code, params?, severity? } — без текста
44
+ RuleOptions, // { code?, params? } — опции фабрик правил
45
+ FormSchemaNode, // узел схемы: поле | массив под-форм | подформа | контейнер
46
+ CompiledSchema, // скомпилированная схема: bundle.render.tree
47
+ FormValidation, // правила формы как данные: { rules?, extras?, strategy?, debounce? }
48
+ FormFlowConfig, // поток шагов в конфиге: { steps: [{ id, rules? }] }
49
+ FormFlow, // поток шагов сборки: current, next(), previous(), goTo(id)
50
+ FlowTransition, // итог перехода: moved | stayed
51
+ FormSubmission, // отправка сборки (bundle.submission): submit(handler), submitting
52
+ SubmitResult, // итог bundle.submission.submit: submitted | invalid | error | cancelled | failed
39
53
  } from '@reformer/core';
40
54
 
41
55
  // Схема валидации: операторы + раннер — из /validation
42
56
  import {
43
- defineValidationSchema,
57
+ defineValidation,
44
58
  validate,
45
59
  validateAsync,
46
60
  validateWhen,
47
- cross,
48
- each,
49
- apply,
50
- validateModel, // раннер: validateModel(model, schema) => Promise<boolean>
61
+ apply, // правила подформы: apply(model.$.group, rules); композиция: apply(rulesA, rulesB)
62
+ applyEach, // правила строк массива: applyEach(model.$.items, rules)
63
+ validateModel, // раннер без сборки: сбор + разнос, Promise<boolean>
64
+ runValidation, // только сбор: Promise<ValidationResult>
65
+ applyValidationResult, // разнос собранного результата по нодам
51
66
  } from '@reformer/core/validation';
52
67
  import type {
53
- Rule, // (value) => ValidationError | null
54
- AsyncRule, // (value, { signal }) => Promise<ValidationError | null>
55
- ValidationSchema,
68
+ Rule, // (value) => ValidationIssue | null
69
+ AsyncRule, // (value, { signal }) => Promise<ValidationIssue | null>
70
+ ValidationDefinition, // ({ model, cross }) => void — cross берётся из аргумента схемы
71
+ ValidationResult, // { status, issues, failures }
56
72
  } from '@reformer/core/validation';
57
73
 
58
74
  // Валидаторы — чистые фабрики из /validators (кладутся в validate(sig, [...]))
59
75
  import { required, min, max, email, minLength, pattern } from '@reformer/core/validators';
60
76
 
77
+ // Тексты замечаний и локаль без React — из /i18n
78
+ import { defineIssueMessages, resolveIssueMessage, createI18n } from '@reformer/core/i18n';
79
+
61
80
  // Декларативный DSL behaviors — из /behaviors
62
81
  import {
63
82
  defineFormBehavior,
@@ -75,25 +94,37 @@ import {
75
94
  applyEach,
76
95
  aggregateInto,
77
96
  exclusiveFlag,
97
+ hideWhen, // операторы узлов схемы — исполняет рендерер
98
+ renderEffect,
78
99
  } from '@reformer/core/behaviors';
100
+
101
+ // Примитивы над сигналами (принимают сигналы, возвращают cleanup) — из /model, в корне их нет
102
+ import { computeFrom, copyFrom, watchField } from '@reformer/core/model';
103
+ // Утилиты значений модели — тоже из /model
104
+ import {
105
+ deepEqual, // сравнение значений модели (на нём isDirty)
106
+ deferredWritesSettled, // дождаться отложенных записей поведения тика; undefined — ждать нечего
107
+ } from '@reformer/core/model';
79
108
  ```
80
109
 
81
- ### Form-shape тип должен быть `type`, а не `interface`
110
+ ### Форма данных — `interface` или `type`
82
111
 
83
- Прокси `createForm<T>` и типы `ArrayNode<U>` / `GroupNode<U>` требуют, чтобы form-shape
84
- структурно совпадал с `Record<string, FormValue>`. У `interface` нет неявной index signature,
85
- поэтому объявляй form-shape (и типы элементов массива, и вложенные группы) через `type`-alias:
112
+ Оба варианта работают одинаково: `createModel<T>`, `createForm<T>`, `FormGroup<T>` и `FormArray<T>`
113
+ принимают и `interface`, и `type`-alias — для корня, вложенных групп и элементов массивов. Группу
114
+ от поля сборка отличает по виду значения (обычный объект — группа; массив объектов — массив
115
+ под-форм; примитив, массив примитивов, дата, файл — поле), а не по index signature.
86
116
 
87
117
  ```typescript
88
- export type AddressForm = {
118
+ export interface AddressForm {
89
119
  street: string;
90
120
  city: string;
91
- };
121
+ }
92
122
 
93
- export type CoBorrower = {
123
+ export interface CoBorrower {
94
124
  fullName: string;
95
125
  phone: string;
96
- };
126
+ }
97
127
  ```
98
128
 
99
- См. `30-type-safety-recipes.md`.
129
+ Оговорка для своих обобщённых хелперов: `interface` не присваивается `Record<string, unknown>`, поэтому
130
+ ограничение пиши `T extends object`. См. `30-type-safety-recipes.md`.