@evolu/common 8.9.0 → 8.11.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.
Files changed (337) hide show
  1. package/dist/src/Bytes.d.ts +647 -0
  2. package/dist/src/Bytes.d.ts.map +1 -0
  3. package/dist/src/{Binary.js → Bytes.js} +266 -16
  4. package/dist/src/Config.d.ts +142 -0
  5. package/dist/src/Config.d.ts.map +1 -0
  6. package/dist/src/Config.js +181 -0
  7. package/dist/src/Console.d.ts +62 -7
  8. package/dist/src/Console.d.ts.map +1 -1
  9. package/dist/src/Console.js +20 -4
  10. package/dist/src/Crypto.d.ts +76 -4
  11. package/dist/src/Crypto.d.ts.map +1 -1
  12. package/dist/src/Crypto.js +55 -4
  13. package/dist/src/Error.d.ts +45 -0
  14. package/dist/src/Error.d.ts.map +1 -1
  15. package/dist/src/Error.js +69 -0
  16. package/dist/src/Fs.d.ts +376 -0
  17. package/dist/src/Fs.d.ts.map +1 -0
  18. package/dist/src/Fs.js +113 -0
  19. package/dist/src/Identicon.d.ts +2 -2
  20. package/dist/src/Identicon.js +2 -2
  21. package/dist/src/LeakDetector.d.ts +22 -3
  22. package/dist/src/LeakDetector.d.ts.map +1 -1
  23. package/dist/src/LeakDetector.js +12 -2
  24. package/dist/src/LockManager.d.ts +8 -0
  25. package/dist/src/LockManager.d.ts.map +1 -1
  26. package/dist/src/LockManager.js +6 -0
  27. package/dist/src/Number.d.ts +50 -7
  28. package/dist/src/Number.d.ts.map +1 -1
  29. package/dist/src/Number.js +47 -8
  30. package/dist/src/Object.d.ts +32 -0
  31. package/dist/src/Object.d.ts.map +1 -1
  32. package/dist/src/Object.js +46 -0
  33. package/dist/src/Platform.d.ts +47 -7
  34. package/dist/src/Platform.d.ts.map +1 -1
  35. package/dist/src/Platform.js +24 -5
  36. package/dist/src/Random.d.ts +25 -2
  37. package/dist/src/Random.d.ts.map +1 -1
  38. package/dist/src/Random.js +14 -2
  39. package/dist/src/Resource.d.ts +156 -1
  40. package/dist/src/Resource.d.ts.map +1 -1
  41. package/dist/src/Resource.js +201 -72
  42. package/dist/src/Schedule.d.ts +11 -10
  43. package/dist/src/Schedule.d.ts.map +1 -1
  44. package/dist/src/Schedule.js +1 -1
  45. package/dist/src/Sqlite.d.ts +132 -16
  46. package/dist/src/Sqlite.d.ts.map +1 -1
  47. package/dist/src/Sqlite.js +64 -10
  48. package/dist/src/Task.d.ts +15 -4
  49. package/dist/src/Task.d.ts.map +1 -1
  50. package/dist/src/Task.js +41 -15
  51. package/dist/src/Test.d.ts +9 -0
  52. package/dist/src/Test.d.ts.map +1 -1
  53. package/dist/src/Test.js +4 -0
  54. package/dist/src/Time.d.ts +179 -20
  55. package/dist/src/Time.d.ts.map +1 -1
  56. package/dist/src/Time.js +95 -6
  57. package/dist/src/Type.d.ts +3056 -1539
  58. package/dist/src/Type.d.ts.map +1 -1
  59. package/dist/src/Type.js +2548 -584
  60. package/dist/src/WebSocket.d.ts +164 -13
  61. package/dist/src/WebSocket.d.ts.map +1 -1
  62. package/dist/src/WebSocket.js +133 -24
  63. package/dist/src/Worker.d.ts +90 -8
  64. package/dist/src/Worker.d.ts.map +1 -1
  65. package/dist/src/Worker.js +28 -2
  66. package/dist/src/index.d.ts +9 -8
  67. package/dist/src/index.d.ts.map +1 -1
  68. package/dist/src/index.js +5 -4
  69. package/dist/src/intl/_en.d.ts +24 -1
  70. package/dist/src/intl/_en.d.ts.map +1 -1
  71. package/dist/src/intl/_en.js +20 -0
  72. package/dist/src/intl/ar.d.ts +24 -1
  73. package/dist/src/intl/ar.d.ts.map +1 -1
  74. package/dist/src/intl/ar.js +20 -0
  75. package/dist/src/intl/bn.d.ts +24 -1
  76. package/dist/src/intl/bn.d.ts.map +1 -1
  77. package/dist/src/intl/bn.js +20 -0
  78. package/dist/src/intl/ca.d.ts +24 -1
  79. package/dist/src/intl/ca.d.ts.map +1 -1
  80. package/dist/src/intl/ca.js +20 -0
  81. package/dist/src/intl/cs.d.ts +24 -1
  82. package/dist/src/intl/cs.d.ts.map +1 -1
  83. package/dist/src/intl/cs.js +20 -0
  84. package/dist/src/intl/da.d.ts +24 -1
  85. package/dist/src/intl/da.d.ts.map +1 -1
  86. package/dist/src/intl/da.js +20 -0
  87. package/dist/src/intl/de.d.ts +24 -1
  88. package/dist/src/intl/de.d.ts.map +1 -1
  89. package/dist/src/intl/de.js +20 -0
  90. package/dist/src/intl/el.d.ts +24 -1
  91. package/dist/src/intl/el.d.ts.map +1 -1
  92. package/dist/src/intl/el.js +20 -0
  93. package/dist/src/intl/es.d.ts +24 -1
  94. package/dist/src/intl/es.d.ts.map +1 -1
  95. package/dist/src/intl/es.js +20 -0
  96. package/dist/src/intl/fa.d.ts +24 -1
  97. package/dist/src/intl/fa.d.ts.map +1 -1
  98. package/dist/src/intl/fa.js +20 -0
  99. package/dist/src/intl/fi.d.ts +24 -1
  100. package/dist/src/intl/fi.d.ts.map +1 -1
  101. package/dist/src/intl/fi.js +20 -0
  102. package/dist/src/intl/fil.d.ts +24 -1
  103. package/dist/src/intl/fil.d.ts.map +1 -1
  104. package/dist/src/intl/fil.js +20 -0
  105. package/dist/src/intl/fr.d.ts +24 -1
  106. package/dist/src/intl/fr.d.ts.map +1 -1
  107. package/dist/src/intl/fr.js +20 -0
  108. package/dist/src/intl/he.d.ts +24 -1
  109. package/dist/src/intl/he.d.ts.map +1 -1
  110. package/dist/src/intl/he.js +20 -0
  111. package/dist/src/intl/hi.d.ts +24 -1
  112. package/dist/src/intl/hi.d.ts.map +1 -1
  113. package/dist/src/intl/hi.js +20 -0
  114. package/dist/src/intl/hr.d.ts +24 -1
  115. package/dist/src/intl/hr.d.ts.map +1 -1
  116. package/dist/src/intl/hr.js +20 -0
  117. package/dist/src/intl/hu.d.ts +22 -1
  118. package/dist/src/intl/hu.d.ts.map +1 -1
  119. package/dist/src/intl/hu.js +18 -0
  120. package/dist/src/intl/id.d.ts +24 -1
  121. package/dist/src/intl/id.d.ts.map +1 -1
  122. package/dist/src/intl/id.js +20 -0
  123. package/dist/src/intl/it.d.ts +24 -1
  124. package/dist/src/intl/it.d.ts.map +1 -1
  125. package/dist/src/intl/it.js +20 -0
  126. package/dist/src/intl/ja.d.ts +24 -1
  127. package/dist/src/intl/ja.d.ts.map +1 -1
  128. package/dist/src/intl/ja.js +20 -0
  129. package/dist/src/intl/ko.d.ts +24 -1
  130. package/dist/src/intl/ko.d.ts.map +1 -1
  131. package/dist/src/intl/ko.js +20 -0
  132. package/dist/src/intl/ml.d.ts +24 -1
  133. package/dist/src/intl/ml.d.ts.map +1 -1
  134. package/dist/src/intl/ml.js +20 -0
  135. package/dist/src/intl/mr.d.ts +24 -1
  136. package/dist/src/intl/mr.d.ts.map +1 -1
  137. package/dist/src/intl/mr.js +20 -0
  138. package/dist/src/intl/ms.d.ts +24 -1
  139. package/dist/src/intl/ms.d.ts.map +1 -1
  140. package/dist/src/intl/ms.js +20 -0
  141. package/dist/src/intl/nb.d.ts +22 -1
  142. package/dist/src/intl/nb.d.ts.map +1 -1
  143. package/dist/src/intl/nb.js +18 -0
  144. package/dist/src/intl/nl.d.ts +24 -1
  145. package/dist/src/intl/nl.d.ts.map +1 -1
  146. package/dist/src/intl/nl.js +20 -0
  147. package/dist/src/intl/pa.d.ts +24 -1
  148. package/dist/src/intl/pa.d.ts.map +1 -1
  149. package/dist/src/intl/pa.js +20 -0
  150. package/dist/src/intl/pl.d.ts +23 -0
  151. package/dist/src/intl/pl.d.ts.map +1 -1
  152. package/dist/src/intl/pl.js +20 -0
  153. package/dist/src/intl/pt-BR.d.ts +24 -1
  154. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  155. package/dist/src/intl/pt-BR.js +20 -0
  156. package/dist/src/intl/pt.d.ts +24 -1
  157. package/dist/src/intl/pt.d.ts.map +1 -1
  158. package/dist/src/intl/pt.js +20 -0
  159. package/dist/src/intl/ro.d.ts +24 -1
  160. package/dist/src/intl/ro.d.ts.map +1 -1
  161. package/dist/src/intl/ro.js +20 -0
  162. package/dist/src/intl/sk.d.ts +24 -1
  163. package/dist/src/intl/sk.d.ts.map +1 -1
  164. package/dist/src/intl/sk.js +20 -0
  165. package/dist/src/intl/sl.d.ts +24 -1
  166. package/dist/src/intl/sl.d.ts.map +1 -1
  167. package/dist/src/intl/sl.js +20 -0
  168. package/dist/src/intl/sv.d.ts +24 -1
  169. package/dist/src/intl/sv.d.ts.map +1 -1
  170. package/dist/src/intl/sv.js +20 -0
  171. package/dist/src/intl/sw.d.ts +21 -0
  172. package/dist/src/intl/sw.d.ts.map +1 -1
  173. package/dist/src/intl/sw.js +18 -0
  174. package/dist/src/intl/ta.d.ts +24 -1
  175. package/dist/src/intl/ta.d.ts.map +1 -1
  176. package/dist/src/intl/ta.js +20 -0
  177. package/dist/src/intl/te.d.ts +24 -1
  178. package/dist/src/intl/te.d.ts.map +1 -1
  179. package/dist/src/intl/te.js +20 -0
  180. package/dist/src/intl/th.d.ts +24 -1
  181. package/dist/src/intl/th.d.ts.map +1 -1
  182. package/dist/src/intl/th.js +20 -0
  183. package/dist/src/intl/tr.d.ts +24 -1
  184. package/dist/src/intl/tr.d.ts.map +1 -1
  185. package/dist/src/intl/tr.js +20 -0
  186. package/dist/src/intl/uk.d.ts +80 -57
  187. package/dist/src/intl/uk.d.ts.map +1 -1
  188. package/dist/src/intl/uk.js +174 -149
  189. package/dist/src/intl/ur.d.ts +24 -1
  190. package/dist/src/intl/ur.d.ts.map +1 -1
  191. package/dist/src/intl/ur.js +20 -0
  192. package/dist/src/intl/vi.d.ts +24 -1
  193. package/dist/src/intl/vi.d.ts.map +1 -1
  194. package/dist/src/intl/vi.js +20 -0
  195. package/dist/src/intl/zh-CN.d.ts +24 -1
  196. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  197. package/dist/src/intl/zh-CN.js +20 -0
  198. package/dist/src/intl/zh-TW.d.ts +24 -1
  199. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  200. package/dist/src/intl/zh-TW.js +20 -0
  201. package/dist/src/local-first/Db.d.ts +52 -3
  202. package/dist/src/local-first/Db.d.ts.map +1 -1
  203. package/dist/src/local-first/Db.js +412 -137
  204. package/dist/src/local-first/Evolu.d.ts +336 -211
  205. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  206. package/dist/src/local-first/Evolu.js +102 -15
  207. package/dist/src/local-first/Owner.d.ts +13 -30
  208. package/dist/src/local-first/Owner.d.ts.map +1 -1
  209. package/dist/src/local-first/Owner.js +13 -30
  210. package/dist/src/local-first/Protocol.d.ts +95 -17
  211. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  212. package/dist/src/local-first/Protocol.js +119 -39
  213. package/dist/src/local-first/Query.d.ts +8 -15
  214. package/dist/src/local-first/Query.d.ts.map +1 -1
  215. package/dist/src/local-first/Schema.d.ts +345 -21
  216. package/dist/src/local-first/Schema.d.ts.map +1 -1
  217. package/dist/src/local-first/Schema.js +214 -17
  218. package/dist/src/local-first/Shared.d.ts +537 -22
  219. package/dist/src/local-first/Shared.d.ts.map +1 -1
  220. package/dist/src/local-first/Shared.js +1437 -234
  221. package/dist/src/local-first/Storage.d.ts +192 -14
  222. package/dist/src/local-first/Storage.d.ts.map +1 -1
  223. package/dist/src/local-first/Storage.js +82 -21
  224. package/dist/src/local-first/Timestamp.d.ts +392 -41
  225. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  226. package/dist/src/local-first/Timestamp.js +404 -82
  227. package/dist/src/local-first/index.d.ts +0 -1
  228. package/dist/src/local-first/index.d.ts.map +1 -1
  229. package/dist/src/local-first/index.js +0 -1
  230. package/package.json +1 -1
  231. package/src/Assert.test.ts +2 -5
  232. package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
  233. package/src/{Binary.ts → Bytes.ts} +652 -21
  234. package/src/Config.test.ts +668 -0
  235. package/src/Config.ts +410 -0
  236. package/src/Console.ts +62 -7
  237. package/src/Crypto.ts +76 -4
  238. package/src/Eq.test.ts +2 -3
  239. package/src/Error.test.ts +76 -3
  240. package/src/Error.ts +71 -0
  241. package/src/Fs.test.ts +105 -0
  242. package/src/Fs.ts +488 -0
  243. package/src/Identicon.ts +2 -2
  244. package/src/LeakDetector.ts +22 -3
  245. package/src/LockManager.ts +8 -0
  246. package/src/Number.test.ts +82 -18
  247. package/src/Number.ts +76 -8
  248. package/src/Object.test.ts +139 -10
  249. package/src/Object.ts +49 -0
  250. package/src/Platform.ts +50 -8
  251. package/src/Random.ts +25 -2
  252. package/src/Resource.test.ts +837 -0
  253. package/src/Resource.ts +235 -15
  254. package/src/Schedule.test.ts +50 -12
  255. package/src/Schedule.ts +24 -14
  256. package/src/Sqlite.ts +138 -18
  257. package/src/Task.test.ts +189 -8
  258. package/src/Task.ts +56 -17
  259. package/src/Test.ts +9 -0
  260. package/src/Time.test.ts +82 -11
  261. package/src/Time.ts +246 -24
  262. package/src/Type.test.ts +3994 -1119
  263. package/src/Type.ts +7258 -3842
  264. package/src/Types.test.ts +4 -14
  265. package/src/WebSocket.ts +313 -40
  266. package/src/Worker.ts +90 -8
  267. package/src/index.ts +18 -7
  268. package/src/intl/_en.ts +70 -0
  269. package/src/intl/ar.ts +71 -0
  270. package/src/intl/bn.ts +70 -0
  271. package/src/intl/ca.ts +70 -0
  272. package/src/intl/cs.ts +70 -0
  273. package/src/intl/da.ts +70 -0
  274. package/src/intl/de.ts +70 -0
  275. package/src/intl/el.ts +70 -0
  276. package/src/intl/es.ts +70 -0
  277. package/src/intl/fa.ts +70 -0
  278. package/src/intl/fi.ts +70 -0
  279. package/src/intl/fil.ts +70 -0
  280. package/src/intl/fr.ts +70 -0
  281. package/src/intl/he.ts +70 -0
  282. package/src/intl/hi.ts +70 -0
  283. package/src/intl/hr.ts +70 -0
  284. package/src/intl/hu.ts +69 -0
  285. package/src/intl/id.ts +70 -0
  286. package/src/intl/intl.test.ts +819 -1
  287. package/src/intl/it.ts +70 -0
  288. package/src/intl/ja.ts +70 -0
  289. package/src/intl/ko.ts +68 -0
  290. package/src/intl/ml.ts +70 -0
  291. package/src/intl/mr.ts +70 -0
  292. package/src/intl/ms.ts +71 -0
  293. package/src/intl/nb.ts +69 -0
  294. package/src/intl/nl.ts +70 -0
  295. package/src/intl/pa.ts +70 -0
  296. package/src/intl/pl.ts +63 -0
  297. package/src/intl/pt-BR.ts +70 -0
  298. package/src/intl/pt.ts +71 -0
  299. package/src/intl/ro.ts +70 -0
  300. package/src/intl/sk.ts +71 -0
  301. package/src/intl/sl.ts +70 -0
  302. package/src/intl/sv.ts +70 -0
  303. package/src/intl/sw.ts +62 -0
  304. package/src/intl/ta.ts +70 -0
  305. package/src/intl/te.ts +70 -0
  306. package/src/intl/th.ts +68 -0
  307. package/src/intl/tr.ts +70 -0
  308. package/src/intl/uk.ts +228 -155
  309. package/src/intl/ur.ts +70 -0
  310. package/src/intl/vi.ts +70 -0
  311. package/src/intl/zh-CN.ts +68 -0
  312. package/src/intl/zh-TW.ts +68 -0
  313. package/src/local-first/Db.ts +644 -339
  314. package/src/local-first/Evolu.test.ts +686 -21
  315. package/src/local-first/Evolu.ts +450 -228
  316. package/src/local-first/Owner.ts +13 -30
  317. package/src/local-first/Protocol.test.ts +618 -11
  318. package/src/local-first/Protocol.ts +197 -73
  319. package/src/local-first/Query.ts +8 -15
  320. package/src/local-first/Schema.test.ts +143 -0
  321. package/src/local-first/Schema.ts +374 -24
  322. package/src/local-first/Shared.test.ts +7731 -559
  323. package/src/local-first/Shared.ts +2036 -267
  324. package/src/local-first/Storage.ts +219 -33
  325. package/src/local-first/Timestamp.test.ts +344 -70
  326. package/src/local-first/Timestamp.ts +435 -119
  327. package/src/local-first/index.ts +0 -1
  328. package/dist/src/Binary.d.ts +0 -254
  329. package/dist/src/Binary.d.ts.map +0 -1
  330. package/dist/src/local-first/Error.d.ts +0 -12
  331. package/dist/src/local-first/Error.d.ts.map +0 -1
  332. package/dist/src/local-first/Error.js +0 -6
  333. package/dist/src/local-first/LocalAuth.d.ts +0 -150
  334. package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
  335. package/dist/src/local-first/LocalAuth.js +0 -179
  336. package/src/local-first/Error.ts +0 -17
  337. package/src/local-first/LocalAuth.ts +0 -457
package/dist/src/Type.js CHANGED
@@ -341,9 +341,9 @@
341
341
  * parameter type. If application code claims an accessor-backed object or an
342
342
  * object with excess properties is an Object Output, the assertion throws
343
343
  * because the application contract is broken. `orThrow` and `orNull` preserve
344
- * the assertion at their typed `Input` boundary, then apply {@link getOrThrow}
345
- * or {@link getOrNull} only to validation failures returned by the remaining
346
- * pipeline.
344
+ * the assertion at their typed `Input` boundary. After that boundary, `orThrow`
345
+ * throws returned validation errors with a formatted message and the original
346
+ * error as `cause`, while `orNull` maps them to `null`.
347
347
  *
348
348
  * Consequently, structural representation errors such as sparse Arrays,
349
349
  * accessors, and excess properties normally do not enter user-facing validation
@@ -454,44 +454,129 @@ import * as bip39 from "@scure/bip39";
454
454
  import { wordlist } from "@scure/bip39/wordlists/english.js";
455
455
  import { createMutableArray, } from "./Array.js";
456
456
  import { assert, assertNonNullable } from "./Assert.js";
457
+ import { eqData } from "./Eq.js";
457
458
  import { identity } from "./Function.js";
458
- import { createMutableRecord, getObjectKind, isPlainObject } from "./Object.js";
459
+ import { createMutableRecord, getObjectKind, isPlainObject, } from "./Object.js";
459
460
  import { hasNodeBuffer } from "./Platform.js";
460
- import { err, flatMapResult, getOk, getOrNull, getOrThrow, ok, trySync, } from "./Result.js";
461
+ import { err, flatMapResult, getOk, getOrNull, ok, trySync, } from "./Result.js";
461
462
  import { safelyStringifyUnknownValue } from "./String.js";
462
- import { instance, isInstance, } from "./Types.js";
463
+ import { isInstance, } from "./Types.js";
464
+ /**
465
+ * Converts an error from a {@link Type} into formatted issues with paths.
466
+ *
467
+ * Pass the Type that produced the error. This uses its nested and localized
468
+ * formatters without validating the input again. To retain every issue, decode
469
+ * with `{ errors: "all" }`; this function cannot recover errors omitted during
470
+ * validation. A root issue has an empty path.
471
+ *
472
+ * ### Example
473
+ *
474
+ * ```ts
475
+ * import {
476
+ * assertEqual,
477
+ * assertErr,
478
+ * IntFromString,
479
+ * object,
480
+ * typeErrorToIssues,
481
+ * } from "@evolu/common";
482
+ *
483
+ * const Settings = object({ port: IntFromString });
484
+ *
485
+ * const result = Settings.fromUnknown({ port: "http" }, { errors: "all" });
486
+ * assertErr(result);
487
+ *
488
+ * assertEqual(typeErrorToIssues(Settings, result.error), [
489
+ * {
490
+ * path: ["port"],
491
+ * message: 'The value "http" is not a decimal integer.',
492
+ * },
493
+ * ]);
494
+ * ```
495
+ *
496
+ * @group Core
497
+ */
498
+ export const typeErrorToIssues = (type, error) => {
499
+ const runtimeType = type;
500
+ return runtimeType[getRuntimeTypeIssuesSymbol](error, "all", []).map((issue) => ({
501
+ path: issue.path,
502
+ message: formatRuntimeTypeIssue(issue),
503
+ }));
504
+ };
463
505
  const outputValidationSymbol =
464
506
  /*#__PURE__*/ globalThis.Symbol();
465
507
  const getRuntimeTypeIssuesSymbol =
466
508
  /*#__PURE__*/ globalThis.Symbol();
467
- const singleRuntimeTypeIssue = (formatterName, error, defaultFormatter, path = []) => [
509
+ // Copying a short path in a loop is about twice as fast as spreading it.
510
+ const appendIssuePath = (path, key) => {
511
+ const issuePath = createMutableArray(path.length + 1);
512
+ for (let index = 0; index < path.length; index++) {
513
+ issuePath[index] = path[index];
514
+ }
515
+ issuePath[path.length] = key;
516
+ return issuePath;
517
+ };
518
+ const singleRuntimeTypeIssue = (formatterName, error, defaultFormatter, path) => [
468
519
  { name: formatterName, error, path, formatError: defaultFormatter },
469
520
  ];
470
- const prependRuntimeTypeIssuePath = (key, issues) => issues.map((issue) => ({
471
- ...issue,
472
- path: [key, ...issue.path],
473
- }));
474
- const createCollectionRuntimeTypeIssues = (name, issuesKind, defaultFormatter, getNestedType) => (error, mode) => {
521
+ const createCollectionRuntimeTypeIssues = (name, issuesKind, defaultFormatter, getNestedType) => (error, mode, path) => {
475
522
  const reason = error.reason;
476
523
  if (reason.kind !== issuesKind) {
477
- return singleRuntimeTypeIssue(name, error, defaultFormatter);
524
+ return singleRuntimeTypeIssue(name, error, defaultFormatter, path);
478
525
  }
479
526
  const allIssues = reason.issues;
480
- const issues = mode === "first" ? [allIssues[0]] : allIssues;
481
- return issues.flatMap((issue) => {
482
- const path = "key" in issue ? issue.key : issue.index;
483
- if (issue.error !== undefined) {
484
- return prependRuntimeTypeIssuePath(path, getNestedType(issue)[getRuntimeTypeIssuesSymbol](issue.error, mode));
527
+ const issueCount = mode === "first" ? 1 : allIssues.length;
528
+ const result = [];
529
+ for (let index = 0; index < issueCount; index++) {
530
+ const issue = allIssues[index];
531
+ const issuePath = appendIssuePath(path, "key" in issue ? issue.key : issue.index);
532
+ if (issue.error === undefined) {
533
+ result.push({
534
+ name,
535
+ error: mode === "first"
536
+ ? error
537
+ : {
538
+ type: name,
539
+ reason: { kind: issuesKind, issues: [issue] },
540
+ },
541
+ path: issuePath,
542
+ formatError: defaultFormatter,
543
+ });
544
+ continue;
485
545
  }
486
- return singleRuntimeTypeIssue(name, mode === "first"
487
- ? error
488
- : {
489
- type: name,
490
- reason: { kind: issuesKind, issues: [issue] },
491
- }, defaultFormatter, [path]);
492
- });
546
+ const nestedIssues = getNestedType(issue)[getRuntimeTypeIssuesSymbol](issue.error, mode, issuePath);
547
+ // Nested issues are a new array, so one visited issue returns them.
548
+ if (issueCount === 1)
549
+ return nestedIssues;
550
+ for (const nestedIssue of nestedIssues)
551
+ result.push(nestedIssue);
552
+ }
553
+ return result;
493
554
  };
494
555
  const formatDefaultRuntimeTypeIssue = (issue) => issue.formatError(issue.error);
556
+ const formatRuntimeTypeIssue = (issue, formatIssue = formatDefaultRuntimeTypeIssue) => {
557
+ let message = formatIssue(issue);
558
+ if (issue.alternatives === undefined)
559
+ return message;
560
+ // Each alternative issue starts a "- " line; continuation lines of a
561
+ // multi-line message are indented.
562
+ for (const { index, name, issues } of issue.alternatives) {
563
+ for (const alternativeIssue of issues) {
564
+ let path = "";
565
+ for (const key of alternativeIssue.path) {
566
+ path +=
567
+ typeof key === "string"
568
+ ? `[${JSON.stringify(key)}]`
569
+ : `[${globalThis.String(key)}]`;
570
+ }
571
+ const alternativeMessage = formatRuntimeTypeIssue(alternativeIssue, formatIssue);
572
+ // replaceAll is slow even without a match.
573
+ message += `\n- ${index}: ${name}${path}: ${alternativeMessage.includes("\n")
574
+ ? alternativeMessage.replaceAll("\n", "\n ")
575
+ : alternativeMessage}`;
576
+ }
577
+ }
578
+ return message;
579
+ };
495
580
  export function assertType(type, value) {
496
581
  if (type === undefined)
497
582
  return;
@@ -530,6 +615,11 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
530
615
  * that produced them. Different localized Type sets can coexist in separate
531
616
  * application or dependency-injection scopes.
532
617
  *
618
+ * A Union formatter supplies the summary of the failure. Retained member
619
+ * failures are appended using their own localized formatters, with member
620
+ * indexes, Type names, and paths identifying each alternative. The Union still
621
+ * produces one issue at its enclosing path.
622
+ *
533
623
  * Localization is scoped to the selected Types instead of a package-wide
534
624
  * translation registry. Static imports give bundlers an explicit dependency
535
625
  * graph, so unrelated Types, locales, and formatters can be removed. Bundling
@@ -670,17 +760,30 @@ const withFormatError = (source, formatIssue, localizedTypeBySource) => {
670
760
  const parent = source.parent
671
761
  ? withFormatError(source.parent, formatIssue, localizedTypeBySource)
672
762
  : null;
673
- const getTypeIssues = (error, mode) => source[getRuntimeTypeIssuesSymbol](error, mode).map((issue) => ({
763
+ const localizeIssue = (issue) => ({
674
764
  ...issue,
675
765
  formatError: () => formatIssue(issue),
676
- }));
677
- const derived = createTypeNode(source.name, parent, source.fromUnknown, source.is, source[outputValidationSymbol], source[fromSymbol], source[encoderSymbol].parent ?? source[encoderSymbol], getTypeIssues, undefined, formatIssue);
766
+ ...(issue.alternatives === undefined
767
+ ? {}
768
+ : {
769
+ alternatives: issue.alternatives.map((alternative) => ({
770
+ ...alternative,
771
+ issues: alternative.issues.map(localizeIssue),
772
+ })),
773
+ }),
774
+ });
775
+ const getTypeIssues = (error, mode, path) => source[getRuntimeTypeIssuesSymbol](error, mode, path).map(localizeIssue);
776
+ const derived = createTypeNode(source.name, parent, source.fromUnknown, source.is, source[outputValidationSymbol], source[fromSymbol], source[encoderSymbol].parent ?? source[encoderSymbol], getTypeIssues, {
777
+ formatIssue,
778
+ check: source[checkSymbol],
779
+ refinements: source[refinementsSymbol],
780
+ });
678
781
  // Localization changes error rendering, not conversion semantics. Preserve
679
782
  // specialized public input operations such as json's single-parse Json
680
783
  // boundary. Private operation chains on derived still support composition.
681
784
  globalThis.Object.assign(derived, {
682
785
  from: source.from,
683
- orThrow: source.orThrow,
786
+ orThrow: createRuntimeOrThrow(getTerminalRuntimeNode(source.from), derived.formatError),
684
787
  orNull: source.orNull,
685
788
  });
686
789
  localizedTypeBySource.set(source, derived);
@@ -712,14 +815,27 @@ const localizeTypeReflection = (value, formatIssue, localizedTypeBySource) => {
712
815
  return value;
713
816
  const localized = globalThis.Object.create(globalThis.Object.getPrototypeOf(value));
714
817
  for (const key of Reflect.ownKeys(value)) {
715
- localized[key] = localizeTypeReflection(value[key], formatIssue, localizedTypeBySource);
818
+ const property = value[key];
819
+ // Configured defaults are opaque data whose identity must be preserved.
820
+ localized[key] =
821
+ key === "value" && globalThis.Object.hasOwn(value, defaultPropertySymbol)
822
+ ? property
823
+ : localizeTypeReflection(property, formatIssue, localizedTypeBySource);
716
824
  }
717
825
  return localized;
718
826
  };
719
827
  export function createType(name, fromUnknownOrParent, fromParentOrFormatError, formatError) {
720
- return typeof fromUnknownOrParent === "function"
721
- ? createRootType(name, assertRefinementIdentity(fromUnknownOrParent), fromParentOrFormatError)
722
- : createChildType(name, fromUnknownOrParent, assertRefinementIdentity(fromParentOrFormatError), formatError);
828
+ if (typeof fromUnknownOrParent === "function") {
829
+ const fromUnknown = assertRefinementIdentity(fromUnknownOrParent);
830
+ return createRootType(name, fromUnknown, fromParentOrFormatError, {
831
+ // The asserted identity makes this an identity Type.
832
+ check: (value) => {
833
+ const result = fromUnknown(value);
834
+ return result.ok ? undefined : result.error;
835
+ },
836
+ });
837
+ }
838
+ return createChildType(name, fromUnknownOrParent, assertRefinementIdentity(fromParentOrFormatError), formatError);
723
839
  }
724
840
  const assertRefinementIdentity = (refinement) => (value) => {
725
841
  const result = refinement(value);
@@ -728,11 +844,68 @@ const assertRefinementIdentity = (refinement) => (value) => {
728
844
  }
729
845
  return result;
730
846
  };
731
- const createRootType = (name, fromUnknown, formatError, getTypeIssues) => {
847
+ /**
848
+ * Creates a root {@link Type} with a custom error for an existing validator.
849
+ *
850
+ * The source must use identity encoding. The new Type accepts its Output as
851
+ * Input and hides its parent boundaries. Validation and error collection are
852
+ * delegated automatically; the mapper receives the failure and original value.
853
+ * The formatter presents the mapped error as one issue.
854
+ *
855
+ * ### Example
856
+ *
857
+ * ```ts
858
+ * import {
859
+ * assertEqual,
860
+ * assertErr,
861
+ * createTypeWithError,
862
+ * Number,
863
+ * String,
864
+ * union,
865
+ * type TypeError,
866
+ * type UnionError,
867
+ * } from "@evolu/common";
868
+ *
869
+ * interface ValueError extends TypeError<"Value"> {
870
+ * readonly cause: UnionError;
871
+ * }
872
+ *
873
+ * const Value = createTypeWithError(
874
+ * "Value",
875
+ * union(String, Number),
876
+ * (cause): ValueError => ({ type: "Value", cause }),
877
+ * () => "Enter text or a number.",
878
+ * );
879
+ *
880
+ * const result = Value.fromUnknown(false, { errors: "all" });
881
+ * assertErr(result);
882
+ * assertEqual(result.error.cause.errors.length, 2);
883
+ * assertEqual(Value.formatError(result.error), "Enter text or a number.");
884
+ * ```
885
+ *
886
+ * @group Construction
887
+ */
888
+ export const createTypeWithError = (name, type, mapError, formatError) => {
889
+ const check = type[checkSymbol];
890
+ return createRootType(name, (value, options) => {
891
+ const result = type.fromUnknown(value, options);
892
+ return result.ok ? result : err(mapError(result.error, value));
893
+ }, formatError, {
894
+ check: check &&
895
+ ((value, options) => {
896
+ const error = check(value, options);
897
+ return error === undefined ? undefined : mapError(error, value);
898
+ }),
899
+ });
900
+ };
901
+ const createRootType = (name, fromUnknown, formatError, { getTypeIssues, isOutput, check, } = {}) => {
732
902
  const runtimeFormatError = formatError;
733
903
  const runtimeGetTypeIssues = getTypeIssues ??
734
- ((error) => singleRuntimeTypeIssue(error.type === "TypeOf" ? name : error.type, error, runtimeFormatError));
735
- const is = (value) => fromUnknown(value).ok;
904
+ ((error, _mode, path) => singleRuntimeTypeIssue(error.type === "TypeOf" ? name : error.type, error, runtimeFormatError, path));
905
+ const is = isOutput ??
906
+ (check
907
+ ? (value) => check(value, firstValidationOptions) === undefined
908
+ : (value) => fromUnknown(value).ok);
736
909
  const from = (value, options = firstValidationOptions) => {
737
910
  assertTypeOutput(name, is, fromUnknown, value, options);
738
911
  return ok(value);
@@ -746,7 +919,7 @@ const createRootType = (name, fromUnknown, formatError, getTypeIssues) => {
746
919
  return value;
747
920
  };
748
921
  return {
749
- ...instance("Type"),
922
+ "~evolu/instance": "Type",
750
923
  name,
751
924
  parent: null,
752
925
  fromUnknown,
@@ -756,32 +929,134 @@ const createRootType = (name, fromUnknown, formatError, getTypeIssues) => {
756
929
  to,
757
930
  orThrow,
758
931
  orNull: to,
759
- "~standard": createStandardSchemaProps(fromUnknown, runtimeGetTypeIssues, formatDefaultRuntimeTypeIssue),
932
+ "~standard": {
933
+ version: 1,
934
+ vendor: "evolu",
935
+ validate: (value) => {
936
+ const result = fromUnknown(value, allValidationOptions);
937
+ return result.ok
938
+ ? { value: result.value }
939
+ : {
940
+ issues: runtimeGetTypeIssues(result.error, "all", []).map((issue) => ({
941
+ message: formatRuntimeTypeIssue(issue),
942
+ path: issue.path,
943
+ })),
944
+ };
945
+ },
946
+ },
760
947
  [outputValidationSymbol]: fromUnknown,
761
948
  [fromSymbol]: ok,
762
949
  [encoderSymbol]: identity,
763
950
  [getRuntimeTypeIssuesSymbol]: runtimeGetTypeIssues,
951
+ [checkSymbol]: check,
952
+ [refinementsSymbol]: undefined,
953
+ [arrayTypeSymbol]: undefined,
954
+ [setTypeSymbol]: undefined,
764
955
  };
765
956
  };
766
957
  const createChildType = (name, parent, fromParent, formatOwnError,
767
958
  // Wrapped child errors can delegate their structured issues.
768
- getTypeIssuesOverride) => {
959
+ getTypeIssuesOverride,
960
+ // A refinement `fromParent` only checks the parent Output and returns
961
+ // `ok()`, so the child can return the parent Result unchanged.
962
+ isRefinement = false) => {
769
963
  const typeParent = parent;
770
964
  const defaultFormatter = formatOwnError;
771
965
  const getTypeIssues = getTypeIssuesOverride ??
772
966
  (formatOwnError
773
- ? (error, mode) => error.type === name
774
- ? singleRuntimeTypeIssue(name, error, defaultFormatter)
775
- : typeParent[getRuntimeTypeIssuesSymbol](error, mode)
967
+ ? (error, mode, path) => error.type === name
968
+ ? singleRuntimeTypeIssue(name, error, defaultFormatter, path)
969
+ : typeParent[getRuntimeTypeIssuesSymbol](error, mode, path)
776
970
  : typeParent[getRuntimeTypeIssuesSymbol]);
971
+ // Validators get only the value and no receiver (refinements are read into
972
+ // a local before a call), so default parameters, `arguments`, and `this`
973
+ // behave as in a direct call.
777
974
  const validate = fromParent;
778
- const mapFromParent = (result, options) => (result.ok ? validate(result.value, options) : result);
779
- const fromParentOperation = mapRuntimeOperations(typeParent[fromSymbol], (operation) => mapRuntimeResult(operation, mapFromParent));
975
+ // Returning the parent Result avoids allocating an equal Ok per layer.
976
+ const mapFromParent = isRefinement
977
+ ? (result) => {
978
+ if (!result.ok)
979
+ return result;
980
+ const refined = validate(result.value);
981
+ return refined.ok ? result : refined;
982
+ }
983
+ : (result) => result.ok ? validate(result.value) : result;
984
+ // Parent operations are read once, so validation skips repeated property
985
+ // lookups on heterogeneous Type nodes.
986
+ const parentFromUnknown = typeParent.fromUnknown;
987
+ const parentValidateOutput = typeParent[outputValidationSymbol];
988
+ const parentFrom = typeParent[fromSymbol];
989
+ const fromParentOperation = (value, options = firstValidationOptions) => mapFromParent(parentFrom(value, options));
990
+ if (parentFrom.parent) {
991
+ fromParentOperation.parent = mapRuntimeOperations(parentFrom.parent, (operation) => mapRuntimeResult(operation, mapFromParent));
992
+ }
780
993
  const from = createFromOperation(fromParentOperation);
781
- const fromUnknown = (value, options = firstValidationOptions) => mapFromParent(typeParent.fromUnknown(value, options), options);
782
- const validateOutput = (value, options = firstValidationOptions) => mapFromParent(typeParent[outputValidationSymbol](value, options), options);
994
+ // Refinement children form a chain whose refinements run in order after
995
+ // its base, the nearest ancestor that is not a refinement, so nested
996
+ // refinements need no call per level.
997
+ const parentRefinements = isRefinement
998
+ ? typeParent[refinementsSymbol]
999
+ : undefined;
1000
+ const refinements = parentRefinements
1001
+ ? [...parentRefinements, validate]
1002
+ : [validate];
1003
+ const refinementCount = refinements.length;
1004
+ let base = typeParent;
1005
+ if (isRefinement) {
1006
+ while (base[refinementsSymbol])
1007
+ base = base.parent;
1008
+ }
1009
+ const isBase = base.is;
1010
+ const is = (value) => {
1011
+ if (!isBase(value))
1012
+ return false;
1013
+ for (let index = 0; index < refinementCount; index++) {
1014
+ const refine = refinements[index];
1015
+ if (!refine(value).ok)
1016
+ return false;
1017
+ }
1018
+ return true;
1019
+ };
1020
+ const baseCheck = base[checkSymbol];
1021
+ // A child returns its parent Output unchanged, so a child of an identity
1022
+ // Type is one too.
1023
+ const check = baseCheck &&
1024
+ ((value, options) => {
1025
+ const error = baseCheck(value, options);
1026
+ if (error !== undefined)
1027
+ return error;
1028
+ for (let index = 0; index < refinementCount; index++) {
1029
+ const refine = refinements[index];
1030
+ const result = refine(value);
1031
+ if (!result.ok)
1032
+ return result.error;
1033
+ }
1034
+ return undefined;
1035
+ });
1036
+ const validateBase = base.fromUnknown;
1037
+ // Like nested refinement children, it returns the base Result or the first
1038
+ // refinement Err. A single refinement is faster with its direct call.
1039
+ const fromUnknown = refinementCount > 1
1040
+ ? (value, options = firstValidationOptions) => {
1041
+ const result = validateBase(value, options);
1042
+ if (!result.ok)
1043
+ return result;
1044
+ for (let index = 0; index < refinementCount; index++) {
1045
+ const refine = refinements[index];
1046
+ const refined = refine(result.value);
1047
+ if (!refined.ok)
1048
+ return refined;
1049
+ }
1050
+ return result;
1051
+ }
1052
+ : (value, options = firstValidationOptions) => mapFromParent(parentFromUnknown(value, options));
1053
+ // A root parent validates its Output with fromUnknown, and so does a child
1054
+ // of such a parent.
1055
+ const validateOutput = parentValidateOutput === parentFromUnknown
1056
+ ? fromUnknown
1057
+ : (value, options = firstValidationOptions) => mapFromParent(parentValidateOutput(value, options));
783
1058
  const to = identity;
784
- return createTypeNode(name, typeParent, fromUnknown, (value) => typeParent.is(value) && validate(value, firstValidationOptions).ok, validateOutput, from, to, getTypeIssues);
1059
+ return createTypeNode(name, typeParent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { check, refinements: isRefinement ? refinements : undefined });
785
1060
  };
786
1061
  export function transform(name, parent, output, { from: transformFrom, to: transformTo, }, formatOwnError) {
787
1062
  const typeParent = parent;
@@ -802,20 +1077,20 @@ export function transform(name, parent, output, { from: transformFrom, to: trans
802
1077
  return parentValue;
803
1078
  };
804
1079
  const defaultFormatter = formatOwnError;
805
- const getTypeIssues = (error, mode) => {
1080
+ const getTypeIssues = (error, mode, path) => {
806
1081
  if (error.type !== name) {
807
- return typeParent[getRuntimeTypeIssuesSymbol](error, mode);
1082
+ return typeParent[getRuntimeTypeIssuesSymbol](error, mode, path);
808
1083
  }
809
1084
  if ("outputError" in error) {
810
- return typeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
1085
+ return typeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode, path);
811
1086
  }
812
- return singleRuntimeTypeIssue(name, error, defaultFormatter);
1087
+ return singleRuntimeTypeIssue(name, error, defaultFormatter, path);
813
1088
  };
814
1089
  const validateOutput = (value, options) => {
815
1090
  const result = typeOutput[outputValidationSymbol](value, options);
816
1091
  return result.ok ? result : err({ type: name, outputError: result.error });
817
1092
  };
818
- return createTypeNode(name, typeParent, fromUnknown, typeOutput.is, validateOutput, from, to, getTypeIssues, { output: typeOutput });
1093
+ return createTypeNode(name, typeParent, fromUnknown, typeOutput.is, validateOutput, from, to, getTypeIssues, { additionalProperties: { output: typeOutput } });
819
1094
  }
820
1095
  const firstValidationOptions = { errors: "first" };
821
1096
  const allValidationOptions = { errors: "all" };
@@ -825,7 +1100,39 @@ const fromSymbol =
825
1100
  /*#__PURE__*/ globalThis.Symbol();
826
1101
  const templateLiteralSyntaxSymbol =
827
1102
  /*#__PURE__*/ globalThis.Symbol();
1103
+ // A Type node caches the Array and Set Types of itself as an element in these
1104
+ // own properties; a Type whose slot cannot be written, such as a frozen one,
1105
+ // uses the typeByElement WeakMaps. A WeakMap alone keeps each entry's value
1106
+ // alive through young-generation GCs, so every schema created at runtime would
1107
+ // be promoted.
1108
+ const arrayTypeSymbol = /*#__PURE__*/ globalThis.Symbol();
1109
+ const setTypeSymbol = /*#__PURE__*/ globalThis.Symbol();
1110
+ const checkSymbol =
1111
+ /*#__PURE__*/ globalThis.Symbol();
1112
+ const refinementsSymbol =
1113
+ /*#__PURE__*/ globalThis.Symbol();
1114
+ // The `fromUnknown` of an identity Type.
1115
+ const checkToFromUnknown = (check) => (value, options = firstValidationOptions) => {
1116
+ const error = check(value, options);
1117
+ return error === undefined ? ok(value) : err(error);
1118
+ };
1119
+ // The checks of identity Types, or `undefined` if any Type is not one.
1120
+ const getRuntimeChecks = (types) => {
1121
+ const checks = [];
1122
+ for (const type of types) {
1123
+ const check = type[checkSymbol];
1124
+ if (check === undefined)
1125
+ return undefined;
1126
+ checks.push(check);
1127
+ }
1128
+ return checks;
1129
+ };
828
1130
  const mapRuntimeResult = (operation, map) => (value, options = firstValidationOptions) => map(operation(value, options), options);
1131
+ const createRuntimeOrThrow = (fromInput, formatError) => mapRuntimeResult(fromInput, (result) => {
1132
+ if (result.ok)
1133
+ return result.value;
1134
+ throw new Error(formatError(result.error), { cause: result.error });
1135
+ });
829
1136
  // `map` must return a fresh operation because this function can attach `.parent`.
830
1137
  const mapRuntimeOperations = (operation, map) => {
831
1138
  const mapped = map(operation);
@@ -841,37 +1148,56 @@ const createFromOperation = (parent) => {
841
1148
  from.parent = parent;
842
1149
  return from;
843
1150
  };
844
- const createToOperation = (parent, own) => {
845
- const mapOwnToParent = (operation) => (value) => operation(own(value));
846
- const to = mapOwnToParent(parent);
847
- const toParent = mapRuntimeResult(own, identity);
848
- if (parent.parent) {
849
- toParent.parent = mapRuntimeOperations(parent.parent, mapOwnToParent);
850
- }
851
- to.parent = toParent;
852
- return to;
853
- };
1151
+ // Composite Types read these operations of their Types on first use, so
1152
+ // construction allocates no arrays and validation skips property lookups on
1153
+ // heterogeneous Type nodes.
1154
+ const getFromUnknown = (type) => type.fromUnknown;
1155
+ const getValidateOutput = (type) => type[outputValidationSymbol];
1156
+ const getIs = (type) => type.is;
854
1157
  function getTerminalRuntimeNode(node) {
855
1158
  while (node.parent != null)
856
1159
  node = node.parent;
857
1160
  return node;
858
1161
  }
859
- const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, ownTo, getTypeIssues, additionalProperties, formatIssue = formatDefaultRuntimeTypeIssue) => {
1162
+ const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, ownTo, getTypeIssues, { additionalProperties, formatIssue = formatDefaultRuntimeTypeIssue, check, refinements, } = {}) => {
860
1163
  const runtimeParent = parent;
861
- const to = runtimeParent
862
- ? createToOperation(runtimeParent[encoderSymbol], ownTo)
863
- : ownTo;
1164
+ const parentTo = runtimeParent?.[encoderSymbol];
1165
+ // Operations defined directly here share this call's closure context, so a
1166
+ // Type node does not allocate one context per helper.
1167
+ let to = ownTo;
1168
+ if (parentTo) {
1169
+ to = (value) => parentTo(ownTo(value));
1170
+ const toParent = (value, options = firstValidationOptions) => ownTo(value, options);
1171
+ if (parentTo.parent) {
1172
+ toParent.parent = mapRuntimeOperations(parentTo.parent, (operation) => (value) => operation(ownTo(value)));
1173
+ }
1174
+ to.parent = toParent;
1175
+ }
864
1176
  const runtimeFormatError = runtimeParent?.[getRuntimeTypeIssuesSymbol] === getTypeIssues
865
1177
  ? runtimeParent.formatError
866
- : (error) => formatIssue(getTypeIssues(error, "first")[0]);
867
- const typedFrom = addRuntimeAssertions(name, is, validateOutput, parent, from);
1178
+ : (error) => formatRuntimeTypeIssue(getTypeIssues(error, "first", [])[0], formatIssue);
1179
+ const typedFrom = (value, options = firstValidationOptions) => {
1180
+ assertTypeOutput(name, is, validateOutput, value, options);
1181
+ return from(value, options);
1182
+ };
1183
+ if (from.parent) {
1184
+ typedFrom.parent = addRuntimeAssertions(runtimeParent, from.parent);
1185
+ }
868
1186
  const fromInput = getTerminalRuntimeNode(typedFrom);
869
- const typedTo = mapRuntimeOperations(to, (operation) => (value) => {
1187
+ const typedTo = (value) => {
870
1188
  assertTypeOutput(name, is, validateOutput, value);
871
- return operation(value);
872
- });
1189
+ return to(value);
1190
+ };
1191
+ if (to.parent) {
1192
+ typedTo.parent = mapRuntimeOperations(to.parent, (operation) => (value) => {
1193
+ assertTypeOutput(name, is, validateOutput, value);
1194
+ return operation(value);
1195
+ });
1196
+ }
1197
+ // A spread-free literal gets a fixed shape; spreading in the middle made
1198
+ // every later property a slow generic definition.
873
1199
  const type = {
874
- ...instance("Type"),
1200
+ "~evolu/instance": "Type",
875
1201
  name,
876
1202
  parent,
877
1203
  fromUnknown,
@@ -879,41 +1205,55 @@ const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, own
879
1205
  is,
880
1206
  from: typedFrom,
881
1207
  to: typedTo,
882
- orThrow: mapRuntimeResult(fromInput, getOrThrow),
883
- orNull: mapRuntimeResult(fromInput, getOrNull),
884
- "~standard": createStandardSchemaProps(fromUnknown, getTypeIssues, formatIssue),
885
- ...additionalProperties,
1208
+ orThrow: (value, options = firstValidationOptions) => {
1209
+ const result = fromInput(value, options);
1210
+ if (result.ok)
1211
+ return result.value;
1212
+ throw new Error(runtimeFormatError(result.error), {
1213
+ cause: result.error,
1214
+ });
1215
+ },
1216
+ orNull: (value, options = firstValidationOptions) => getOrNull(fromInput(value, options)),
1217
+ "~standard": {
1218
+ version: 1,
1219
+ vendor: "evolu",
1220
+ validate: (value) => {
1221
+ const result = fromUnknown(value, allValidationOptions);
1222
+ return result.ok
1223
+ ? { value: result.value }
1224
+ : {
1225
+ issues: getTypeIssues(result.error, "all", []).map((issue) => ({
1226
+ message: formatRuntimeTypeIssue(issue, formatIssue),
1227
+ path: issue.path,
1228
+ })),
1229
+ };
1230
+ },
1231
+ },
886
1232
  [outputValidationSymbol]: validateOutput,
887
1233
  [fromSymbol]: from,
888
1234
  [encoderSymbol]: to,
889
1235
  [getRuntimeTypeIssuesSymbol]: getTypeIssues,
1236
+ [checkSymbol]: check,
1237
+ [refinementsSymbol]: refinements,
1238
+ [arrayTypeSymbol]: undefined,
1239
+ [setTypeSymbol]: undefined,
890
1240
  };
1241
+ if (additionalProperties !== undefined) {
1242
+ globalThis.Object.assign(type, additionalProperties);
1243
+ }
891
1244
  return type;
892
1245
  };
893
- const createStandardSchemaProps = (fromUnknown, getTypeIssues, formatIssue) => ({
894
- version: 1,
895
- vendor: "evolu",
896
- validate: (value) => {
897
- const result = fromUnknown(value, allValidationOptions);
898
- return result.ok
899
- ? { value: result.value }
900
- : {
901
- issues: getTypeIssues(result.error, "all").map((issue) => ({
902
- message: formatIssue(issue),
903
- path: issue.path,
904
- })),
905
- };
906
- },
907
- });
908
- const addRuntimeAssertions = (name, is, validateOutput, parent, operation) => {
1246
+ // Each `from.parent` suffix asserts the Output of the next Type toward the
1247
+ // root before running the remaining pipeline.
1248
+ const addRuntimeAssertions = (boundary, operation) => {
1249
+ const { name, is } = boundary;
1250
+ const validateOutput = boundary[outputValidationSymbol];
909
1251
  const asserted = (value, options = firstValidationOptions) => {
910
1252
  assertTypeOutput(name, is, validateOutput, value, options);
911
1253
  return operation(value, options);
912
1254
  };
913
1255
  if (operation.parent) {
914
- const typeParent = parent;
915
- const runtimeParent = typeParent;
916
- asserted.parent = addRuntimeAssertions(runtimeParent.name, runtimeParent.is, runtimeParent[outputValidationSymbol], runtimeParent.parent, operation.parent);
1256
+ asserted.parent = addRuntimeAssertions(boundary.parent, operation.parent);
917
1257
  }
918
1258
  return asserted;
919
1259
  };
@@ -922,18 +1262,26 @@ const addRuntimeAssertions = (name, is, validateOutput, parent, operation) => {
922
1262
  *
923
1263
  * @group Base
924
1264
  */
925
- export const Unknown = /*#__PURE__*/ createRootType("Unknown", ok, identity);
1265
+ export const Unknown = /*#__PURE__*/ createRootType("Unknown", ok, identity, { isOutput: () => true, check: () => undefined });
926
1266
  /**
927
1267
  * A {@link Type} rejecting every value.
928
1268
  *
929
1269
  * @group Base
930
1270
  */
931
- export const Never = /*#__PURE__*/ createRootType("Never", (value) => err({ type: "Never", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not valid for type Never.`);
1271
+ export const Never = /*#__PURE__*/ createRootType("Never", (value) => err({ type: "Never", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not valid for type Never.`, {
1272
+ isOutput: () => false,
1273
+ check: (value) => ({ type: "Never", value }),
1274
+ });
932
1275
  const createTypeOfType = (name) => {
933
1276
  const typeOf = name.toLowerCase();
934
1277
  return createRootType(name, (value) => typeof value === typeOf
935
1278
  ? ok(value)
936
- : err({ type: "TypeOf", expected: name, value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not a ${typeOf}.`);
1279
+ : err({ type: "TypeOf", expected: name, value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not a ${typeOf}.`, {
1280
+ isOutput: (value) => typeof value === typeOf,
1281
+ check: (value) => typeof value === typeOf
1282
+ ? undefined
1283
+ : { type: "TypeOf", expected: name, value },
1284
+ });
937
1285
  };
938
1286
  /**
939
1287
  * A JavaScript string {@link Type} without additional constraints.
@@ -1035,6 +1383,7 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
1035
1383
  * >();
1036
1384
  *
1037
1385
  * assertOk(Age.fromUnknown(122), 122);
1386
+ *
1038
1387
  * const invalid = Age.fromUnknown(200);
1039
1388
  * assertErr(invalid);
1040
1389
  * assertType(Data, invalid.error);
@@ -1060,6 +1409,46 @@ export const BigInt = /*#__PURE__*/ createTypeOfType("BigInt");
1060
1409
  * @group Base
1061
1410
  */
1062
1411
  export const Boolean = /*#__PURE__*/ createTypeOfType("Boolean");
1412
+ /**
1413
+ * Transforms a boolean spelled as text into a {@link Boolean}.
1414
+ *
1415
+ * This is useful for inputs that carry booleans as text, such as environment
1416
+ * variables, URL query parameters, and form fields. Exactly `true` and `false`
1417
+ * are accepted, the spellings JSON and JavaScript use, so a boolean has one
1418
+ * representation in every source.
1419
+ *
1420
+ * ### Example
1421
+ *
1422
+ * ```ts
1423
+ * import {
1424
+ * assertEqual,
1425
+ * assertErr,
1426
+ * assertOk,
1427
+ * BooleanFromString,
1428
+ * } from "@evolu/common";
1429
+ *
1430
+ * assertOk(BooleanFromString.fromUnknown("true"), true);
1431
+ * assertOk(BooleanFromString.fromUnknown("false"), false);
1432
+ * assertEqual(BooleanFromString.to(true), "true");
1433
+ *
1434
+ * const invalid = BooleanFromString.fromUnknown("yes");
1435
+ * assertErr(invalid, { type: "BooleanFromString", value: "yes" });
1436
+ * assertEqual(
1437
+ * BooleanFromString.formatError(invalid.error),
1438
+ * 'The value "yes" is not a boolean. Use true or false.',
1439
+ * );
1440
+ * ```
1441
+ *
1442
+ * @group Base
1443
+ */
1444
+ export const BooleanFromString = /*#__PURE__*/ transform("BooleanFromString", String, Boolean, {
1445
+ from: (value) => value === "true"
1446
+ ? ok(true)
1447
+ : value === "false"
1448
+ ? ok(false)
1449
+ : err({ type: "BooleanFromString", value }),
1450
+ to: (value) => (value ? "true" : "false"),
1451
+ }, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a boolean. Use true or false.`);
1063
1452
  /**
1064
1453
  * A JavaScript symbol {@link Type}.
1065
1454
  *
@@ -1085,18 +1474,25 @@ export const EvoluType = /*#__PURE__*/ createType("EvoluType", (value) => isInst
1085
1474
  : err({ type: "EvoluType", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not an Evolu Type.`);
1086
1475
  export function objectTag(name, outputType) {
1087
1476
  const formatError = (error) => `A value ${safelyStringifyUnknownValue(error.value)} does not have the expected object tag ${safelyStringifyUnknownValue(error.expected)}.`;
1477
+ const tag = `[object ${name}]`;
1088
1478
  if (outputType === undefined) {
1089
- return createRootType(name, (value) => hasObjectTag(value, name)
1479
+ return createRootType(name, (value) => hasObjectTag(value, tag)
1090
1480
  ? ok(value)
1091
- : err({ type: "ObjectTag", expected: name, value }), formatError);
1481
+ : err({ type: "ObjectTag", expected: name, value }), formatError, {
1482
+ isOutput: (value) => hasObjectTag(value, tag),
1483
+ check: (value) => hasObjectTag(value, tag)
1484
+ ? undefined
1485
+ : { type: "ObjectTag", expected: name, value },
1486
+ });
1092
1487
  }
1093
- return globalThis.Object.assign(createChildType("ObjectTag", outputType, (value) => hasObjectTag(value, name)
1488
+ return globalThis.Object.assign(createChildType("ObjectTag", outputType, (value) => hasObjectTag(value, tag)
1094
1489
  ? ok(value)
1095
1490
  : err({ type: "ObjectTag", expected: name, value }), formatError), { expected: name });
1096
1491
  }
1097
- const hasObjectTag = (value, expected) => value !== null &&
1492
+ // Takes the complete `[object Name]` tag, so hot calls build no string.
1493
+ const hasObjectTag = (value, tag) => value !== null &&
1098
1494
  (typeof value === "object" || typeof value === "function") &&
1099
- globalThis.Object.prototype.toString.call(value) === `[object ${expected}]`;
1495
+ globalThis.Object.prototype.toString.call(value) === tag;
1100
1496
  /**
1101
1497
  * A realm-neutral JavaScript Date {@link Type} for trusted values.
1102
1498
  *
@@ -1158,7 +1554,12 @@ export const instanceOf = (constructor) => {
1158
1554
  const constructorName = concreteConstructor.name;
1159
1555
  const is = (value) => globalThis.Function.prototype[globalThis.Symbol.hasInstance].call(concreteConstructor, value);
1160
1556
  const fromUnknown = (value) => is(value) ? ok(value) : err({ type: "InstanceOf", constructorName, value });
1161
- return globalThis.Object.assign(createRootType("InstanceOf", fromUnknown, (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not an instance of ${error.constructorName}.`), { constructor: concreteConstructor });
1557
+ return globalThis.Object.assign(createRootType("InstanceOf", fromUnknown, (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not an instance of ${error.constructorName}.`, {
1558
+ isOutput: is,
1559
+ check: (value) => is(value)
1560
+ ? undefined
1561
+ : { type: "InstanceOf", constructorName, value },
1562
+ }), { constructor: concreteConstructor });
1162
1563
  };
1163
1564
  /**
1164
1565
  * Literal {@link Type}.
@@ -1188,6 +1589,7 @@ export const instanceOf = (constructor) => {
1188
1589
  *
1189
1590
  * assertType<typeof Ready.Output, "ready">();
1190
1591
  * assertOk(Ready.fromUnknown("ready"), "ready");
1592
+ *
1191
1593
  * const invalid = Ready.fromUnknown("pending");
1192
1594
  * assertErr(invalid);
1193
1595
  * assertType(Data, invalid.error);
@@ -1202,9 +1604,6 @@ export const instanceOf = (constructor) => {
1202
1604
  */
1203
1605
  export const literal = (expected) => {
1204
1606
  const literalExpected = expected;
1205
- const validate = (value) => value === literalExpected
1206
- ? ok(value)
1207
- : err({ type: "Literal", expected: literalExpected, value });
1208
1607
  const parent = typeof literalExpected === "string"
1209
1608
  ? String
1210
1609
  : typeof literalExpected === "number"
@@ -1216,8 +1615,17 @@ export const literal = (expected) => {
1216
1615
  : null;
1217
1616
  const formatError = (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`;
1218
1617
  return globalThis.Object.assign(parent
1219
- ? createChildType("Literal", parent, validate, formatError)
1220
- : createRootType("Literal", validate, formatError), {
1618
+ ? createChildType("Literal", parent, (value) => value === literalExpected
1619
+ ? ok()
1620
+ : err({ type: "Literal", expected: literalExpected, value }), formatError, undefined, true)
1621
+ : createRootType("Literal", (value) => value === literalExpected
1622
+ ? ok(value)
1623
+ : err({ type: "Literal", expected: literalExpected, value }), formatError, {
1624
+ isOutput: (value) => value === literalExpected,
1625
+ check: (value) => value === literalExpected
1626
+ ? undefined
1627
+ : { type: "Literal", expected: literalExpected, value },
1628
+ }), {
1221
1629
  expected: literalExpected,
1222
1630
  [templateLiteralSyntaxSymbol]: true,
1223
1631
  });
@@ -1239,17 +1647,35 @@ export function union(...typesOrLiterals) {
1239
1647
  ? typeOrLiteral
1240
1648
  : literal(typeOrLiteral));
1241
1649
  const inputMembers = members.map(getTerminalRuntimeNode);
1242
- const inputFrom = createUnionValidation(inputMembers, (member, value, options) => member.fromUnknown(value, options));
1243
- const inputValidateOutput = createUnionValidation(inputMembers, (member, value, options) => member[outputValidationSymbol](value, options));
1244
- const defaultFormatter = (() => "A value does not match any allowed variant.");
1245
- const getTypeIssues = (error) => singleRuntimeTypeIssue("Union", error, defaultFormatter);
1246
- const input = createTypeNode("Union", null, inputFrom, (value) => inputMembers.some((member) => member.is(value)), inputValidateOutput, ok, identity, getTypeIssues);
1247
- const fromUnknown = createUnionValidation(members, (member, value, options) => member.fromUnknown(value, options));
1248
- const validateOutput = createUnionValidation(members, (member, value, options) => member[outputValidationSymbol](value, options));
1249
- const memberFromInputs = members.map((member) => getTerminalRuntimeNode(member[fromSymbol]));
1250
- const fromParent = createUnionValidation(members, (_member, value, options, index) => inputMembers[index].is(value)
1251
- ? memberFromInputs[index](value, options)
1252
- : undefined);
1650
+ const inputCheck = createUnionCheck(inputMembers);
1651
+ const inputFrom = inputCheck
1652
+ ? checkToFromUnknown(inputCheck)
1653
+ : createUnionValidation(inputMembers, getFromUnknown);
1654
+ const getTypeIssues = createUnionRuntimeTypeIssues(members);
1655
+ const input = createTypeNode("Union", null, inputFrom, createUnionIs(inputMembers), inputCheck
1656
+ ? inputFrom
1657
+ : createUnionValidation(inputMembers, getValidateOutput), ok, identity, createUnionRuntimeTypeIssues(inputMembers), { check: inputCheck });
1658
+ // Literal members accept exactly their `===` equal values, so a match skips
1659
+ // them.
1660
+ const isLiteralUnion = typesOrLiterals.every((typeOrLiteral) => typeOrLiteral === null || typeof typeOrLiteral !== "object");
1661
+ const isLiteral = (value) =>
1662
+ // oxlint-disable-next-line typescript/prefer-includes -- Like Literal validation, indexOf uses ===, so NaN never matches.
1663
+ typesOrLiterals.indexOf(value) !== -1;
1664
+ const checkMembers = createUnionCheck(members);
1665
+ const check = checkMembers && isLiteralUnion
1666
+ ? (value, options) => isLiteral(value) ? undefined : checkMembers(value, options)
1667
+ : checkMembers;
1668
+ const fromUnknown = check
1669
+ ? checkToFromUnknown(check)
1670
+ : createUnionValidation(members, getFromUnknown);
1671
+ const validateOutput = check
1672
+ ? fromUnknown
1673
+ : createUnionValidation(members, getValidateOutput);
1674
+ const fromParent = createUnionValidation(members, (member, index) => {
1675
+ const memberFromInput = getTerminalRuntimeNode(member[fromSymbol]);
1676
+ const inputIs = inputMembers[index].is;
1677
+ return (value, options) => inputIs(value) ? memberFromInput(value, options) : undefined;
1678
+ });
1253
1679
  const getOutputMember = (value) => members.find((member) => member.is(value));
1254
1680
  const from = createFromOperation(fromParent);
1255
1681
  const to = (value) => {
@@ -1257,25 +1683,99 @@ export function union(...typesOrLiterals) {
1257
1683
  assertNonNullable(member);
1258
1684
  return member[encoderSymbol](value);
1259
1685
  };
1260
- return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members, [templateLiteralSyntaxSymbol]: true });
1686
+ return createTypeNode("Union", input, fromUnknown, isLiteralUnion ? isLiteral : createUnionIs(members), validateOutput, from, to, getTypeIssues, {
1687
+ additionalProperties: { members, [templateLiteralSyntaxSymbol]: true },
1688
+ check,
1689
+ });
1261
1690
  }
1262
- const createUnionValidation = (members, validateMember) => (value, options = firstValidationOptions) => {
1263
- let errors;
1264
- for (let index = 0; index < members.length; index++) {
1265
- const result = validateMember(members[index], value, options, index);
1266
- if (result === undefined)
1267
- continue;
1268
- if (result.ok)
1269
- return result;
1270
- if (errors === undefined || options.errors === "all") {
1271
- (errors ??= []).push({ index, error: result.error });
1272
- }
1691
+ const createUnionRuntimeTypeIssues = (members) => (error, _mode, path) => [
1692
+ {
1693
+ name: "Union",
1694
+ error,
1695
+ path,
1696
+ formatError: () => "A value does not match any allowed variant.",
1697
+ // Alternative issue paths are relative to the Union value.
1698
+ alternatives: error.errors.map(({ index, error }) => ({
1699
+ index,
1700
+ name: members[index].name,
1701
+ issues: members[index][getRuntimeTypeIssuesSymbol](error, "all", []),
1702
+ })),
1703
+ },
1704
+ ];
1705
+ const createUnionIs = (members) => {
1706
+ // Two members, as in nullOr, are checked without a loop.
1707
+ if (members.length === 2) {
1708
+ const firstIs = members[0].is;
1709
+ const secondIs = members[1].is;
1710
+ return (value) => firstIs(value) || secondIs(value);
1273
1711
  }
1274
- assertNonNullable(errors);
1275
- return err({
1276
- type: "Union",
1277
- errors: errors,
1278
- });
1712
+ let memberIs;
1713
+ return (value) => {
1714
+ const isMembers = (memberIs ??= members.map(getIs));
1715
+ for (let index = 0; index < isMembers.length; index++) {
1716
+ if (isMembers[index](value))
1717
+ return true;
1718
+ }
1719
+ return false;
1720
+ };
1721
+ };
1722
+ // A Union of identity Types returns the Result of the first valid member, so
1723
+ // it is an identity Type too.
1724
+ const createUnionCheck = (members) => {
1725
+ const checks = getRuntimeChecks(members);
1726
+ if (checks === undefined)
1727
+ return undefined;
1728
+ return (value, options) => {
1729
+ // The same errors as createUnionValidation.
1730
+ let first;
1731
+ let errors;
1732
+ for (let index = 0; index < checks.length; index++) {
1733
+ const error = checks[index](value, options);
1734
+ if (error === undefined)
1735
+ return undefined;
1736
+ if (first === undefined) {
1737
+ first = { index, error };
1738
+ }
1739
+ else if (options.errors === "all") {
1740
+ (errors ??= [first]).push({ index, error });
1741
+ }
1742
+ }
1743
+ assertNonNullable(first);
1744
+ return {
1745
+ type: "Union",
1746
+ errors: (errors ?? [first]),
1747
+ };
1748
+ };
1749
+ };
1750
+ // A member validation returns `undefined` to skip its member.
1751
+ const createUnionValidation = (members, getMemberValidation) => {
1752
+ let memberValidations;
1753
+ return (value, options = firstValidationOptions) => {
1754
+ const validations = (memberValidations ??=
1755
+ members.map(getMemberValidation));
1756
+ // A later member often matches (nullOr), so the error array is created
1757
+ // only when a second member fails or the Union fails.
1758
+ let first;
1759
+ let errors;
1760
+ for (let index = 0; index < validations.length; index++) {
1761
+ const result = validations[index](value, options);
1762
+ if (result === undefined)
1763
+ continue;
1764
+ if (result.ok)
1765
+ return result;
1766
+ if (first === undefined) {
1767
+ first = { index, error: result.error };
1768
+ }
1769
+ else if (options.errors === "all") {
1770
+ (errors ??= [first]).push({ index, error: result.error });
1771
+ }
1772
+ }
1773
+ assertNonNullable(first);
1774
+ return err({
1775
+ type: "Union",
1776
+ errors: (errors ?? [first]),
1777
+ });
1778
+ };
1279
1779
  };
1280
1780
  /**
1281
1781
  * Union {@link Type} containing the supplied Type and `undefined`.
@@ -1398,6 +1898,7 @@ const isRuntimeUnionTypeNode = (type) => type.name === "Union" && "members" in t
1398
1898
  * assertOk(result, ["cs", "CZ"]);
1399
1899
  * const locale = result.value;
1400
1900
  * assertType<typeof locale, SupportedLocale>();
1901
+ *
1401
1902
  * const invalid = SupportedLocale.fromUnknown("cs/CZ");
1402
1903
  * assertErr(invalid);
1403
1904
  * assertType(Data, invalid.error);
@@ -1568,14 +2069,14 @@ const createTemplateLiteralParserType = (templateParts) => {
1568
2069
  return result;
1569
2070
  return err({ type: "TemplateLiteral", value: stringResult.value });
1570
2071
  };
1571
- const getTypeIssues = (error, mode) => {
2072
+ const getTypeIssues = (error, mode, path) => {
1572
2073
  if (error.type !== "TemplateLiteral") {
1573
- return String[getRuntimeTypeIssuesSymbol](error, mode);
2074
+ return String[getRuntimeTypeIssuesSymbol](error, mode, path);
1574
2075
  }
1575
2076
  if ("outputError" in error) {
1576
- return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
2077
+ return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode, path);
1577
2078
  }
1578
- return singleRuntimeTypeIssue("TemplateLiteral", error, ((error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`));
2079
+ return singleRuntimeTypeIssue("TemplateLiteral", error, ((error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`), path);
1579
2080
  };
1580
2081
  const canonicalStringFromUnknown = (value, options = firstValidationOptions) => {
1581
2082
  const stringResult = String.fromUnknown(value, options);
@@ -1584,7 +2085,7 @@ const createTemplateLiteralParserType = (templateParts) => {
1584
2085
  : stringResult;
1585
2086
  };
1586
2087
  const canonicalStringFrom = createFromOperation((value, options = firstValidationOptions) => canonicalizeString(value, options));
1587
- const stringType = createTypeNode("TemplateLiteral", String, canonicalStringFromUnknown, (value) => validateCanonicalString(value, firstValidationOptions).ok, validateCanonicalString, canonicalStringFrom, identity, getTypeIssues, reflection);
2088
+ const stringType = createTypeNode("TemplateLiteral", String, canonicalStringFromUnknown, (value) => validateCanonicalString(value, firstValidationOptions).ok, validateCanonicalString, canonicalStringFrom, identity, getTypeIssues, { additionalProperties: reflection });
1588
2089
  const fromUnknown = (value, options = firstValidationOptions) => {
1589
2090
  const stringResult = String.fromUnknown(value, options);
1590
2091
  if (!stringResult.ok)
@@ -1614,7 +2115,7 @@ const createTemplateLiteralParserType = (templateParts) => {
1614
2115
  };
1615
2116
  fromCanonicalString.parent = fromString;
1616
2117
  const from = createFromOperation(fromCanonicalString);
1617
- const type = createTypeNode("TemplateLiteral", stringType, fromUnknown, runtimeOutput.is, runtimeOutput[outputValidationSymbol], from, encodeCaptures, getTypeIssues, reflection);
2118
+ const type = createTypeNode("TemplateLiteral", stringType, fromUnknown, runtimeOutput.is, runtimeOutput[outputValidationSymbol], from, encodeCaptures, getTypeIssues, { additionalProperties: reflection });
1618
2119
  type.from.parent =
1619
2120
  fromCanonicalString;
1620
2121
  return type;
@@ -1756,9 +2257,7 @@ const getTemplateLiteralPartFraming = (part) => {
1756
2257
  return getTemplateLiteralPartFraming(type.parent);
1757
2258
  };
1758
2259
  export function brand(name, parent, validate, formatError) {
1759
- return createChildType(name, parent, validate
1760
- ? (value) => flatMapResult(validate(value), () => ok(value))
1761
- : ok, formatError);
2260
+ return createChildType(name, parent, validate ?? (() => ok()), formatError, undefined, true);
1762
2261
  }
1763
2262
  /**
1764
2263
  * Canonical ISO date-time {@link String}.
@@ -1785,6 +2284,7 @@ export function brand(name, parent, validate, formatError) {
1785
2284
  *
1786
2285
  * const value = "2023-01-01T12:00:00.000Z";
1787
2286
  * assertOk(DateIso.fromUnknown(value), value);
2287
+ *
1788
2288
  * const invalid = DateIso.fromUnknown("2023-01-01");
1789
2289
  * assertErr(invalid);
1790
2290
  * assertType(Data, invalid.error);
@@ -1837,126 +2337,790 @@ export const UInt64 = /*#__PURE__*/ brand("UInt64", BigInt, (value) => globalThi
1837
2337
  ? ok()
1838
2338
  : err({ type: "UInt64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid unsigned 64-bit integer (UInt64).`);
1839
2339
  /**
1840
- * Capitalized {@link Brand}.
1841
- *
1842
- * Requires the first character of a string to be uppercase.
2340
+ * Adds identifier validation in a naming convention to an existing string Type.
2341
+ *
2342
+ * An identifier is one or more ASCII words, each starting with a letter and
2343
+ * continuing with letters or digits. camelCase and PascalCase start every word
2344
+ * after the first with an uppercase letter, so `httpUrl` has two words and
2345
+ * `httpURL` has four. snake_case, kebab-case, and CONSTANT_CASE put exactly one
2346
+ * separator between words. Validation keeps the spelling and rejects empty
2347
+ * strings, whitespace, punctuation, and non-ASCII characters.
2348
+ *
2349
+ * Words never start with a digit. Identifier grammars in most languages forbid
2350
+ * a leading digit, and camelCase cannot mark a word boundary before one, so the
2351
+ * rule applies to every word and keeps every conversion exact. Join an
2352
+ * abbreviation such as `2FA` to the previous word, as in `MAX2FA_ATTEMPTS` and
2353
+ * `max2faAttempts`, or spell the number out, as in `TWO_FACTOR_SECRET`.
2354
+ *
2355
+ * Convert validated identifiers with functions such as
2356
+ * {@link camelCaseToSnakeCase}. Conversions preserve word boundaries, and
2357
+ * converting back restores the original spelling. They return only the
2358
+ * destination brand, dropping unrelated constraints such as input length.
1843
2359
  *
1844
2360
  * ### Example
1845
2361
  *
1846
2362
  * ```ts
1847
2363
  * import {
1848
- * assertEqual,
1849
2364
  * assertErr,
1850
2365
  * assertOk,
1851
- * assertType,
1852
- * Data,
2366
+ * identifier,
2367
+ * maxLength,
1853
2368
  * String,
1854
- * capitalized,
1855
- * type Brand,
1856
2369
  * } from "@evolu/common";
1857
2370
  *
1858
- * const CapitalizedString = capitalized(String);
1859
- * type CapitalizedString = typeof CapitalizedString.Output;
1860
- *
1861
- * assertType<CapitalizedString, string & Brand<"Capitalized">>();
2371
+ * const EnvName = identifier("CONSTANT_CASE")(maxLength(16)(String));
1862
2372
  *
1863
- * assertOk(CapitalizedString.fromUnknown("Evolu"), "Evolu");
1864
- * const invalid = CapitalizedString.fromUnknown("evolu");
1865
- * assertErr(invalid);
1866
- * assertType(Data, invalid.error);
1867
- * assertEqual(invalid.error, {
1868
- * type: "Capitalized",
1869
- * value: "evolu",
1870
- * });
2373
+ * assertOk(EnvName.fromUnknown("HTTP2_PORT"), "HTTP2_PORT");
2374
+ * assertErr(EnvName.fromUnknown("HTTP_2_PORT"));
1871
2375
  * ```
1872
2376
  *
1873
2377
  * @group String
1874
2378
  */
1875
- export const capitalized = (parent) => brand("Capitalized", parent, (value) => {
1876
- const [first = ""] = value;
1877
- return value === first.toUpperCase() + value.slice(first.length)
2379
+ export const identifier = (casing) => (parent) => {
2380
+ const name = identifierBrandByCasing[casing];
2381
+ const pattern = identifierPatternByCasing[casing];
2382
+ return brand(name, parent, (value) => pattern.test(value)
1878
2383
  ? ok()
1879
- : err({ type: "Capitalized", value });
1880
- }, (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`);
2384
+ : err({ type: name, value, casing }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a ${error.casing} identifier.`);
2385
+ };
2386
+ const identifierBrandByCasing = {
2387
+ camelCase: "CamelCaseIdentifier",
2388
+ PascalCase: "PascalCaseIdentifier",
2389
+ snake_case: "SnakeCaseIdentifier",
2390
+ "kebab-case": "KebabCaseIdentifier",
2391
+ CONSTANT_CASE: "ConstantCaseIdentifier",
2392
+ };
2393
+ const identifierPatternByCasing = {
2394
+ camelCase: /^[a-z][a-zA-Z0-9]*$/u,
2395
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/u,
2396
+ snake_case: /^[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*$/u,
2397
+ "kebab-case": /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*$/u,
2398
+ CONSTANT_CASE: /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)*$/u,
2399
+ };
1881
2400
  /**
1882
- * Capitalized {@link String}.
2401
+ * A validated camelCase identifier, such as `http2Port`.
2402
+ *
2403
+ * See {@link identifier} for the grammar and the conversion functions.
1883
2404
  *
1884
2405
  * @group String
1885
2406
  */
1886
- export const CapitalizedString = /*#__PURE__*/ capitalized(String);
2407
+ export const CamelCaseIdentifier =
2408
+ /*#__PURE__*/ identifier("camelCase")(String);
1887
2409
  /**
1888
- * String {@link Brand} without surrounding whitespace.
1889
- *
1890
- * ### Example
2410
+ * A validated PascalCase identifier, such as `Http2Port`.
1891
2411
  *
1892
- * ```ts
1893
- * import { assertOk, String, trimmed } from "@evolu/common";
2412
+ * See {@link identifier} for the grammar and the conversion functions.
1894
2413
  *
1895
- * const Trimmed = trimmed(String);
2414
+ * @group String
2415
+ */
2416
+ export const PascalCaseIdentifier =
2417
+ /*#__PURE__*/ identifier("PascalCase")(String);
2418
+ /**
2419
+ * A validated snake_case identifier, such as `http2_port`.
1896
2420
  *
1897
- * assertOk(Trimmed.fromUnknown("Evolu"), "Evolu");
1898
- * ```
2421
+ * See {@link identifier} for the grammar and the conversion functions.
1899
2422
  *
1900
2423
  * @group String
1901
2424
  */
1902
- export const trimmed = (parent) => brand("Trimmed", parent, (value) => value === value.trim()
1903
- ? ok()
1904
- : err({ type: "Trimmed", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be trimmed.`);
2425
+ export const SnakeCaseIdentifier =
2426
+ /*#__PURE__*/ identifier("snake_case")(String);
1905
2427
  /**
1906
- * A {@link String} without surrounding whitespace.
2428
+ * A validated kebab-case identifier, such as `http2-port`.
1907
2429
  *
1908
- * This Type validates that a string is already trimmed; it does not modify the
1909
- * value. Use {@link trim} to normalize a string. Because an empty string is
1910
- * valid, this Type is useful as an intermediate boundary for input controls
1911
- * that trim their values before domain validation. If an empty string is
1912
- * invalid, use {@link NonEmptyTrimmedString}.
2430
+ * See {@link identifier} for the grammar and the conversion functions.
1913
2431
  *
1914
2432
  * @group String
1915
2433
  */
1916
- export const TrimmedString = /*#__PURE__*/ trimmed(String);
2434
+ export const KebabCaseIdentifier =
2435
+ /*#__PURE__*/ identifier("kebab-case")(String);
1917
2436
  /**
1918
- * Trims a string and returns a {@link TrimmedString}.
2437
+ * A validated CONSTANT_CASE identifier, such as `HTTP2_PORT`.
1919
2438
  *
1920
- * ### Example
2439
+ * See {@link identifier} for the grammar and the conversion functions.
1921
2440
  *
1922
- * ```ts
1923
- * import { assertEqual, trim } from "@evolu/common";
2441
+ * @group String
2442
+ */
2443
+ export const ConstantCaseIdentifier =
2444
+ /*#__PURE__*/ identifier("CONSTANT_CASE")(String);
2445
+ /**
2446
+ * Converts a {@link CamelCaseIdentifier} to a {@link PascalCaseIdentifier}.
1924
2447
  *
1925
- * assertEqual(trim(" Evolu "), "Evolu");
1926
- * ```
2448
+ * Converts `http2Port` to `Http2Port`. Use {@link pascalCaseToCamelCase} to
2449
+ * recover the original spelling.
1927
2450
  *
1928
2451
  * @group String
1929
2452
  */
1930
- export const trim = (value) => value.trim();
2453
+ export const camelCaseToPascalCase = (value) => (value.charAt(0).toUpperCase() + value.slice(1));
1931
2454
  /**
1932
- * Minimum-length {@link Brand} for values whose `length` is at least `min`.
1933
- *
1934
- * ### Example
2455
+ * Converts a {@link CamelCaseIdentifier} to a {@link SnakeCaseIdentifier}.
1935
2456
  *
1936
- * ```ts
1937
- * import { assertOk, String, array, minLength } from "@evolu/common";
2457
+ * Converts `http2Port` to `http2_port`. Use {@link snakeCaseToCamelCase} to
2458
+ * recover the original spelling.
1938
2459
  *
1939
- * const AtLeastThreeCharacters = minLength(3)(String);
1940
- * const AtLeastTwoItems = minLength(2)(array(String));
2460
+ * @group String
2461
+ */
2462
+ export const camelCaseToSnakeCase = (value) => value.replaceAll(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`);
2463
+ /**
2464
+ * Converts a {@link CamelCaseIdentifier} to a {@link KebabCaseIdentifier}.
1941
2465
  *
1942
- * assertOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
1943
- * assertOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
1944
- * ```
2466
+ * Converts `http2Port` to `http2-port`. Use {@link kebabCaseToCamelCase} to
2467
+ * recover the original spelling.
1945
2468
  *
1946
2469
  * @group String
1947
- * @group Collection
1948
2470
  */
1949
- export const minLength = (min) => (parent) => {
1950
- const name = `MinLength${min}`;
1951
- return brand(name, parent, (value) => value.length >= min
1952
- ? ok()
1953
- : err({ type: name, value, min }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not meet the minimum length of ${error.min}.`);
1954
- };
2471
+ export const camelCaseToKebabCase = (value) => snakeCaseToKebabCase(camelCaseToSnakeCase(value));
1955
2472
  /**
1956
- * A non-empty {@link TrimmedString}.
2473
+ * Converts a {@link CamelCaseIdentifier} to a {@link ConstantCaseIdentifier}.
1957
2474
  *
1958
- * Use as the base Type for ordinary human-entered text, which should usually be
1959
- * trimmed, non-empty, and bounded. Add a domain-appropriate maximum with
2475
+ * Converts `http2Port` to `HTTP2_PORT`. Use {@link constantCaseToCamelCase} to
2476
+ * recover the original spelling.
2477
+ *
2478
+ * @group String
2479
+ */
2480
+ export const camelCaseToConstantCase = (value) => snakeCaseToConstantCase(camelCaseToSnakeCase(value));
2481
+ /**
2482
+ * Converts a {@link PascalCaseIdentifier} to a {@link CamelCaseIdentifier}.
2483
+ *
2484
+ * Converts `Http2Port` to `http2Port`. Use {@link camelCaseToPascalCase} to
2485
+ * recover the original spelling.
2486
+ *
2487
+ * @group String
2488
+ */
2489
+ export const pascalCaseToCamelCase = (value) => (value.charAt(0).toLowerCase() + value.slice(1));
2490
+ /**
2491
+ * Converts a {@link PascalCaseIdentifier} to a {@link SnakeCaseIdentifier}.
2492
+ *
2493
+ * Converts `Http2Port` to `http2_port`. Use {@link snakeCaseToPascalCase} to
2494
+ * recover the original spelling.
2495
+ *
2496
+ * @group String
2497
+ */
2498
+ export const pascalCaseToSnakeCase = (value) => camelCaseToSnakeCase(pascalCaseToCamelCase(value));
2499
+ /**
2500
+ * Converts a {@link PascalCaseIdentifier} to a {@link KebabCaseIdentifier}.
2501
+ *
2502
+ * Converts `Http2Port` to `http2-port`. Use {@link kebabCaseToPascalCase} to
2503
+ * recover the original spelling.
2504
+ *
2505
+ * @group String
2506
+ */
2507
+ export const pascalCaseToKebabCase = (value) => snakeCaseToKebabCase(pascalCaseToSnakeCase(value));
2508
+ /**
2509
+ * Converts a {@link PascalCaseIdentifier} to a {@link ConstantCaseIdentifier}.
2510
+ *
2511
+ * Converts `Http2Port` to `HTTP2_PORT`. Use {@link constantCaseToPascalCase} to
2512
+ * recover the original spelling.
2513
+ *
2514
+ * @group String
2515
+ */
2516
+ export const pascalCaseToConstantCase = (value) => snakeCaseToConstantCase(pascalCaseToSnakeCase(value));
2517
+ /**
2518
+ * Converts a {@link SnakeCaseIdentifier} to a {@link CamelCaseIdentifier}.
2519
+ *
2520
+ * Converts `http2_port` to `http2Port`. Use {@link camelCaseToSnakeCase} to
2521
+ * recover the original spelling.
2522
+ *
2523
+ * @group String
2524
+ */
2525
+ export const snakeCaseToCamelCase = (value) => value.replaceAll(/_[a-z]/gu, (word) => word.charAt(1).toUpperCase());
2526
+ /**
2527
+ * Converts a {@link SnakeCaseIdentifier} to a {@link PascalCaseIdentifier}.
2528
+ *
2529
+ * Converts `http2_port` to `Http2Port`. Use {@link pascalCaseToSnakeCase} to
2530
+ * recover the original spelling.
2531
+ *
2532
+ * @group String
2533
+ */
2534
+ export const snakeCaseToPascalCase = (value) => camelCaseToPascalCase(snakeCaseToCamelCase(value));
2535
+ /**
2536
+ * Converts a {@link SnakeCaseIdentifier} to a {@link KebabCaseIdentifier}.
2537
+ *
2538
+ * Converts `http2_port` to `http2-port`. Use {@link kebabCaseToSnakeCase} to
2539
+ * recover the original spelling.
2540
+ *
2541
+ * @group String
2542
+ */
2543
+ export const snakeCaseToKebabCase = (value) => value.replaceAll("_", "-");
2544
+ /**
2545
+ * Converts a {@link SnakeCaseIdentifier} to a {@link ConstantCaseIdentifier}.
2546
+ *
2547
+ * Converts `http2_port` to `HTTP2_PORT`. Use {@link constantCaseToSnakeCase} to
2548
+ * recover the original spelling.
2549
+ *
2550
+ * @group String
2551
+ */
2552
+ export const snakeCaseToConstantCase = (value) => value.toUpperCase();
2553
+ /**
2554
+ * Converts a {@link KebabCaseIdentifier} to a {@link CamelCaseIdentifier}.
2555
+ *
2556
+ * Converts `http2-port` to `http2Port`. Use {@link camelCaseToKebabCase} to
2557
+ * recover the original spelling.
2558
+ *
2559
+ * @group String
2560
+ */
2561
+ export const kebabCaseToCamelCase = (value) => snakeCaseToCamelCase(kebabCaseToSnakeCase(value));
2562
+ /**
2563
+ * Converts a {@link KebabCaseIdentifier} to a {@link PascalCaseIdentifier}.
2564
+ *
2565
+ * Converts `http2-port` to `Http2Port`. Use {@link pascalCaseToKebabCase} to
2566
+ * recover the original spelling.
2567
+ *
2568
+ * @group String
2569
+ */
2570
+ export const kebabCaseToPascalCase = (value) => camelCaseToPascalCase(kebabCaseToCamelCase(value));
2571
+ /**
2572
+ * Converts a {@link KebabCaseIdentifier} to a {@link SnakeCaseIdentifier}.
2573
+ *
2574
+ * Converts `http2-port` to `http2_port`. Use {@link snakeCaseToKebabCase} to
2575
+ * recover the original spelling.
2576
+ *
2577
+ * @group String
2578
+ */
2579
+ export const kebabCaseToSnakeCase = (value) => value.replaceAll("-", "_");
2580
+ /**
2581
+ * Converts a {@link KebabCaseIdentifier} to a {@link ConstantCaseIdentifier}.
2582
+ *
2583
+ * Converts `http2-port` to `HTTP2_PORT`. Use {@link constantCaseToKebabCase} to
2584
+ * recover the original spelling.
2585
+ *
2586
+ * @group String
2587
+ */
2588
+ export const kebabCaseToConstantCase = (value) => snakeCaseToConstantCase(kebabCaseToSnakeCase(value));
2589
+ /**
2590
+ * Converts a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier}.
2591
+ *
2592
+ * Converts `HTTP2_PORT` to `http2Port`. Use {@link camelCaseToConstantCase} to
2593
+ * recover the original spelling.
2594
+ *
2595
+ * @group String
2596
+ */
2597
+ export const constantCaseToCamelCase = (value) => snakeCaseToCamelCase(constantCaseToSnakeCase(value));
2598
+ /**
2599
+ * Decodes a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier} and
2600
+ * restores the original spelling when encoding.
2601
+ *
2602
+ * ### Example
2603
+ *
2604
+ * ```ts
2605
+ * import {
2606
+ * assertEqual,
2607
+ * assertOk,
2608
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
2609
+ * } from "@evolu/common";
2610
+ *
2611
+ * const Key = CamelCaseIdentifierFromConstantCaseIdentifier;
2612
+ * const result = Key.fromUnknown("HTTP2_PORT");
2613
+ * assertOk(result, "http2Port");
2614
+ * assertEqual(Key.to(result.value), "HTTP2_PORT");
2615
+ * ```
2616
+ *
2617
+ * @group String
2618
+ */
2619
+ export const CamelCaseIdentifierFromConstantCaseIdentifier =
2620
+ /*#__PURE__*/ transform("CamelCaseIdentifierFromConstantCaseIdentifier", ConstantCaseIdentifier, CamelCaseIdentifier, {
2621
+ from: (value) => ok(constantCaseToCamelCase(value)),
2622
+ to: camelCaseToConstantCase,
2623
+ });
2624
+ /**
2625
+ * Converts a {@link ConstantCaseIdentifier} to a {@link PascalCaseIdentifier}.
2626
+ *
2627
+ * Converts `HTTP2_PORT` to `Http2Port`. Use {@link pascalCaseToConstantCase} to
2628
+ * recover the original spelling.
2629
+ *
2630
+ * @group String
2631
+ */
2632
+ export const constantCaseToPascalCase = (value) => camelCaseToPascalCase(constantCaseToCamelCase(value));
2633
+ /**
2634
+ * Converts a {@link ConstantCaseIdentifier} to a {@link SnakeCaseIdentifier}.
2635
+ *
2636
+ * Converts `HTTP2_PORT` to `http2_port`. Use {@link snakeCaseToConstantCase} to
2637
+ * recover the original spelling.
2638
+ *
2639
+ * @group String
2640
+ */
2641
+ export const constantCaseToSnakeCase = (value) => value.toLowerCase();
2642
+ /**
2643
+ * Converts a {@link ConstantCaseIdentifier} to a {@link KebabCaseIdentifier}.
2644
+ *
2645
+ * Converts `HTTP2_PORT` to `http2-port`. Use {@link kebabCaseToConstantCase} to
2646
+ * recover the original spelling.
2647
+ *
2648
+ * @group String
2649
+ */
2650
+ export const constantCaseToKebabCase = (value) => snakeCaseToKebabCase(constantCaseToSnakeCase(value));
2651
+ /**
2652
+ * Adds capitalized text validation to an existing string Type.
2653
+ *
2654
+ * Narrows the output to TypeScript's `Capitalize<string>` while preserving the
2655
+ * parent Type's constraints. Validation leaves the text unchanged. Use
2656
+ * {@link capitalize} to change its casing.
2657
+ *
2658
+ * ### Example
2659
+ *
2660
+ * ```ts
2661
+ * import {
2662
+ * assertErr,
2663
+ * assertOk,
2664
+ * capitalized,
2665
+ * maxLength,
2666
+ * String,
2667
+ * } from "@evolu/common";
2668
+ *
2669
+ * const Label = capitalized(maxLength(50)(String));
2670
+ * assertOk(Label.fromUnknown("Hello world"), "Hello world");
2671
+ * assertErr(Label.fromUnknown("hello world"));
2672
+ * ```
2673
+ *
2674
+ * @group String
2675
+ */
2676
+ export const capitalized = (parent) => createType("Capitalized", parent, (value) => value === capitalize(value)
2677
+ ? ok(value)
2678
+ : err({ type: "Capitalized", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`);
2679
+ /**
2680
+ * Validates capitalized text as TypeScript's `Capitalize<string>`.
2681
+ *
2682
+ * The rest of the text can use any casing. Empty strings and text starting with
2683
+ * an uncased character, such as a digit or emoji, are valid. Use
2684
+ * {@link capitalize} to produce a capitalized value from any string.
2685
+ *
2686
+ * Capitalization applies to general text, including spaces and punctuation.
2687
+ * Both `hello` and `Hello` become `Hello`, so the original initial casing
2688
+ * cannot be recovered. Capitalization does not identify words or turn text into
2689
+ * an identifier.
2690
+ *
2691
+ * ### Example
2692
+ *
2693
+ * ```ts
2694
+ * import { assertErr, assertOk, CapitalizedString } from "@evolu/common";
2695
+ *
2696
+ * const text: CapitalizedString = "Hello world";
2697
+ * assertOk(CapitalizedString.fromUnknown(text), text);
2698
+ * assertErr(CapitalizedString.fromUnknown("hello world"));
2699
+ * assertOk(CapitalizedString.fromUnknown(""), "");
2700
+ * ```
2701
+ *
2702
+ * @group String
2703
+ */
2704
+ export const CapitalizedString = /*#__PURE__*/ capitalized(String);
2705
+ /**
2706
+ * Uppercases the first Unicode code point and returns a
2707
+ * {@link CapitalizedString}.
2708
+ *
2709
+ * Preserves the remainder of the string and leaves an empty string unchanged.
2710
+ * Uses JavaScript's default Unicode casing without locale-specific rules.
2711
+ * Changing case can change the length, so input brands are not retained.
2712
+ *
2713
+ * ### Example
2714
+ *
2715
+ * ```ts
2716
+ * import { assertEqual, assertType, capitalize } from "@evolu/common";
2717
+ *
2718
+ * const text = capitalize("hello world");
2719
+ * assertEqual(text, "Hello world");
2720
+ * assertType<typeof text, "Hello world">();
2721
+ * assertEqual(capitalize(text), text);
2722
+ * ```
2723
+ *
2724
+ * @group String
2725
+ */
2726
+ export const capitalize = (value) => {
2727
+ const [first = ""] = value;
2728
+ return (first.toUpperCase() + value.slice(first.length));
2729
+ };
2730
+ /**
2731
+ * Adds uncapitalized text validation to an existing string Type.
2732
+ *
2733
+ * Narrows the output to TypeScript's `Uncapitalize<string>` while preserving
2734
+ * the parent Type's constraints. Validation leaves the text unchanged. Use
2735
+ * {@link uncapitalize} to change its casing.
2736
+ *
2737
+ * ### Example
2738
+ *
2739
+ * ```ts
2740
+ * import {
2741
+ * assertErr,
2742
+ * assertOk,
2743
+ * uncapitalized,
2744
+ * maxLength,
2745
+ * String,
2746
+ * } from "@evolu/common";
2747
+ *
2748
+ * const Label = uncapitalized(maxLength(50)(String));
2749
+ * assertOk(Label.fromUnknown("hello WORLD"), "hello WORLD");
2750
+ * assertErr(Label.fromUnknown("Hello WORLD"));
2751
+ * ```
2752
+ *
2753
+ * @group String
2754
+ */
2755
+ export const uncapitalized = (parent) => createType("Uncapitalized", parent, (value) => value === uncapitalize(value)
2756
+ ? ok(value)
2757
+ : err({ type: "Uncapitalized", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must not start with an uppercase letter.`);
2758
+ /**
2759
+ * Validates uncapitalized text as TypeScript's `Uncapitalize<string>`.
2760
+ *
2761
+ * The rest of the text can use any casing. Empty strings and text starting with
2762
+ * an uncased character, such as a digit or emoji, are valid. Use
2763
+ * {@link uncapitalize} to produce an uncapitalized value from any string.
2764
+ *
2765
+ * ### Example
2766
+ *
2767
+ * ```ts
2768
+ * import { assertErr, assertOk, UncapitalizedString } from "@evolu/common";
2769
+ *
2770
+ * const text: UncapitalizedString = "hello WORLD";
2771
+ * assertOk(UncapitalizedString.fromUnknown(text), text);
2772
+ * assertErr(UncapitalizedString.fromUnknown("Hello WORLD"));
2773
+ * assertOk(UncapitalizedString.fromUnknown(""), "");
2774
+ * ```
2775
+ *
2776
+ * @group String
2777
+ */
2778
+ export const UncapitalizedString = /*#__PURE__*/ uncapitalized(String);
2779
+ /**
2780
+ * Lowercases the first Unicode code point and returns a
2781
+ * {@link UncapitalizedString}.
2782
+ *
2783
+ * Preserves the remainder of the string and leaves an empty string unchanged.
2784
+ * Uses JavaScript's default Unicode casing without locale-specific rules.
2785
+ * Changing case can change the length, so input brands are not retained.
2786
+ *
2787
+ * ### Example
2788
+ *
2789
+ * ```ts
2790
+ * import { assertEqual, assertType, uncapitalize } from "@evolu/common";
2791
+ *
2792
+ * const text = uncapitalize("Hello WORLD");
2793
+ * assertEqual(text, "hello WORLD");
2794
+ * assertType<typeof text, "hello WORLD">();
2795
+ * assertEqual(uncapitalize(text), text);
2796
+ * ```
2797
+ *
2798
+ * @group String
2799
+ */
2800
+ export const uncapitalize = (value) => {
2801
+ const [first = ""] = value;
2802
+ return (first.toLowerCase() + value.slice(first.length));
2803
+ };
2804
+ /**
2805
+ * Adds uppercased text validation to an existing string Type.
2806
+ *
2807
+ * Narrows the output to TypeScript's `Uppercase<string>` while preserving the
2808
+ * parent Type's constraints. Validation leaves the text unchanged. Use
2809
+ * {@link uppercase} to change its casing.
2810
+ *
2811
+ * ### Example
2812
+ *
2813
+ * ```ts
2814
+ * import {
2815
+ * assertErr,
2816
+ * assertOk,
2817
+ * uppercased,
2818
+ * maxLength,
2819
+ * String,
2820
+ * } from "@evolu/common";
2821
+ *
2822
+ * const Label = uppercased(maxLength(50)(String));
2823
+ * assertOk(Label.fromUnknown("HELLO WORLD"), "HELLO WORLD");
2824
+ * assertErr(Label.fromUnknown("Hello world"));
2825
+ * ```
2826
+ *
2827
+ * @group String
2828
+ */
2829
+ export const uppercased = (parent) => createType("Uppercased", parent, (value) => value === uppercase(value)
2830
+ ? ok(value)
2831
+ : err({ type: "Uppercased", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be uppercased.`);
2832
+ /**
2833
+ * Validates uppercased text as TypeScript's `Uppercase<string>`.
2834
+ *
2835
+ * Checks the whole string using JavaScript's Unicode uppercase mapping. Empty
2836
+ * strings and uncased characters, such as digits and emoji, are valid. Use
2837
+ * {@link uppercase} to produce an uppercased value from any string.
2838
+ *
2839
+ * ### Example
2840
+ *
2841
+ * ```ts
2842
+ * import { assertErr, assertOk, UppercasedString } from "@evolu/common";
2843
+ *
2844
+ * const text: UppercasedString = "HELLO WORLD";
2845
+ * assertOk(UppercasedString.fromUnknown(text), text);
2846
+ * assertErr(UppercasedString.fromUnknown("Hello world"));
2847
+ * assertOk(UppercasedString.fromUnknown(""), "");
2848
+ * ```
2849
+ *
2850
+ * @group String
2851
+ */
2852
+ export const UppercasedString = /*#__PURE__*/ uppercased(String);
2853
+ /**
2854
+ * Uppercases the whole string and returns a {@link UppercasedString}.
2855
+ *
2856
+ * Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
2857
+ * without locale-specific rules. Changing case can change the length, so input
2858
+ * brands are not retained.
2859
+ *
2860
+ * ### Example
2861
+ *
2862
+ * ```ts
2863
+ * import { assertEqual, assertType, uppercase } from "@evolu/common";
2864
+ *
2865
+ * const text = uppercase("Hello world");
2866
+ * assertEqual(text, "HELLO WORLD");
2867
+ * assertType<typeof text, "HELLO WORLD">();
2868
+ * assertEqual(uppercase(text), text);
2869
+ * ```
2870
+ *
2871
+ * @group String
2872
+ */
2873
+ export const uppercase = (value) => value.toUpperCase();
2874
+ /**
2875
+ * Adds lowercased text validation to an existing string Type.
2876
+ *
2877
+ * Narrows the output to TypeScript's `Lowercase<string>` while preserving the
2878
+ * parent Type's constraints. Validation leaves the text unchanged. Use
2879
+ * {@link lowercase} to change its casing.
2880
+ *
2881
+ * ### Example
2882
+ *
2883
+ * ```ts
2884
+ * import {
2885
+ * assertErr,
2886
+ * assertOk,
2887
+ * lowercased,
2888
+ * maxLength,
2889
+ * String,
2890
+ * } from "@evolu/common";
2891
+ *
2892
+ * const Label = lowercased(maxLength(50)(String));
2893
+ * assertOk(Label.fromUnknown("hello world"), "hello world");
2894
+ * assertErr(Label.fromUnknown("Hello WORLD"));
2895
+ * ```
2896
+ *
2897
+ * @group String
2898
+ */
2899
+ export const lowercased = (parent) => createType("Lowercased", parent, (value) => value === lowercase(value)
2900
+ ? ok(value)
2901
+ : err({ type: "Lowercased", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be lowercased.`);
2902
+ /**
2903
+ * Validates lowercased text as TypeScript's `Lowercase<string>`.
2904
+ *
2905
+ * Checks the whole string using JavaScript's Unicode lowercase mapping. Empty
2906
+ * strings and uncased characters, such as digits and emoji, are valid. Use
2907
+ * {@link lowercase} to produce a lowercased value from any string.
2908
+ *
2909
+ * ### Example
2910
+ *
2911
+ * ```ts
2912
+ * import { assertErr, assertOk, LowercasedString } from "@evolu/common";
2913
+ *
2914
+ * const text: LowercasedString = "hello world";
2915
+ * assertOk(LowercasedString.fromUnknown(text), text);
2916
+ * assertErr(LowercasedString.fromUnknown("Hello WORLD"));
2917
+ * assertOk(LowercasedString.fromUnknown(""), "");
2918
+ * ```
2919
+ *
2920
+ * @group String
2921
+ */
2922
+ export const LowercasedString = /*#__PURE__*/ lowercased(String);
2923
+ /**
2924
+ * Lowercases the whole string and returns a {@link LowercasedString}.
2925
+ *
2926
+ * Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
2927
+ * without locale-specific rules. Changing case can change the length, so input
2928
+ * brands are not retained.
2929
+ *
2930
+ * ### Example
2931
+ *
2932
+ * ```ts
2933
+ * import { assertEqual, assertType, lowercase } from "@evolu/common";
2934
+ *
2935
+ * const text = lowercase("Hello WORLD");
2936
+ * assertEqual(text, "hello world");
2937
+ * assertType<typeof text, "hello world">();
2938
+ * assertEqual(lowercase(text), text);
2939
+ * ```
2940
+ *
2941
+ * @group String
2942
+ */
2943
+ export const lowercase = (value) => value.toLowerCase();
2944
+ /**
2945
+ * String {@link Brand} without surrounding whitespace.
2946
+ *
2947
+ * ### Example
2948
+ *
2949
+ * ```ts
2950
+ * import { assertOk, String, trimmed } from "@evolu/common";
2951
+ *
2952
+ * const Trimmed = trimmed(String);
2953
+ *
2954
+ * assertOk(Trimmed.fromUnknown("Evolu"), "Evolu");
2955
+ * ```
2956
+ *
2957
+ * @group String
2958
+ */
2959
+ export const trimmed = (parent) => brand("Trimmed", parent, (value) => value === value.trim()
2960
+ ? ok()
2961
+ : err({ type: "Trimmed", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be trimmed.`);
2962
+ /**
2963
+ * A {@link String} without surrounding whitespace.
2964
+ *
2965
+ * This Type validates that a string is already trimmed; it does not modify the
2966
+ * value. Use {@link trim} to normalize a string. Because an empty string is
2967
+ * valid, this Type is useful as an intermediate boundary for input controls
2968
+ * that trim their values before domain validation. If an empty string is
2969
+ * invalid, use {@link NonEmptyTrimmedString}.
2970
+ *
2971
+ * @group String
2972
+ */
2973
+ export const TrimmedString = /*#__PURE__*/ trimmed(String);
2974
+ /**
2975
+ * Trims a string and returns a {@link TrimmedString}.
2976
+ *
2977
+ * ### Example
2978
+ *
2979
+ * ```ts
2980
+ * import { assertEqual, trim } from "@evolu/common";
2981
+ *
2982
+ * assertEqual(trim(" Evolu "), "Evolu");
2983
+ * ```
2984
+ *
2985
+ * @group String
2986
+ */
2987
+ export const trim = (value) => value.trim();
2988
+ /**
2989
+ * String {@link Brand} requiring an exact, case-sensitive prefix.
2990
+ *
2991
+ * Validation preserves the complete string, including the prefix. An empty
2992
+ * prefix accepts every string allowed by the parent Type. The prefix must be
2993
+ * one concrete string literal so different prefixes have distinct brands.
2994
+ *
2995
+ * ### Example
2996
+ *
2997
+ * ```ts
2998
+ * import {
2999
+ * assertEqual,
3000
+ * assertErr,
3001
+ * assertOk,
3002
+ * assertType,
3003
+ * maxLength,
3004
+ * startsWith,
3005
+ * String,
3006
+ * type Brand,
3007
+ * } from "@evolu/common";
3008
+ *
3009
+ * const EnvName = startsWith("APP_")(maxLength(64)(String));
3010
+ *
3011
+ * const name = EnvName.fromUnknown("APP_PORT");
3012
+ * assertOk(name, "APP_PORT");
3013
+ * assertType<
3014
+ * typeof name.value,
3015
+ * string & Brand<"MaxLength64"> & Brand<"StartsWithAPP_">
3016
+ * >();
3017
+ *
3018
+ * assertEqual(EnvName.to(name.value), "APP_PORT");
3019
+ *
3020
+ * assertErr(EnvName.fromUnknown("app_PORT"));
3021
+ * assertErr(EnvName.fromUnknown("APP_" + "X".repeat(61)));
3022
+ * ```
3023
+ *
3024
+ * @group String
3025
+ */
3026
+ export const startsWith = (prefix) => {
3027
+ const name = `StartsWith${prefix}`;
3028
+ return (parent) => brand(name, parent, (value) => value.startsWith(prefix)
3029
+ ? ok()
3030
+ : err({
3031
+ type: name,
3032
+ value,
3033
+ prefix,
3034
+ }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must start with ${safelyStringifyUnknownValue(error.prefix)}.`);
3035
+ };
3036
+ /**
3037
+ * Decodes a prefixed string with another {@link Type} and restores the prefix
3038
+ * when encoding.
3039
+ *
3040
+ * Uses {@link startsWith} to validate an exact, case-sensitive prefix before
3041
+ * removing one occurrence. The wrapped Type validates and decodes the suffix;
3042
+ * its constraints apply to the suffix, and its Output is preserved. Encoding
3043
+ * prepends the prefix to the wrapped Type's canonical string representation. An
3044
+ * empty prefix leaves that representation unchanged.
3045
+ *
3046
+ * The prefix must be one concrete string literal. The wrapped Type must accept
3047
+ * a string Input and encode to strings; its Output can have another type, as
3048
+ * with {@link PortFromString}.
3049
+ *
3050
+ * ### Example
3051
+ *
3052
+ * ```ts
3053
+ * import {
3054
+ * assertEqual,
3055
+ * assertErr,
3056
+ * assertOk,
3057
+ * assertType,
3058
+ * ConstantCaseIdentifier,
3059
+ * prefixed,
3060
+ * PortFromString,
3061
+ * type Port,
3062
+ * } from "@evolu/common";
3063
+ *
3064
+ * const EnvName = prefixed("APP_")(ConstantCaseIdentifier);
3065
+ *
3066
+ * const name = EnvName.fromUnknown("APP_PORT");
3067
+ * assertOk(name, "PORT");
3068
+ * assertType<typeof name.value, ConstantCaseIdentifier>();
3069
+ *
3070
+ * assertEqual(EnvName.to(name.value), "APP_PORT");
3071
+ *
3072
+ * assertErr(EnvName.fromUnknown("OTHER_PORT"));
3073
+ * assertErr(EnvName.fromUnknown("APP_port"));
3074
+ *
3075
+ * const PortSetting = prefixed("port:")(PortFromString);
3076
+ *
3077
+ * const port = PortSetting.fromUnknown("port:04000");
3078
+ * assertOk(port, 4000);
3079
+ * assertType<typeof port.value, Port>();
3080
+ *
3081
+ * assertEqual(PortSetting.to(port.value), "port:4000");
3082
+ * ```
3083
+ *
3084
+ * @group String
3085
+ */
3086
+ export const prefixed = (prefix) => (type) => {
3087
+ // A literal prefix makes the generated names concrete and distinct.
3088
+ const source = startsWith(prefix)(String);
3089
+ return transform(`Prefixed${prefix}`, source, type, {
3090
+ // The Input guard permits any string; concatenation establishes the prefix brand.
3091
+ from: (value) => ok(value.slice(prefix.length)),
3092
+ to: (value) => `${prefix}${value}`,
3093
+ });
3094
+ };
3095
+ /**
3096
+ * Minimum-length {@link Brand} for values whose `length` is at least `min`.
3097
+ *
3098
+ * ### Example
3099
+ *
3100
+ * ```ts
3101
+ * import { assertOk, String, array, minLength } from "@evolu/common";
3102
+ *
3103
+ * const AtLeastThreeCharacters = minLength(3)(String);
3104
+ * const AtLeastTwoItems = minLength(2)(array(String));
3105
+ *
3106
+ * assertOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
3107
+ * assertOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
3108
+ * ```
3109
+ *
3110
+ * @group String
3111
+ * @group Collection
3112
+ */
3113
+ export const minLength = (min) => (parent) => {
3114
+ const name = `MinLength${min}`;
3115
+ return brand(name, parent, (value) => value.length >= min
3116
+ ? ok()
3117
+ : err({ type: name, value, min }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not meet the minimum length of ${error.min}.`);
3118
+ };
3119
+ /**
3120
+ * A non-empty {@link TrimmedString}.
3121
+ *
3122
+ * Use as the base Type for ordinary human-entered text, which should usually be
3123
+ * trimmed, non-empty, and bounded. Add a domain-appropriate maximum with
1960
3124
  * {@link maxLength}, or use {@link NonEmptyTrimmedString100} or
1961
3125
  * {@link NonEmptyTrimmedString1000}.
1962
3126
  *
@@ -2055,6 +3219,7 @@ export const length = (exact) => (parent) => {
2055
3219
  * assertType<UrlSafeString, string & Brand<"UrlSafeString">>();
2056
3220
  *
2057
3221
  * assertOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
3222
+ *
2058
3223
  * const invalid = UrlSafeString.fromUnknown("not safe");
2059
3224
  * assertErr(invalid);
2060
3225
  * assertType(Data, invalid.error);
@@ -2183,7 +3348,13 @@ export const uint8ArrayToBase64Url = (bytes) => uint8ArrayToBase64UrlString(byte
2183
3348
  */
2184
3349
  export const base64UrlToUint8Array = (value) => base64UrlStringToUint8Array(value);
2185
3350
  /**
2186
- * A non-empty URL-safe name containing at most 64 UTF-16 code units.
3351
+ * A non-empty file-system-safe and URL-safe token of at most 64 UTF-16 code
3352
+ * units.
3353
+ *
3354
+ * Evolu uses it for database file names, storage pool names, and log prefixes.
3355
+ * It accepts the {@link UrlSafeString} alphabet in any order, so it may start
3356
+ * with a digit, `-`, or `_`. It is not a language identifier; for
3357
+ * word-structured names, use {@link identifier}.
2187
3358
  *
2188
3359
  * @group String
2189
3360
  */
@@ -2643,6 +3814,7 @@ export const PositiveFiniteNumber = /*#__PURE__*/ positive(NonNegativeFiniteNumb
2643
3814
  * assertType<Int, number & Brand<"Int">>();
2644
3815
  *
2645
3816
  * assertOk(Int.fromUnknown(42), 42);
3817
+ *
2646
3818
  * const invalid = Int.fromUnknown(1.5);
2647
3819
  * assertErr(invalid);
2648
3820
  * assertType(Data, invalid.error);
@@ -2670,18 +3842,61 @@ export const NonNegativeInt = /*#__PURE__*/ nonNegative(Int);
2670
3842
  /**
2671
3843
  * Minimum {@link NonNegativeInt} value.
2672
3844
  *
2673
- * @group Number
2674
- */
2675
- export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
2676
- /**
2677
- * Positive {@link Int}.
3845
+ * @group Number
3846
+ */
3847
+ export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
3848
+ /**
3849
+ * Positive {@link Int}.
3850
+ *
3851
+ * Also satisfies {@link NonNegativeInt}, so it can be used wherever a
3852
+ * non-negative integer is required.
3853
+ *
3854
+ * @group Number
3855
+ */
3856
+ export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
3857
+ /**
3858
+ * Transforms a decimal integer string into an {@link Int}.
3859
+ *
3860
+ * This is useful for inputs that carry numbers as text, such as environment
3861
+ * variables, URL query parameters, and form fields. The string must consist of
3862
+ * an optional minus sign and digits; the {@link Int} constraint then rejects
3863
+ * values outside the safe integer range.
3864
+ *
3865
+ * ### Example
3866
+ *
3867
+ * ```ts
3868
+ * import {
3869
+ * assertEqual,
3870
+ * assertErr,
3871
+ * assertOk,
3872
+ * assertSame,
3873
+ * IntFromString,
3874
+ * } from "@evolu/common";
3875
+ *
3876
+ * assertOk(IntFromString.fromUnknown("4000"), 4000);
3877
+ * assertOk(IntFromString.fromUnknown("-1"), -1);
3878
+ * assertEqual(IntFromString.to(IntFromString.orThrow("42")), "42");
2678
3879
  *
2679
- * Also satisfies {@link NonNegativeInt}, so it can be used wherever a
2680
- * non-negative integer is required.
3880
+ * const negativeZero = IntFromString.orThrow("-0");
3881
+ * assertSame(negativeZero, -0);
3882
+ * assertEqual(IntFromString.to(negativeZero), "-0");
3883
+ *
3884
+ * const invalid = IntFromString.fromUnknown("4000.5");
3885
+ * assertErr(invalid, { type: "IntFromString", value: "4000.5" });
3886
+ * assertEqual(
3887
+ * IntFromString.formatError(invalid.error),
3888
+ * 'The value "4000.5" is not a decimal integer.',
3889
+ * );
3890
+ * ```
2681
3891
  *
2682
3892
  * @group Number
2683
3893
  */
2684
- export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
3894
+ export const IntFromString = /*#__PURE__*/ transform("IntFromString", String, Int, {
3895
+ from: (value) => /^-?\d+$/u.test(value)
3896
+ ? ok(globalThis.Number(value))
3897
+ : err({ type: "IntFromString", value }),
3898
+ to: (value) => globalThis.Object.is(value, -0) ? "-0" : globalThis.String(value),
3899
+ }, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a decimal integer.`);
2685
3900
  /**
2686
3901
  * Minimum {@link PositiveInt} value.
2687
3902
  *
@@ -2805,6 +4020,55 @@ export const lessThanOrEqualTo = (max) => (parent) => {
2805
4020
  ? ok()
2806
4021
  : err({ type: name, value, max }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be less than or equal to ${error.max}.`);
2807
4022
  };
4023
+ /**
4024
+ * A TCP or UDP port as an integer from zero through 65535, inclusive.
4025
+ *
4026
+ * When binding a server, zero requests an automatically assigned port. Use
4027
+ * {@link PortFromString} for configuration values supplied as text.
4028
+ *
4029
+ * ### Example
4030
+ *
4031
+ * ```ts
4032
+ * import { assertErr, assertOk, Port } from "@evolu/common";
4033
+ *
4034
+ * assertOk(Port.fromUnknown(0), 0);
4035
+ * assertOk(Port.fromUnknown(4000), 4000);
4036
+ * assertOk(Port.fromUnknown(65535), 65535);
4037
+ * assertErr(Port.fromUnknown(-1));
4038
+ * assertErr(Port.fromUnknown(65536));
4039
+ * assertErr(Port.fromUnknown(4000.5));
4040
+ * ```
4041
+ *
4042
+ * @group Number
4043
+ */
4044
+ export const Port = /*#__PURE__*/ brand("Port",
4045
+ /*#__PURE__*/ lessThanOrEqualTo(65535)(NonNegativeInt));
4046
+ /**
4047
+ * Parses a decimal integer string and validates it as a {@link Port}.
4048
+ *
4049
+ * Uses {@link IntFromString} for decimal parsing, including its rejection of
4050
+ * whitespace, plus signs, fractions, and exponent notation.
4051
+ *
4052
+ * ### Example
4053
+ *
4054
+ * ```ts
4055
+ * import {
4056
+ * assertEqual,
4057
+ * assertErr,
4058
+ * assertOk,
4059
+ * PortFromString,
4060
+ * } from "@evolu/common";
4061
+ *
4062
+ * assertOk(PortFromString.fromUnknown("0"), 0);
4063
+ * assertOk(PortFromString.fromUnknown("4000"), 4000);
4064
+ * assertErr(PortFromString.fromUnknown("65536"));
4065
+ * assertErr(PortFromString.fromUnknown("http"));
4066
+ * assertEqual(PortFromString.to(PortFromString.orThrow("04000")), "4000");
4067
+ * ```
4068
+ *
4069
+ * @group Number
4070
+ */
4071
+ export const PortFromString = /*#__PURE__*/ transform("PortFromString", IntFromString, Port, { from: ok, to: identity });
2808
4072
  /**
2809
4073
  * Finite {@link Number} from zero to one, inclusive.
2810
4074
  *
@@ -3033,6 +4297,7 @@ export const NegativeDecimalString = /*#__PURE__*/ negativeDecimalString(NonPosi
3033
4297
  * >();
3034
4298
  *
3035
4299
  * assertOk(Tenths.fromUnknown(0.3), 0.3);
4300
+ *
3036
4301
  * const invalid = Tenths.fromUnknown(0.31);
3037
4302
  * assertErr(invalid);
3038
4303
  * assertType(Data, invalid.error);
@@ -3152,6 +4417,7 @@ export const between = (min, max) => (parent) => {
3152
4417
  *
3153
4418
  * const UserId = brand("UserId", String);
3154
4419
  * const UserIds = array(UserId);
4420
+ *
3155
4421
  * const result = UserIds.from.parent(["ada", "grace"]);
3156
4422
  *
3157
4423
  * assertType<
@@ -3160,6 +4426,7 @@ export const between = (min, max) => (parent) => {
3160
4426
  * >();
3161
4427
  * assertOk(result, ["ada", "grace"]);
3162
4428
  * assertOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
4429
+ *
3163
4430
  * const invalid = UserIds.fromUnknown("ada");
3164
4431
  * assertErr(invalid);
3165
4432
  * assertType(Data, invalid.error);
@@ -3211,9 +4478,13 @@ const isArrayCollection = (value, isElement) => {
3211
4478
  };
3212
4479
  const arrayRuntimeConfig = {
3213
4480
  name: "Array",
4481
+ typeSymbol: arrayTypeSymbol,
3214
4482
  typeByElement: arrayTypeByElement,
3215
4483
  validate: validateArrayCollection,
3216
4484
  validateItems: (value, validateElement, options) => validateArrayItems(value, validateElement, options, false),
4485
+ check: (value, checkElement, options) => Array.isArray(value)
4486
+ ? checkIndexedArrayItems("Array", value, checkElement, options)
4487
+ : { type: "Array", reason: { kind: "NotArray", value } },
3217
4488
  encode: encodeArrayCollection,
3218
4489
  is: isArrayCollection,
3219
4490
  formatError: ((error) => {
@@ -3238,23 +4509,10 @@ const validateArrayItems = (value, validate, options, checkStructure) => validat
3238
4509
  // Encoding and Output membership stay feature-local because sharing those
3239
4510
  // shorter hot paths measurably increases standalone bundles.
3240
4511
  const validateIndexedArrayItems = (name, value, validate, options, checkStructure) => {
3241
- let issues;
4512
+ let issues = checkStructure
4513
+ ? getArrayExcessPropertyIssues(value, options)
4514
+ : undefined;
3242
4515
  let output;
3243
- if (checkStructure) {
3244
- for (const key of Reflect.ownKeys(value)) {
3245
- if (key === "length")
3246
- continue;
3247
- if (typeof key === "string") {
3248
- const index = globalThis.Number(key) >>> 0;
3249
- if (index < value.length && globalThis.String(index) === key) {
3250
- continue;
3251
- }
3252
- }
3253
- (issues ??= []).push({ kind: "ExcessProperty", key });
3254
- if (options.errors === "first")
3255
- break;
3256
- }
3257
- }
3258
4516
  for (let index = 0; (issues === undefined || options.errors === "all") && index < value.length; index++) {
3259
4517
  let item;
3260
4518
  if (checkStructure) {
@@ -3301,6 +4559,66 @@ const validateIndexedArrayItems = (name, value, validate, options, checkStructur
3301
4559
  },
3302
4560
  });
3303
4561
  };
4562
+ // Like validateIndexedArrayItems for identity elements, which leave the value
4563
+ // unchanged, so it returns only the error.
4564
+ const checkIndexedArrayItems = (name, value, check, options) => {
4565
+ let issues = getArrayExcessPropertyIssues(value, options);
4566
+ for (let index = 0; (issues === undefined || options.errors === "all") && index < value.length; index++) {
4567
+ const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, index);
4568
+ if (descriptor === undefined) {
4569
+ (issues ??= []).push({ kind: "Hole", index });
4570
+ if (options.errors === "first")
4571
+ break;
4572
+ continue;
4573
+ }
4574
+ if (!("value" in descriptor)) {
4575
+ (issues ??= []).push({ kind: "Accessor", index });
4576
+ if (options.errors === "first")
4577
+ break;
4578
+ continue;
4579
+ }
4580
+ const error = check(descriptor.value, options, index);
4581
+ if (error === undefined)
4582
+ continue;
4583
+ (issues ??= []).push({ kind: "Element", index, error });
4584
+ if (options.errors === "first")
4585
+ break;
4586
+ }
4587
+ return issues === undefined
4588
+ ? undefined
4589
+ : {
4590
+ type: name,
4591
+ reason: {
4592
+ kind: "Items",
4593
+ issues: issues,
4594
+ },
4595
+ };
4596
+ };
4597
+ // Own keys other than indexes and `length` are excess properties.
4598
+ const getArrayExcessPropertyIssues = (value, options) => {
4599
+ let issues;
4600
+ const keys = Reflect.ownKeys(value);
4601
+ for (let position = 0; position < keys.length; position++) {
4602
+ const key = keys[position];
4603
+ if (key === "length")
4604
+ continue;
4605
+ if (typeof key === "string") {
4606
+ // Ordinary arrays list their indexes first, in ascending order. Comparing
4607
+ // the key first reads `length` only for an index key.
4608
+ if (key === globalThis.String(position) && position < value.length) {
4609
+ continue;
4610
+ }
4611
+ const index = globalThis.Number(key) >>> 0;
4612
+ if (index < value.length && globalThis.String(index) === key) {
4613
+ continue;
4614
+ }
4615
+ }
4616
+ (issues ??= []).push({ kind: "ExcessProperty", key });
4617
+ if (options.errors === "first")
4618
+ break;
4619
+ }
4620
+ return issues;
4621
+ };
3304
4622
  const copyArrayPrefix = (value, endIndex) => {
3305
4623
  const output = createMutableArray(value.length);
3306
4624
  for (let index = 0; index < endIndex; index++) {
@@ -3334,11 +4652,23 @@ export const set = (element) => createHomogeneousCollectionType(element, setRunt
3334
4652
  // Array(String) +82/+63 B gzip, while Array(String)+Set(String) is 2501/2492 B
3335
4653
  // and Set adds only +303/+304 B once Array is present. See TreeShaking.test.ts.
3336
4654
  const createHomogeneousCollectionType = (typeElement, config) => {
3337
- const cached = config.typeByElement.get(typeElement);
4655
+ const slot = typeElement[config.typeSymbol];
4656
+ // A copy of a Type, spread or with the Type as its prototype, has the slot
4657
+ // of the original.
4658
+ const cached = slot !== undefined && slot.element === typeElement
4659
+ ? slot
4660
+ : config.typeByElement.get(typeElement);
3338
4661
  if (cached)
3339
4662
  return cached;
3340
- const fromUnknown = (value, options = firstValidationOptions) => config.validate(value, typeElement.fromUnknown, options);
3341
- const validateOutput = (value, options = firstValidationOptions) => config.validate(value, typeElement[outputValidationSymbol], options);
4663
+ const checkElement = typeElement[checkSymbol];
4664
+ const check = checkElement &&
4665
+ ((value, options) => config.check(value, checkElement, options));
4666
+ const fromUnknown = check
4667
+ ? checkToFromUnknown(check)
4668
+ : (value, options = firstValidationOptions) => config.validate(value, typeElement.fromUnknown, options);
4669
+ const validateOutput = check
4670
+ ? fromUnknown
4671
+ : (value, options = firstValidationOptions) => config.validate(value, typeElement[outputValidationSymbol], options);
3342
4672
  const parent = typeElement.parent
3343
4673
  ? createHomogeneousCollectionType(typeElement.parent, config)
3344
4674
  : null;
@@ -3353,16 +4683,20 @@ const createHomogeneousCollectionType = (typeElement, config) => {
3353
4683
  ? identity
3354
4684
  : (value) => config.encode(value, encodeElement);
3355
4685
  const getTypeIssues = createCollectionRuntimeTypeIssues(config.name, "Items", config.formatError, () => typeElement);
3356
- const type = createTypeNode(config.name, parent, fromUnknown, (value) => config.is(value, typeElement.is), validateOutput, from, to, getTypeIssues, { element: typeElement });
3357
- config.typeByElement.set(typeElement, type);
4686
+ const type = createTypeNode(config.name, parent, fromUnknown, (value) => config.is(value, typeElement.is), validateOutput, from, to, getTypeIssues, { additionalProperties: { element: typeElement }, check });
4687
+ // A frozen element Type keeps its Collection Type in the WeakMap.
4688
+ if (!Reflect.set(typeElement, config.typeSymbol, type)) {
4689
+ config.typeByElement.set(typeElement, type);
4690
+ }
3358
4691
  return type;
3359
4692
  };
3360
4693
  const setTypeByElement = /*#__PURE__*/ new WeakMap();
3361
4694
  const setRuntimeConfig = {
3362
4695
  name: "Set",
4696
+ typeSymbol: setTypeSymbol,
3363
4697
  typeByElement: setTypeByElement,
3364
4698
  validate: (value, validateElement, options) => {
3365
- if (!hasObjectTag(value, "Set")) {
4699
+ if (!hasObjectTag(value, "[object Set]")) {
3366
4700
  return err({
3367
4701
  type: "Set",
3368
4702
  reason: { kind: "NotSet", value },
@@ -3371,6 +4705,37 @@ const setRuntimeConfig = {
3371
4705
  return validateSetItems(value, validateElement, options, true);
3372
4706
  },
3373
4707
  validateItems: (value, validateElement, options) => validateSetItems(value, validateElement, options, false),
4708
+ check: (value, checkElement, options) => {
4709
+ if (!hasObjectTag(value, "[object Set]")) {
4710
+ return { type: "Set", reason: { kind: "NotSet", value } };
4711
+ }
4712
+ // The same issues as validateSetItems, without building an Output.
4713
+ let issues;
4714
+ for (const key of Reflect.ownKeys(value)) {
4715
+ (issues ??= []).push({ kind: "ExcessProperty", key });
4716
+ if (options.errors === "first")
4717
+ break;
4718
+ }
4719
+ let index = 0;
4720
+ for (const item of value) {
4721
+ if (issues !== undefined && options.errors === "first")
4722
+ break;
4723
+ const error = checkElement(item, options);
4724
+ if (error !== undefined) {
4725
+ (issues ??= []).push({ kind: "Element", index, error });
4726
+ }
4727
+ index++;
4728
+ }
4729
+ return issues === undefined
4730
+ ? undefined
4731
+ : {
4732
+ type: "Set",
4733
+ reason: {
4734
+ kind: "Items",
4735
+ issues: issues,
4736
+ },
4737
+ };
4738
+ },
3374
4739
  encode: (value, encodeElement) => {
3375
4740
  let changed = false;
3376
4741
  const output = new Set();
@@ -3383,7 +4748,7 @@ const setRuntimeConfig = {
3383
4748
  return changed ? output : value;
3384
4749
  },
3385
4750
  is: (value, isElement) => {
3386
- if (!hasObjectTag(value, "Set") ||
4751
+ if (!hasObjectTag(value, "[object Set]") ||
3387
4752
  Reflect.ownKeys(value).length !== 0) {
3388
4753
  return false;
3389
4754
  }
@@ -3474,7 +4839,7 @@ export const map = (key, value) => {
3474
4839
  if (cached)
3475
4840
  return cached;
3476
4841
  const validate = (input, validateKey, validateValue, options) => {
3477
- if (!hasObjectTag(input, "Map")) {
4842
+ if (!hasObjectTag(input, "[object Map]")) {
3478
4843
  return err({
3479
4844
  type: "Map",
3480
4845
  reason: { kind: "NotMap", value: input },
@@ -3482,8 +4847,90 @@ export const map = (key, value) => {
3482
4847
  }
3483
4848
  return validateMapEntries(input, validateKey, validateValue, options, true);
3484
4849
  };
3485
- const fromUnknown = (input, options = firstValidationOptions) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
3486
- const validateOutput = (input, options = firstValidationOptions) => validate(input, typeKey[outputValidationSymbol], typeValue[outputValidationSymbol], options);
4850
+ const checkKey = typeKey[checkSymbol];
4851
+ const checkValue = typeValue[checkSymbol];
4852
+ // Like validate for identity keys and values, which leave the Map unchanged,
4853
+ // so it returns only the error. A custom iterator can repeat a key, so
4854
+ // collisions are still detected.
4855
+ const check = checkKey &&
4856
+ checkValue &&
4857
+ ((input, options) => {
4858
+ if (!hasObjectTag(input, "[object Map]")) {
4859
+ return {
4860
+ type: "Map",
4861
+ reason: { kind: "NotMap", value: input },
4862
+ };
4863
+ }
4864
+ const entries = input;
4865
+ let issues;
4866
+ const entryByKey = new Map();
4867
+ for (const key of Reflect.ownKeys(entries)) {
4868
+ (issues ??= []).push({ kind: "ExcessProperty", key });
4869
+ if (options.errors === "first")
4870
+ break;
4871
+ }
4872
+ let index = 0;
4873
+ for (const [inputKey, inputValue] of entries) {
4874
+ if (issues !== undefined && options.errors === "first")
4875
+ break;
4876
+ const keyError = checkKey(inputKey, options);
4877
+ if (keyError !== undefined) {
4878
+ (issues ??= []).push({
4879
+ kind: "Key",
4880
+ index,
4881
+ key: inputKey,
4882
+ error: keyError,
4883
+ });
4884
+ if (options.errors === "first")
4885
+ break;
4886
+ }
4887
+ const valueError = checkValue(inputValue, options);
4888
+ if (valueError !== undefined) {
4889
+ (issues ??= []).push({
4890
+ kind: "Value",
4891
+ index,
4892
+ key: inputKey,
4893
+ error: valueError,
4894
+ });
4895
+ if (options.errors === "first")
4896
+ break;
4897
+ }
4898
+ if (keyError === undefined) {
4899
+ const previous = entryByKey.get(inputKey);
4900
+ if (previous === undefined) {
4901
+ entryByKey.set(inputKey, { index, key: inputKey });
4902
+ }
4903
+ else {
4904
+ (issues ??= []).push({
4905
+ kind: "Collision",
4906
+ index,
4907
+ key: inputKey,
4908
+ previousIndex: previous.index,
4909
+ previousKey: previous.key,
4910
+ outputKey: inputKey,
4911
+ });
4912
+ if (options.errors === "first")
4913
+ break;
4914
+ }
4915
+ }
4916
+ index++;
4917
+ }
4918
+ return issues === undefined
4919
+ ? undefined
4920
+ : {
4921
+ type: "Map",
4922
+ reason: {
4923
+ kind: "Entries",
4924
+ issues: issues,
4925
+ },
4926
+ };
4927
+ });
4928
+ const fromUnknown = check
4929
+ ? checkToFromUnknown(check)
4930
+ : (input, options = firstValidationOptions) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
4931
+ const validateOutput = check
4932
+ ? fromUnknown
4933
+ : (input, options = firstValidationOptions) => validate(input, typeKey[outputValidationSymbol], typeValue[outputValidationSymbol], options);
3487
4934
  const formatError = (error) => {
3488
4935
  if (error.reason.kind === "NotMap")
3489
4936
  return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Map.`;
@@ -3524,7 +4971,7 @@ export const map = (key, value) => {
3524
4971
  return changed ? output : input;
3525
4972
  };
3526
4973
  const is = (input) => {
3527
- if (!hasObjectTag(input, "Map")) {
4974
+ if (!hasObjectTag(input, "[object Map]")) {
3528
4975
  return false;
3529
4976
  }
3530
4977
  if (Reflect.ownKeys(input).length !== 0)
@@ -3535,26 +4982,36 @@ export const map = (key, value) => {
3535
4982
  }
3536
4983
  return true;
3537
4984
  };
3538
- const getTypeIssues = (error, mode) => {
4985
+ const getTypeIssues = (error, mode, path) => {
3539
4986
  const mapError = error;
3540
4987
  if (mapError.reason.kind !== "Entries") {
3541
- return singleRuntimeTypeIssue("Map", error, formatError);
4988
+ return singleRuntimeTypeIssue("Map", error, formatError, path);
3542
4989
  }
3543
4990
  const allIssues = mapError.reason.issues;
3544
4991
  const issues = mode === "first" ? [allIssues[0]] : allIssues;
3545
- return issues.flatMap((issue) => {
4992
+ const result = [];
4993
+ for (const issue of issues) {
3546
4994
  if (issue.kind === "Key" || issue.kind === "Value") {
3547
- return prependRuntimeTypeIssuePath(issue.index, prependRuntimeTypeIssuePath(issue.kind === "Key" ? "key" : "value", (issue.kind === "Key" ? typeKey : typeValue)[getRuntimeTypeIssuesSymbol](issue.error, mode)));
4995
+ for (const nestedIssue of (issue.kind === "Key" ? typeKey : typeValue)[getRuntimeTypeIssuesSymbol](issue.error, mode, appendIssuePath(appendIssuePath(path, issue.index), issue.kind === "Key" ? "key" : "value"))) {
4996
+ result.push(nestedIssue);
4997
+ }
4998
+ continue;
3548
4999
  }
3549
- return singleRuntimeTypeIssue("Map", mode === "first"
3550
- ? error
3551
- : {
3552
- type: "Map",
3553
- reason: { kind: "Entries", issues: [issue] },
3554
- }, formatError, [issue.kind === "ExcessProperty" ? issue.key : issue.index]);
3555
- });
5000
+ result.push({
5001
+ name: "Map",
5002
+ error: mode === "first"
5003
+ ? error
5004
+ : {
5005
+ type: "Map",
5006
+ reason: { kind: "Entries", issues: [issue] },
5007
+ },
5008
+ path: appendIssuePath(path, issue.kind === "ExcessProperty" ? issue.key : issue.index),
5009
+ formatError: formatError,
5010
+ });
5011
+ }
5012
+ return result;
3556
5013
  };
3557
- const type = createTypeNode("Map", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { key: typeKey, value: typeValue });
5014
+ const type = createTypeNode("Map", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { additionalProperties: { key: typeKey, value: typeValue }, check });
3558
5015
  if (typeByValue === undefined) {
3559
5016
  typeByValue = new WeakMap();
3560
5017
  mapTypeByValueByKey.set(typeKey, typeByValue);
@@ -3667,8 +5124,33 @@ const createTupleType = (typeElements) => {
3667
5124
  }
3668
5125
  return validateTupleItems(value, typeElements, validateElement, options, true);
3669
5126
  };
3670
- const fromUnknown = (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element.fromUnknown(item, elementOptions), options);
3671
- const validateOutput = (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element[outputValidationSymbol](item, elementOptions), options);
5127
+ const elementChecks = getRuntimeChecks(typeElements);
5128
+ const checkElement = elementChecks &&
5129
+ ((item, options, index) => elementChecks[index](item, options));
5130
+ // The same errors as validate, for identity elements.
5131
+ const check = checkElement &&
5132
+ ((value, options) => {
5133
+ if (!Array.isArray(value)) {
5134
+ return { type: "Tuple", reason: { kind: "NotArray", value } };
5135
+ }
5136
+ if (value.length !== expectedLength) {
5137
+ return {
5138
+ type: "Tuple",
5139
+ reason: {
5140
+ kind: "InvalidLength",
5141
+ expected: expectedLength,
5142
+ actual: value.length,
5143
+ },
5144
+ };
5145
+ }
5146
+ return checkIndexedArrayItems("Tuple", value, checkElement, options);
5147
+ });
5148
+ const fromUnknown = check
5149
+ ? checkToFromUnknown(check)
5150
+ : (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element.fromUnknown(item, elementOptions), options);
5151
+ const validateOutput = check
5152
+ ? fromUnknown
5153
+ : (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element[outputValidationSymbol](item, elementOptions), options);
3672
5154
  const formatError = (error) => {
3673
5155
  if (error.reason.kind === "NotArray")
3674
5156
  return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a tuple.`;
@@ -3727,29 +5209,29 @@ const createTupleType = (typeElements) => {
3727
5209
  return true;
3728
5210
  };
3729
5211
  const getTypeIssues = createCollectionRuntimeTypeIssues("Tuple", "Items", formatError, (issue) => typeElements[issue.index]);
3730
- return createTypeNode("Tuple", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { elements: typeElements });
5212
+ return createTypeNode("Tuple", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { additionalProperties: { elements: typeElements }, check });
3731
5213
  };
3732
5214
  const validateTupleItems = (value, elements, validate, options, checkStructure) => validateIndexedArrayItems("Tuple", value, (value, elementOptions, index) => validate(elements[index], value, elementOptions, index), options, checkStructure);
3733
5215
  /**
3734
- * Decimal digit from `"0"` to `"9"`.
5216
+ * Decimal integer string from `"0"` to `"9"`.
3735
5217
  *
3736
5218
  * @group String
3737
5219
  */
3738
5220
  export const Digit = /*#__PURE__*/ union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
3739
5221
  /**
3740
- * Decimal digit from `"1"` to `"9"`.
5222
+ * Decimal integer string from `"1"` to `"9"`.
3741
5223
  *
3742
5224
  * @group String
3743
5225
  */
3744
5226
  export const Digit1To9 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6", "7", "8", "9");
3745
5227
  /**
3746
- * Decimal string from `"1"` to `"6"`.
5228
+ * Decimal integer string from `"1"` to `"6"`.
3747
5229
  *
3748
5230
  * @group String
3749
5231
  */
3750
5232
  export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
3751
5233
  /**
3752
- * Decimal string from `"1"` to `"23"`.
5234
+ * Decimal integer string from `"1"` to `"23"`, without a leading zero.
3753
5235
  *
3754
5236
  * @group String
3755
5237
  */
@@ -3757,7 +5239,7 @@ export const Digit1To23 = /*#__PURE__*/ union(Digit1To9,
3757
5239
  /*#__PURE__*/ templateLiteral("1", Digit),
3758
5240
  /*#__PURE__*/ templateLiteral("2", /*#__PURE__*/ union("0", "1", "2", "3")));
3759
5241
  /**
3760
- * Decimal string from `"1"` to `"51"`.
5242
+ * Decimal integer string from `"1"` to `"51"`, without a leading zero.
3761
5243
  *
3762
5244
  * @group String
3763
5245
  */
@@ -3765,31 +5247,32 @@ export const Digit1To51 = /*#__PURE__*/ union(Digit1To9,
3765
5247
  /*#__PURE__*/ templateLiteral(/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
3766
5248
  /*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")));
3767
5249
  /**
3768
- * Decimal string from `"1"` to `"99"`.
5250
+ * Decimal integer string from `"1"` to `"99"`, without a leading zero.
3769
5251
  *
3770
5252
  * @group String
3771
5253
  */
3772
5254
  export const Digit1To99 = /*#__PURE__*/ union(Digit1To9,
3773
5255
  /*#__PURE__*/ templateLiteral(Digit1To9, Digit));
3774
5256
  /**
3775
- * Decimal string from `"1"` to `"59"`.
5257
+ * Decimal integer string from `"1"` to `"59"`, without a leading zero.
3776
5258
  *
3777
5259
  * @group String
3778
5260
  */
3779
5261
  export const Digit1To59 = /*#__PURE__*/ union(Digit1To9,
3780
5262
  /*#__PURE__*/ templateLiteral(
3781
5263
  /*#__PURE__*/ union("1", "2", "3", "4", "5"), Digit));
3782
- const createObjectRuntimeTypeIssues = (defaultFormatter, props, recordType) => (error, mode) => {
5264
+ const createObjectRuntimeTypeIssues = (props, recordType) => (error, mode, path) => {
3783
5265
  const objectError = error;
3784
5266
  if (objectError.reason.kind !== "Properties") {
3785
- return singleRuntimeTypeIssue("Object", error, defaultFormatter);
5267
+ return singleRuntimeTypeIssue("Object", error, formatObjectError, path);
3786
5268
  }
3787
5269
  const propertyErrors = objectError.reason.errors;
3788
5270
  const keys = Reflect.ownKeys(propertyErrors);
3789
- const firstKey = keys[0];
3790
- assertNonNullable(firstKey);
3791
- const keysToVisit = mode === "first" ? [firstKey] : keys;
3792
- return keysToVisit.flatMap((key) => {
5271
+ assertNonNullable(keys[0]);
5272
+ const visitCount = mode === "first" ? 1 : keys.length;
5273
+ const result = [];
5274
+ for (let index = 0; index < visitCount; index++) {
5275
+ const key = keys[index];
3793
5276
  const propertyError = propertyErrors[key];
3794
5277
  const property = typeof key === "string" &&
3795
5278
  props !== undefined &&
@@ -3809,51 +5292,86 @@ const createObjectRuntimeTypeIssues = (defaultFormatter, props, recordType) => (
3809
5292
  reason: { kind: "Properties", errors },
3810
5293
  };
3811
5294
  }
3812
- return singleRuntimeTypeIssue("Object", ownError, defaultFormatter, [
3813
- key,
3814
- ]);
5295
+ result.push({
5296
+ name: "Object",
5297
+ error: ownError,
5298
+ path: appendIssuePath(path, key),
5299
+ // The issue error has this first key, so its message needs no key
5300
+ // enumeration of the error record.
5301
+ formatError: () => formatObjectPropertyError(key, propertyError),
5302
+ });
5303
+ continue;
3815
5304
  }
3816
- if (property !== undefined) {
3817
- return prependRuntimeTypeIssuePath(key, objectPropertyToType(property)[getRuntimeTypeIssuesSymbol](propertyError, mode));
5305
+ // A record property error adds its own key.
5306
+ const nestedIssues = property === undefined
5307
+ ? recordType[getRuntimeTypeIssuesSymbol](propertyError, mode, path.slice())
5308
+ : objectPropertyToType(property)[getRuntimeTypeIssuesSymbol](propertyError, mode, appendIssuePath(path, key));
5309
+ // Nested issues are a new array, so one visited issue returns them.
5310
+ if (visitCount === 1)
5311
+ return nestedIssues;
5312
+ for (const issue of nestedIssues)
5313
+ result.push(issue);
5314
+ }
5315
+ return result;
5316
+ };
5317
+ const formatObjectError = (error) => {
5318
+ if (error.reason.kind !== "Properties")
5319
+ return formatPlainObjectRootError(error.reason);
5320
+ const key = Reflect.ownKeys(error.reason.errors).at(0);
5321
+ assertNonNullable(key);
5322
+ return formatObjectPropertyError(key, error.reason.errors[key]);
5323
+ };
5324
+ const formatObjectPropertyError = (key, propertyError) => {
5325
+ assertNonNullable(propertyError);
5326
+ if (propertyError.type === "ObjectPropertyAccess") {
5327
+ switch (propertyError.reason) {
5328
+ case "Accessor":
5329
+ return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
5330
+ case "NonEnumerable":
5331
+ return "An Object property must be enumerable. Make it enumerable or use a different Type.";
3818
5332
  }
3819
- return recordType[getRuntimeTypeIssuesSymbol](propertyError, mode);
3820
- });
5333
+ }
5334
+ if (propertyError.type === "ObjectMissingProperty")
5335
+ return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
5336
+ if (typeof key === "symbol")
5337
+ return "An Object property key must be a string. Remove the symbol property or use a different Type.";
5338
+ if (propertyError.type === "ObjectExcessProperty")
5339
+ return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
5340
+ return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
3821
5341
  };
3822
5342
  const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
3823
5343
  ? `A value ${safelyStringifyUnknownValue(reason.value)} is not an object.`
3824
5344
  : "The value is an object, but an Object Output must be a plain object or have a null prototype.";
3825
- /**
3826
- * A {@link Type} for readonly plain objects with unknown property values.
3827
- *
3828
- * `Object` is the runtime counterpart of a `Readonly<Record<string, unknown>>`
3829
- * data boundary. Its prototype rule uses the realm-neutral structural heuristic
3830
- * described by {@link isPlainObject}. A matching custom root prototype can be
3831
- * classified as plain; other custom prototypes and class instances are
3832
- * rejected. Every own property must have a string key and be an enumerable data
3833
- * property. Accessors, non-enumerable properties, and symbol properties are
3834
- * rejected without reading their values.
3835
- *
3836
- * Use {@link object} when property names are fixed, {@link record} when keys and
3837
- * values have their own Types, and {@link instanceOf} when an instance belongs
3838
- * to the domain.
3839
- *
3840
- * @group Base
3841
- */
3842
- const _Object = /*#__PURE__*/ createRootType("Object", (value, options = firstValidationOptions) => {
5345
+ // Ordinary objects list string keys before symbols, so without symbols the
5346
+ // names are the own keys, and cheaper than Reflect.ownKeys. A Proxy may list
5347
+ // symbols first, and Reflect.ownKeys keeps that order.
5348
+ const getOwnKeys = (value) => {
5349
+ const names = globalThis.Object.getOwnPropertyNames(value);
5350
+ return globalThis.Object.getOwnPropertySymbols(value).length === 0
5351
+ ? names
5352
+ : Reflect.ownKeys(value);
5353
+ };
5354
+ // An `is` walk in own-key order fails at the first symbol key, after checking
5355
+ // the names before it. Ordinary objects list symbols last, but a Proxy may
5356
+ // list one first. Walking names is faster than walking mixed own keys.
5357
+ const getNamesBeforeSymbol = (value) => {
5358
+ const names = [];
5359
+ for (const key of Reflect.ownKeys(value)) {
5360
+ if (typeof key !== "string")
5361
+ break;
5362
+ names.push(key);
5363
+ }
5364
+ return names;
5365
+ };
5366
+ const checkPlainObject = (value, options) => {
3843
5367
  if (value === null || typeof value !== "object") {
3844
- return err({
3845
- type: "Object",
3846
- reason: { kind: "NotObject", value },
3847
- });
5368
+ return { type: "Object", reason: { kind: "NotObject", value } };
3848
5369
  }
3849
5370
  if (!isPlainObject(value)) {
3850
- return err({
3851
- type: "Object",
3852
- reason: { kind: "UnexpectedPrototype", value },
3853
- });
5371
+ return { type: "Object", reason: { kind: "UnexpectedPrototype", value } };
3854
5372
  }
3855
5373
  let errors;
3856
- for (const key of Reflect.ownKeys(value)) {
5374
+ for (const key of getOwnKeys(value)) {
3857
5375
  let propertyError;
3858
5376
  if (typeof key !== "string") {
3859
5377
  propertyError = { type: "ObjectExcessProperty" };
@@ -3882,57 +5400,38 @@ const _Object = /*#__PURE__*/ createRootType("Object", (value, options = firstVa
3882
5400
  break;
3883
5401
  }
3884
5402
  return errors === undefined
3885
- ? ok(value)
3886
- : err({
5403
+ ? undefined
5404
+ : {
3887
5405
  type: "Object",
3888
5406
  reason: { kind: "Properties", errors },
3889
- });
3890
- }, (error) => {
3891
- if (error.reason.kind !== "Properties")
3892
- return formatPlainObjectRootError(error.reason);
3893
- const key = Reflect.ownKeys(error.reason.errors).at(0);
3894
- assertNonNullable(key);
3895
- const propertyError = error.reason.errors[key];
3896
- assertNonNullable(propertyError);
3897
- if (propertyError.type === "ObjectPropertyAccess") {
3898
- switch (propertyError.reason) {
3899
- case "Accessor":
3900
- return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
3901
- case "NonEnumerable":
3902
- return "An Object property must be enumerable. Make it enumerable or use a different Type.";
3903
- }
3904
- }
3905
- if (propertyError.type === "ObjectMissingProperty")
3906
- return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
3907
- if (typeof key === "symbol")
3908
- return "An Object property key must be a string. Remove the symbol property or use a different Type.";
3909
- if (propertyError.type === "ObjectExcessProperty")
3910
- return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
3911
- return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
3912
- },
3913
- /*#__PURE__*/ createObjectRuntimeTypeIssues(((error) => {
3914
- if (error.reason.kind !== "Properties")
3915
- return formatPlainObjectRootError(error.reason);
3916
- const key = Reflect.ownKeys(error.reason.errors).at(0);
3917
- assertNonNullable(key);
3918
- const propertyError = error.reason.errors[key];
3919
- assertNonNullable(propertyError);
3920
- if (propertyError.type === "ObjectPropertyAccess") {
3921
- switch (propertyError.reason) {
3922
- case "Accessor":
3923
- return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
3924
- case "NonEnumerable":
3925
- return "An Object property must be enumerable. Make it enumerable or use a different Type.";
3926
- }
3927
- }
3928
- if (propertyError.type === "ObjectMissingProperty")
3929
- return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
3930
- if (typeof key === "symbol")
3931
- return "An Object property key must be a string. Remove the symbol property or use a different Type.";
3932
- if (propertyError.type === "ObjectExcessProperty")
3933
- return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
3934
- return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
3935
- })));
5407
+ };
5408
+ };
5409
+ /**
5410
+ * A {@link Type} for readonly plain objects with unknown property values.
5411
+ *
5412
+ * `Object` is the runtime counterpart of a `Readonly<Record<string, unknown>>`
5413
+ * data boundary. Its prototype rule uses the realm-neutral structural heuristic
5414
+ * described by {@link isPlainObject}. A matching custom root prototype can be
5415
+ * classified as plain; other custom prototypes and class instances are
5416
+ * rejected. Every own property must have a string key and be an enumerable data
5417
+ * property. Accessors, non-enumerable properties, and symbol properties are
5418
+ * rejected without reading their values.
5419
+ *
5420
+ * Use {@link object} when property names are fixed, {@link record} when keys and
5421
+ * values have their own Types, and {@link instanceOf} when an instance belongs
5422
+ * to the domain.
5423
+ *
5424
+ * @group Base
5425
+ */
5426
+ const _Object = /*#__PURE__*/ createRootType("Object", (value, options = firstValidationOptions) => {
5427
+ const error = checkPlainObject(value, options);
5428
+ return error === undefined
5429
+ ? ok(value)
5430
+ : err(error);
5431
+ }, formatObjectError, {
5432
+ getTypeIssues: /*#__PURE__*/ createObjectRuntimeTypeIssues(),
5433
+ check: checkPlainObject,
5434
+ });
3936
5435
  // Avoid a local `Object` binding because Babel's CommonJS transform injects
3937
5436
  // `Object.defineProperty` before it is initialized:
3938
5437
  // https://github.com/babel/babel/issues/16943
@@ -4026,6 +5525,7 @@ export { _Object as Object };
4026
5525
  * const valueType = typeof value;
4027
5526
  *
4028
5527
  * assertEqual(valueType, "function");
5528
+ *
4029
5529
  * const called = trySync(
4030
5530
  * () => {
4031
5531
  * if (value !== undefined) value.toFixed(0);
@@ -4061,6 +5561,7 @@ export { _Object as Object };
4061
5561
  * const valueType = typeof value;
4062
5562
  *
4063
5563
  * assertEqual(valueType, "function");
5564
+ *
4064
5565
  * const called = trySync(
4065
5566
  * () => {
4066
5567
  * if (value !== undefined) value.toFixed(0);
@@ -4094,8 +5595,71 @@ export const record = (key, value) => {
4094
5595
  }
4095
5596
  return validateRecordEntries(input, validateKey, validateValue, options);
4096
5597
  };
4097
- const fromUnknown = (input, options = firstValidationOptions) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
4098
- const validateOutput = (input, options = firstValidationOptions) => validate(input, typeKey[outputValidationSymbol], typeValue[outputValidationSymbol], options);
5598
+ const checkKey = typeKey[checkSymbol];
5599
+ const checkValue = typeValue[checkSymbol];
5600
+ // The same errors as validate for identity keys and values. Own keys are
5601
+ // unique, so identity keys never collide.
5602
+ const check = checkKey &&
5603
+ checkValue &&
5604
+ ((input, options) => {
5605
+ if (input === null || typeof input !== "object") {
5606
+ return { type: "Record", reason: { kind: "NotRecord", value: input } };
5607
+ }
5608
+ if (!isPlainObject(input)) {
5609
+ return {
5610
+ type: "Record",
5611
+ reason: { kind: "NotPlainRecord", value: input },
5612
+ };
5613
+ }
5614
+ let issues;
5615
+ for (const inputKey of getOwnKeys(input)) {
5616
+ const keyError = checkKey(inputKey, options);
5617
+ if (keyError !== undefined) {
5618
+ (issues ??= []).push({ kind: "Key", key: inputKey, error: keyError });
5619
+ if (options.errors === "first")
5620
+ break;
5621
+ }
5622
+ const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, inputKey);
5623
+ assert(descriptor !== undefined, "Record property descriptor is missing.");
5624
+ if (!("value" in descriptor)) {
5625
+ (issues ??= []).push({ kind: "Accessor", key: inputKey });
5626
+ if (options.errors === "first")
5627
+ break;
5628
+ }
5629
+ else if (!descriptor.enumerable) {
5630
+ (issues ??= []).push({ kind: "NonEnumerable", key: inputKey });
5631
+ if (options.errors === "first")
5632
+ break;
5633
+ }
5634
+ else {
5635
+ const valueError = checkValue(descriptor.value, options);
5636
+ if (valueError !== undefined) {
5637
+ (issues ??= []).push({
5638
+ kind: "Value",
5639
+ key: inputKey,
5640
+ error: valueError,
5641
+ });
5642
+ if (options.errors === "first")
5643
+ break;
5644
+ }
5645
+ }
5646
+ }
5647
+ return issues === undefined
5648
+ ? undefined
5649
+ : {
5650
+ type: "Record",
5651
+ reason: {
5652
+ kind: "Entries",
5653
+ issues: issues,
5654
+ },
5655
+ };
5656
+ });
5657
+ const fromUnknown = check
5658
+ ? checkToFromUnknown(check)
5659
+ : (input, options = firstValidationOptions) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
5660
+ const validateOutput = check
5661
+ ? fromUnknown
5662
+ : (input, options = firstValidationOptions) => validate(input, typeKey[outputValidationSymbol], typeValue[outputValidationSymbol], options);
4099
5663
  const formatError = (error) => {
4100
5664
  if (error.reason.kind === "NotRecord")
4101
5665
  return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Record.`;
@@ -4149,8 +5713,16 @@ export const record = (key, value) => {
4149
5713
  return false;
4150
5714
  if (!isPlainObject(input))
4151
5715
  return false;
4152
- for (const key of Reflect.ownKeys(input)) {
4153
- if (typeof key !== "string" || !typeKey.is(key))
5716
+ // Separate name and symbol enumerations cost dictionary-mode inputs, such
5717
+ // as null-prototype records, about 10% over one Reflect.ownKeys, which is
5718
+ // much slower for ordinary objects.
5719
+ const hasSymbols = globalThis.Object.getOwnPropertySymbols(input).length !== 0;
5720
+ const names = hasSymbols
5721
+ ? getNamesBeforeSymbol(input)
5722
+ : globalThis.Object.getOwnPropertyNames(input);
5723
+ for (let index = 0; index < names.length; index++) {
5724
+ const key = names[index];
5725
+ if (!typeKey.is(key))
4154
5726
  return false;
4155
5727
  const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, key);
4156
5728
  if (descriptor === undefined ||
@@ -4161,17 +5733,17 @@ export const record = (key, value) => {
4161
5733
  if (!typeValue.is(descriptor.value))
4162
5734
  return false;
4163
5735
  }
4164
- return true;
5736
+ return !hasSymbols;
4165
5737
  };
4166
5738
  const getTypeIssues = createCollectionRuntimeTypeIssues("Record", "Entries", formatError, (issue) => (issue.kind === "Key" ? typeKey : typeValue));
4167
- return createTypeNode("Record", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { key: typeKey, value: typeValue });
5739
+ return createTypeNode("Record", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { additionalProperties: { key: typeKey, value: typeValue }, check });
4168
5740
  };
4169
5741
  const validateRecordEntries = (input, validateKey, validateValue, options) => {
4170
5742
  let issues;
4171
5743
  const output = createMutableRecord();
4172
5744
  const inputKeyByOutputKey = createMutableRecord();
4173
5745
  let changed = false;
4174
- for (const inputKey of Reflect.ownKeys(input)) {
5746
+ for (const inputKey of getOwnKeys(input)) {
4175
5747
  const keyResult = validateKey(inputKey, options);
4176
5748
  if (!keyResult.ok) {
4177
5749
  (issues ??= []).push({
@@ -4279,17 +5851,139 @@ const validateRecordEntries = (input, validateKey, validateValue, options) => {
4279
5851
  * @group Objects
4280
5852
  */
4281
5853
  export const optional = (type) => createOptionalProperty(type);
4282
- const optionalPropertySymbol = /*#__PURE__*/ globalThis.Symbol();
5854
+ const optionalPropertySymbol =
5855
+ /*#__PURE__*/ globalThis.Symbol();
4283
5856
  const createOptionalProperty = (type) => ({
4284
5857
  type,
4285
5858
  [optionalPropertySymbol]: true,
4286
5859
  });
5860
+ export function withDefault(property, value, { strategy } = {}) {
5861
+ assert(strategy === undefined || strategy === "preserve", 'withDefault strategy must be omitted or "preserve".');
5862
+ const optionalInput = optionalPropertySymbol in property;
5863
+ const type = (optionalInput ? property.type : property);
5864
+ assert(type.is(value), `withDefault value must be an Output of ${type.name}.`);
5865
+ const originals = new Set([
5866
+ ...(optionalInput ? ["missing"] : []),
5867
+ ...(type.is(null) ? ["null"] : []),
5868
+ ...(type.is(undefined) ? ["undefined"] : []),
5869
+ ]);
5870
+ const supplied = createRootType("DefaultSupplied", (candidate) => (strategy === undefined && globalThis.Object.is(candidate, value)) ||
5871
+ (candidate !== null && candidate !== undefined && type.is(candidate))
5872
+ ? ok(candidate)
5873
+ : err({ type: "DefaultValue" }), () => "The value must be a non-nullish Output of its Type or, in replacement mode, the configured default.");
5874
+ const defaultValue = createRootType("DefaultValue", (candidate) => type.is(candidate) &&
5875
+ (globalThis.Object.is(candidate, value) ||
5876
+ (Data.is(value) && Data.is(candidate) && eqData(candidate, value)))
5877
+ ? ok(candidate)
5878
+ : err({ type: "DefaultValue" }), () => "A preserved default must equal the configured default value.");
5879
+ const output = (strategy === undefined
5880
+ ? supplied
5881
+ : discriminatedUnion("defaultUsed", object({ value: supplied, defaultUsed: literal(false) }), object({
5882
+ value: defaultValue,
5883
+ defaultUsed: literal(true),
5884
+ original: createRootType("DefaultOriginal", (candidate) => typeof candidate === "string" && originals.has(candidate)
5885
+ ? ok(candidate)
5886
+ : err({ type: "DefaultOriginal" }), () => "The original absence must be handled by this default declaration."),
5887
+ })));
5888
+ const ownGetTypeIssues = output[getRuntimeTypeIssuesSymbol];
5889
+ const outputWithIssues = {
5890
+ ...output,
5891
+ [outputValidationSymbol]: (value, options) => {
5892
+ const result = output[outputValidationSymbol](value, options);
5893
+ return result.ok
5894
+ ? result
5895
+ : err({ type: "WithDefault", outputError: result.error });
5896
+ },
5897
+ [getRuntimeTypeIssuesSymbol]: (error, mode, path) => error.type === "WithDefault" && "outputError" in error
5898
+ ? ownGetTypeIssues(error.outputError, mode, path)
5899
+ : type[getRuntimeTypeIssuesSymbol](error, mode, path),
5900
+ // Output validation wraps errors, so a copied check would not match it.
5901
+ [checkSymbol]: undefined,
5902
+ [refinementsSymbol]: undefined,
5903
+ };
5904
+ const operations = {
5905
+ output: outputWithIssues,
5906
+ createObject: createDefaultObjectType,
5907
+ partial: (type) => createOptionalProperty(strategy === "preserve"
5908
+ ? withDefault(type, value, {
5909
+ strategy,
5910
+ })
5911
+ : withDefault(type, value)),
5912
+ decode: (candidate) => {
5913
+ const original = candidate === missingDefaultValue
5914
+ ? "missing"
5915
+ : candidate === null
5916
+ ? "null"
5917
+ : candidate === undefined
5918
+ ? "undefined"
5919
+ : undefined;
5920
+ const effective = original === undefined ? candidate : value;
5921
+ return strategy === undefined
5922
+ ? effective
5923
+ : original === undefined
5924
+ ? { value: effective, defaultUsed: false }
5925
+ : { value: effective, defaultUsed: true, original };
5926
+ },
5927
+ encode: (candidate) => {
5928
+ if (strategy === undefined)
5929
+ return candidate;
5930
+ const preserved = candidate;
5931
+ if (!preserved.defaultUsed)
5932
+ return preserved.value;
5933
+ switch (preserved.original) {
5934
+ case "missing":
5935
+ return missingDefaultValue;
5936
+ case "null":
5937
+ return null;
5938
+ case "undefined":
5939
+ return undefined;
5940
+ }
5941
+ },
5942
+ };
5943
+ if (optionalInput) {
5944
+ return {
5945
+ type,
5946
+ value,
5947
+ strategy: strategy ?? "replace",
5948
+ [defaultPropertySymbol]: operations,
5949
+ };
5950
+ }
5951
+ const fromOwn = (value) => ok(operations.decode(value));
5952
+ const fromParent = mapRuntimeOperations(type[fromSymbol], (operation) => (value, options) => flatMapResult(operation(value, options), fromOwn));
5953
+ return createTypeNode("WithDefault", type, (value, options) => flatMapResult(type.fromUnknown(value, options), fromOwn), operations.output.is, operations.output[outputValidationSymbol], createFromOperation(fromParent), operations.encode, operations.output[getRuntimeTypeIssuesSymbol]);
5954
+ }
5955
+ const defaultPropertySymbol =
5956
+ /*#__PURE__*/ globalThis.Symbol();
5957
+ const missingDefaultValue = /*#__PURE__*/ globalThis.Symbol();
4287
5958
  export function object(props, recordType) {
4288
5959
  return createObjectType(snapshotObjectProps(props), recordType);
4289
5960
  }
4290
- const createObjectType = (props, recordType) => {
5961
+ const createObjectType = (props, recordType,
5962
+ // A parent Object Type reuses the key order of its child.
5963
+ keys = globalThis.Object.keys(props)) => {
4291
5964
  const runtimeProps = props;
4292
- const keys = globalThis.Object.keys(runtimeProps);
5965
+ const keyCount = keys.length;
5966
+ const propertyTypes = createMutableArray(keyCount);
5967
+ const optionalFlags = createMutableArray(keyCount);
5968
+ for (let index = 0; index < keyCount; index++) {
5969
+ const property = runtimeProps[keys[index]];
5970
+ if (isDefaultProperty(property)) {
5971
+ return property[defaultPropertySymbol].createObject(props, recordType);
5972
+ }
5973
+ const isOptional = isOptionalProperty(property);
5974
+ const type = isOptional ? property.type : property;
5975
+ propertyTypes[index] = type;
5976
+ optionalFlags[index] = isOptional;
5977
+ }
5978
+ const propertyChecks = propertyTypes.map((type) => type[checkSymbol]);
5979
+ const recordCheck = recordType?.value[checkSymbol];
5980
+ // When every property and Record value Type is an identity Type, validation
5981
+ // never changes a value.
5982
+ const isIdentity = !propertyChecks.includes(undefined) &&
5983
+ (recordType === undefined || recordCheck !== undefined);
5984
+ let isOutputs;
5985
+ // Returns `undefined` for a valid input it did not change, so a valid
5986
+ // identity Object allocates no Result.
4293
5987
  const validate = (value, options, exactOutput) => {
4294
5988
  if (value === null || typeof value !== "object") {
4295
5989
  return err({
@@ -4304,116 +5998,158 @@ const createObjectType = (props, recordType) => {
4304
5998
  reason: { kind: "UnexpectedPrototype", value: input },
4305
5999
  });
4306
6000
  }
6001
+ const ownKeys = getOwnKeys(input);
6002
+ // Descriptors by visit index let a copy-on-write Output reuse the values
6003
+ // already read. Only a property without a check can change its value.
6004
+ const descriptors = isIdentity
6005
+ ? undefined
6006
+ : createMutableArray(keyCount + ownKeys.length);
4307
6007
  let errors;
4308
6008
  let output;
4309
- const inputDescriptorByKey = new Map();
4310
- for (const key of keys)
4311
- inputDescriptorByKey.set(key, undefined);
4312
- for (const key of Reflect.ownKeys(input)) {
4313
- inputDescriptorByKey.set(key, undefined);
4314
- }
4315
- const setError = (key, error) => {
4316
- errors ??= createMutableRecord();
4317
- errors[key] = error;
4318
- };
4319
- for (const [key] of inputDescriptorByKey) {
4320
- const property = typeof key === "string" && globalThis.Object.hasOwn(runtimeProps, key)
4321
- ? runtimeProps[key]
4322
- : undefined;
6009
+ // Own keys that are exactly the declared keys in their order leave the
6010
+ // undeclared-key loop nothing to visit. A first-mode stop leaves the flag
6011
+ // unchecked, but then that loop does not run either.
6012
+ let ownKeysAreDeclared = ownKeys.length === keyCount;
6013
+ for (let index = 0; (errors === undefined || options.errors !== "first") && index < keyCount; index++) {
6014
+ const key = keys[index];
6015
+ if (ownKeysAreDeclared && ownKeys[index] !== key) {
6016
+ ownKeysAreDeclared = false;
6017
+ }
4323
6018
  const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, key);
4324
- inputDescriptorByKey.set(key, descriptor);
6019
+ if (descriptors !== undefined)
6020
+ descriptors[index] = descriptor;
6021
+ let error;
4325
6022
  if (descriptor === undefined) {
4326
- assert(property !== undefined, "Object property is missing.");
4327
- if (isOptionalProperty(property)) {
6023
+ if (optionalFlags[index])
4328
6024
  continue;
6025
+ error = { type: "ObjectMissingProperty" };
6026
+ }
6027
+ else if (!("value" in descriptor)) {
6028
+ error = {
6029
+ type: "ObjectPropertyAccess",
6030
+ reason: "Accessor",
6031
+ };
6032
+ }
6033
+ else if (!descriptor.enumerable) {
6034
+ error = {
6035
+ type: "ObjectPropertyAccess",
6036
+ reason: "NonEnumerable",
6037
+ };
6038
+ }
6039
+ else {
6040
+ const propertyValue = descriptor.value;
6041
+ const check = propertyChecks[index];
6042
+ if (check !== undefined) {
6043
+ const propertyError = check(propertyValue, options);
6044
+ if (propertyError === undefined) {
6045
+ if (output !== undefined && errors === undefined) {
6046
+ output[key] = propertyValue;
6047
+ }
6048
+ continue;
6049
+ }
6050
+ error = propertyError;
4329
6051
  }
4330
- setError(key, { type: "ObjectMissingProperty" });
4331
- if (options.errors === "first")
4332
- break;
6052
+ else {
6053
+ const result = exactOutput
6054
+ ? propertyTypes[index][outputValidationSymbol](propertyValue, options)
6055
+ : propertyTypes[index].fromUnknown(propertyValue, options);
6056
+ if (result.ok) {
6057
+ if (errors !== undefined || exactOutput)
6058
+ continue;
6059
+ if (output === undefined) {
6060
+ if (globalThis.Object.is(result.value, propertyValue))
6061
+ continue;
6062
+ output = copyVisitedProperties(descriptors, ownKeys, index);
6063
+ }
6064
+ output[key] = result.value;
6065
+ continue;
6066
+ }
6067
+ error = result.error;
6068
+ }
6069
+ }
6070
+ errors ??= createMutableRecord();
6071
+ errors[key] = error;
6072
+ }
6073
+ const ownKeyEnd = ownKeysAreDeclared ? 0 : ownKeys.length;
6074
+ for (let position = 0; (errors === undefined || options.errors !== "first") &&
6075
+ position < ownKeyEnd; position++) {
6076
+ const key = ownKeys[position];
6077
+ const index = keyCount + position;
6078
+ if (typeof key === "string" &&
6079
+ // Own keys usually follow the declared order.
6080
+ ((position < keyCount && key === keys[position]) ||
6081
+ globalThis.Object.hasOwn(runtimeProps, key))) {
6082
+ if (descriptors !== undefined)
6083
+ descriptors[index] = undefined;
4333
6084
  continue;
4334
6085
  }
4335
- if (property === undefined && recordType === undefined) {
4336
- setError(key, {
6086
+ const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, key);
6087
+ assert(descriptor !== undefined, "Object property is missing.");
6088
+ if (descriptors !== undefined)
6089
+ descriptors[index] = descriptor;
6090
+ let error;
6091
+ if (recordType === undefined) {
6092
+ error = {
4337
6093
  type: "ObjectExcessProperty",
4338
- });
4339
- if (options.errors === "first")
4340
- break;
4341
- continue;
6094
+ };
4342
6095
  }
4343
- if (property === undefined && typeof key !== "string") {
4344
- setError(key, createRecordPropertyError({
6096
+ else if (typeof key !== "string") {
6097
+ error = createRecordPropertyError({
4345
6098
  kind: "Key",
4346
6099
  key,
4347
- error: {
4348
- type: "TypeOf",
4349
- expected: "String",
4350
- value: key,
4351
- },
4352
- }));
4353
- if (options.errors === "first")
4354
- break;
4355
- continue;
6100
+ error: { type: "TypeOf", expected: "String", value: key },
6101
+ });
4356
6102
  }
4357
- if (!("value" in descriptor)) {
4358
- const propertyError = {
6103
+ else if (!("value" in descriptor)) {
6104
+ error = {
4359
6105
  type: "ObjectPropertyAccess",
4360
6106
  reason: "Accessor",
4361
6107
  };
4362
- setError(key, propertyError);
4363
- if (options.errors === "first")
4364
- break;
4365
- continue;
4366
6108
  }
4367
- if (!descriptor.enumerable) {
4368
- const propertyError = {
6109
+ else if (!descriptor.enumerable) {
6110
+ error = {
4369
6111
  type: "ObjectPropertyAccess",
4370
6112
  reason: "NonEnumerable",
4371
6113
  };
4372
- setError(key, propertyError);
4373
- if (options.errors === "first")
4374
- break;
4375
- continue;
4376
- }
4377
- const propertyValue = descriptor.value;
4378
- const propertyType = property === undefined
4379
- ? recordType.value
4380
- : objectPropertyToType(property);
4381
- const result = exactOutput
4382
- ? propertyType[outputValidationSymbol](propertyValue, options)
4383
- : propertyType.fromUnknown(propertyValue, options);
4384
- if (!result.ok) {
4385
- setError(key, property === undefined
4386
- ? createRecordPropertyError({
4387
- kind: "Value",
4388
- key,
4389
- error: result.error,
4390
- })
4391
- : result.error);
4392
- if (options.errors === "first")
4393
- break;
4394
- continue;
4395
6114
  }
4396
- if (errors !== undefined)
4397
- continue;
4398
- if (exactOutput)
4399
- continue;
4400
- if (output === undefined &&
4401
- globalThis.Object.is(result.value, propertyValue)) {
4402
- continue;
4403
- }
4404
- if (output === undefined) {
4405
- output = globalThis.Object.create(null);
4406
- for (const [previousKey, previousDescriptor] of inputDescriptorByKey) {
4407
- if (previousKey === key)
4408
- break;
4409
- if (previousDescriptor !== undefined &&
4410
- "value" in previousDescriptor &&
4411
- previousDescriptor.enumerable) {
4412
- output[previousKey] = previousDescriptor.value;
6115
+ else {
6116
+ const propertyValue = descriptor.value;
6117
+ let valueError;
6118
+ if (recordCheck !== undefined) {
6119
+ const checkError = recordCheck(propertyValue, options);
6120
+ if (checkError === undefined) {
6121
+ if (output !== undefined && errors === undefined) {
6122
+ output[key] = propertyValue;
6123
+ }
6124
+ continue;
4413
6125
  }
6126
+ valueError = checkError;
4414
6127
  }
6128
+ else {
6129
+ const result = exactOutput
6130
+ ? recordType.value[outputValidationSymbol](propertyValue, options)
6131
+ : recordType.value.fromUnknown(propertyValue, options);
6132
+ if (result.ok) {
6133
+ if (errors !== undefined || exactOutput)
6134
+ continue;
6135
+ if (output === undefined) {
6136
+ if (globalThis.Object.is(result.value, propertyValue))
6137
+ continue;
6138
+ output = copyVisitedProperties(descriptors, ownKeys, index);
6139
+ }
6140
+ output[key] = result.value;
6141
+ continue;
6142
+ }
6143
+ valueError = result.error;
6144
+ }
6145
+ error = createRecordPropertyError({
6146
+ kind: "Value",
6147
+ key,
6148
+ error: valueError,
6149
+ });
4415
6150
  }
4416
- output[key] = result.value;
6151
+ errors ??= createMutableRecord();
6152
+ errors[key] = error;
4417
6153
  }
4418
6154
  if (errors !== undefined) {
4419
6155
  return err({
@@ -4421,69 +6157,70 @@ const createObjectType = (props, recordType) => {
4421
6157
  reason: { kind: "Properties", errors },
4422
6158
  });
4423
6159
  }
4424
- if (output === undefined)
4425
- return ok(input);
4426
- return ok(output);
6160
+ return output === undefined ? undefined : ok(output);
4427
6161
  };
4428
- const fromUnknown = (value, options = firstValidationOptions) => validate(value, options, false);
4429
- const validateOutput = (value, options = firstValidationOptions) => validate(value, options, true);
4430
- const formatError = (error) => {
4431
- if (error.reason.kind !== "Properties")
4432
- return formatPlainObjectRootError(error.reason);
4433
- const key = Reflect.ownKeys(error.reason.errors).at(0);
4434
- assertNonNullable(key);
4435
- const propertyError = error.reason.errors[key];
4436
- assertNonNullable(propertyError);
4437
- if (propertyError.type === "ObjectPropertyAccess") {
4438
- switch (propertyError.reason) {
4439
- case "Accessor":
4440
- return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
4441
- case "NonEnumerable":
4442
- return "An Object property must be enumerable. Make it enumerable or use a different Type.";
4443
- }
6162
+ // Properties visited before `end` are valid and unchanged, so a new Output
6163
+ // starts with their values.
6164
+ const copyVisitedProperties = (descriptors, ownKeys, end) => {
6165
+ const output = globalThis.Object.create(null);
6166
+ for (let index = 0; index < end; index++) {
6167
+ const descriptor = descriptors[index];
6168
+ if (descriptor === undefined)
6169
+ continue;
6170
+ output[index < keyCount ? keys[index] : ownKeys[index - keyCount]] =
6171
+ descriptor.value;
4444
6172
  }
4445
- if (propertyError.type === "ObjectMissingProperty")
4446
- return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
4447
- if (typeof key === "symbol")
4448
- return "An Object property key must be a string. Remove the symbol property or use a different Type.";
4449
- if (propertyError.type === "ObjectExcessProperty")
4450
- return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
4451
- return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
6173
+ return output;
4452
6174
  };
4453
- const rootProps = createMutableRecord();
6175
+ // An identity Object builds no Output, so its validation returns an Err or
6176
+ // `undefined`.
6177
+ const check = isIdentity
6178
+ ? (value, options) => {
6179
+ const result = validate(value, options, false);
6180
+ return result?.ok === false ? result.error : undefined;
6181
+ }
6182
+ : undefined;
6183
+ const fromUnknown = (value, options = firstValidationOptions) => validate(value, options, false) ??
6184
+ ok(value);
6185
+ const validateOutput = isIdentity
6186
+ ? fromUnknown
6187
+ : (value, options = firstValidationOptions) => validate(value, options, true) ??
6188
+ ok(value);
4454
6189
  let hasNonRootType = false;
4455
6190
  let canSkipTo = recordType === undefined || recordType.value[encoderSymbol] === identity;
4456
- for (const key of keys) {
4457
- const property = runtimeProps[key];
4458
- const type = objectPropertyToType(property);
6191
+ for (let index = 0; index < keyCount; index++) {
6192
+ const type = propertyTypes[index];
4459
6193
  if (type[encoderSymbol] !== identity)
4460
6194
  canSkipTo = false;
4461
- const rootType = getTerminalRuntimeNode(type);
4462
- if (rootType !== type)
6195
+ if (type.parent != null)
4463
6196
  hasNonRootType = true;
4464
- rootProps[key] = isOptionalProperty(property)
4465
- ? optional(rootType)
4466
- : rootType;
4467
6197
  }
4468
- const parent = hasNonRootType || recordType?.parent
4469
- ? createObjectType(rootProps, (recordType?.parent ?? recordType))
4470
- : null;
4471
- const inputFromByKey = createMutableRecord();
4472
- for (const key of keys) {
4473
- inputFromByKey[key] = getTerminalRuntimeNode(objectPropertyToType(runtimeProps[key])[fromSymbol]);
6198
+ let parent = null;
6199
+ if (hasNonRootType || recordType?.parent) {
6200
+ const rootProps = createMutableRecord();
6201
+ for (let index = 0; index < keyCount; index++) {
6202
+ const rootType = getTerminalRuntimeNode(propertyTypes[index]);
6203
+ rootProps[keys[index]] = optionalFlags[index]
6204
+ ? optional(rootType)
6205
+ : rootType;
6206
+ }
6207
+ parent = createObjectType(rootProps, (recordType?.parent ?? recordType), keys);
4474
6208
  }
6209
+ let inputFroms;
4475
6210
  const recordValueFromInput = recordType
4476
6211
  ? getTerminalRuntimeNode(recordType.value[fromSymbol])
4477
6212
  : undefined;
4478
6213
  const fromParent = parent
4479
6214
  ? (value, options = firstValidationOptions) => {
6215
+ const propertyFroms = (inputFroms ??= propertyTypes.map((type) => getTerminalRuntimeNode(type[fromSymbol])));
4480
6216
  let errors;
4481
6217
  let output;
4482
- for (const key of keys) {
6218
+ for (let index = 0; index < keyCount; index++) {
6219
+ const key = keys[index];
4483
6220
  if (!globalThis.Object.hasOwn(value, key))
4484
6221
  continue;
4485
6222
  const propertyValue = value[key];
4486
- const result = inputFromByKey[key](propertyValue, options);
6223
+ const result = propertyFroms[index](propertyValue, options);
4487
6224
  if (result.ok) {
4488
6225
  if (!globalThis.Object.is(result.value, propertyValue)) {
4489
6226
  (output ??= createMutableRecord(value))[key] = result.value;
@@ -4531,11 +6268,12 @@ const createObjectType = (props, recordType) => {
4531
6268
  ? identity
4532
6269
  : (value) => {
4533
6270
  let output;
4534
- for (const key of keys) {
6271
+ for (let index = 0; index < keyCount; index++) {
6272
+ const key = keys[index];
4535
6273
  if (!globalThis.Object.hasOwn(value, key))
4536
6274
  continue;
4537
6275
  const propertyValue = value[key];
4538
- const encoded = objectPropertyToType(runtimeProps[key])[encoderSymbol](propertyValue);
6276
+ const encoded = propertyTypes[index][encoderSymbol](propertyValue);
4539
6277
  if (!globalThis.Object.is(encoded, propertyValue)) {
4540
6278
  (output ??= createMutableRecord(value))[key] = encoded;
4541
6279
  }
@@ -4553,57 +6291,139 @@ const createObjectType = (props, recordType) => {
4553
6291
  }
4554
6292
  return output ?? value;
4555
6293
  };
4556
- const is = (value) => {
4557
- if (value === null || typeof value !== "object")
4558
- return false;
4559
- if (!isPlainObject(value))
4560
- return false;
4561
- for (const key of keys) {
4562
- const property = runtimeProps[key];
4563
- const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, key);
6294
+ const isDeclared = (value) => {
6295
+ const isProperties = (isOutputs ??= propertyTypes.map(getIs));
6296
+ for (let index = 0; index < keyCount; index++) {
6297
+ const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, keys[index]);
4564
6298
  if (descriptor === undefined) {
4565
- if (!isOptionalProperty(property))
6299
+ if (!optionalFlags[index])
4566
6300
  return false;
4567
6301
  continue;
4568
6302
  }
4569
6303
  if (!("value" in descriptor) ||
4570
6304
  !descriptor.enumerable ||
4571
- !objectPropertyToType(property).is(descriptor.value)) {
6305
+ !isProperties[index](descriptor.value)) {
4572
6306
  return false;
4573
6307
  }
4574
6308
  }
4575
- for (const key of Reflect.ownKeys(value)) {
4576
- if (typeof key === "string" &&
4577
- globalThis.Object.hasOwn(runtimeProps, key)) {
4578
- continue;
4579
- }
4580
- if (recordType === undefined || typeof key !== "string")
6309
+ return true;
6310
+ };
6311
+ // Separate predicates keep each small; one shared by both cases was up to
6312
+ // 5% slower in either.
6313
+ const is = recordType === undefined
6314
+ ? (value) => {
6315
+ if (value === null || typeof value !== "object")
4581
6316
  return false;
4582
- const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, key);
4583
- if (descriptor === undefined ||
4584
- !("value" in descriptor) ||
4585
- !descriptor.enumerable) {
6317
+ if (!isPlainObject(value) || !isDeclared(value))
4586
6318
  return false;
6319
+ // Checking names has no effects without a Record, so symbols, which
6320
+ // fail, are counted after them.
6321
+ const names = globalThis.Object.getOwnPropertyNames(value);
6322
+ for (let position = 0; position < names.length; position++) {
6323
+ const key = names[position];
6324
+ if (!(position < keyCount && key === keys[position]) &&
6325
+ !globalThis.Object.hasOwn(runtimeProps, key)) {
6326
+ return false;
6327
+ }
4587
6328
  }
4588
- if (!recordType.value.is(descriptor.value))
6329
+ return globalThis.Object.getOwnPropertySymbols(value).length === 0;
6330
+ }
6331
+ : (value) => {
6332
+ if (value === null || typeof value !== "object")
6333
+ return false;
6334
+ if (!isPlainObject(value) || !isDeclared(value))
4589
6335
  return false;
6336
+ const hasSymbols = globalThis.Object.getOwnPropertySymbols(value).length !== 0;
6337
+ const names = hasSymbols
6338
+ ? getNamesBeforeSymbol(value)
6339
+ : globalThis.Object.getOwnPropertyNames(value);
6340
+ for (let position = 0; position < names.length; position++) {
6341
+ const key = names[position];
6342
+ if ((position < keyCount && key === keys[position]) ||
6343
+ globalThis.Object.hasOwn(runtimeProps, key)) {
6344
+ continue;
6345
+ }
6346
+ const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, key);
6347
+ if (descriptor === undefined ||
6348
+ !("value" in descriptor) ||
6349
+ !descriptor.enumerable) {
6350
+ return false;
6351
+ }
6352
+ if (!recordType.value.is(descriptor.value))
6353
+ return false;
6354
+ }
6355
+ return !hasSymbols;
6356
+ };
6357
+ const getTypeIssues = createObjectRuntimeTypeIssues(runtimeProps, recordType);
6358
+ return createTypeNode("Object", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, {
6359
+ additionalProperties: recordType
6360
+ ? {
6361
+ props: runtimeProps,
6362
+ record: recordType,
6363
+ }
6364
+ : { props: runtimeProps },
6365
+ check,
6366
+ });
6367
+ };
6368
+ const createDefaultObjectType = (props, recordType) => {
6369
+ const inputProps = createMutableRecord();
6370
+ const outputProps = createMutableRecord();
6371
+ const defaults = new Map();
6372
+ for (const [key, property] of globalThis.Object.entries(props)) {
6373
+ if (isDefaultProperty(property)) {
6374
+ const defaultProperty = property;
6375
+ inputProps[key] = createOptionalProperty(defaultProperty.type);
6376
+ outputProps[key] = defaultProperty[defaultPropertySymbol].output;
6377
+ defaults.set(key, defaultProperty[defaultPropertySymbol]);
4590
6378
  }
4591
- return true;
6379
+ else {
6380
+ inputProps[key] = property;
6381
+ const type = objectPropertyToType(property);
6382
+ const output = createRootType(type.name, type[outputValidationSymbol], type.formatError, { getTypeIssues: type[getRuntimeTypeIssuesSymbol] });
6383
+ outputProps[key] =
6384
+ optionalPropertySymbol in property
6385
+ ? createOptionalProperty(output)
6386
+ : output;
6387
+ }
6388
+ }
6389
+ const source = createObjectType(inputProps, recordType);
6390
+ // The object parent validates only its input structure and root value Types.
6391
+ const parent = getTerminalRuntimeNode(source);
6392
+ const output = createObjectType(outputProps, recordType);
6393
+ const finish = (value) => {
6394
+ const result = createMutableRecord(value);
6395
+ for (const [key, operations] of defaults) {
6396
+ result[key] = operations.decode(globalThis.Object.hasOwn(result, key)
6397
+ ? result[key]
6398
+ : missingDefaultValue);
6399
+ }
6400
+ return ok(result);
4592
6401
  };
4593
- const getTypeIssues = createObjectRuntimeTypeIssues(formatError, runtimeProps, recordType);
4594
- return createTypeNode("Object", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, recordType
4595
- ? {
4596
- props: runtimeProps,
4597
- record: recordType,
6402
+ const fromInput = getTerminalRuntimeNode(source[fromSymbol]);
6403
+ const fromParent = (value, options) => flatMapResult(fromInput(value, options), finish);
6404
+ const to = (value) => {
6405
+ const result = createMutableRecord(value);
6406
+ for (const [key, operations] of defaults) {
6407
+ const original = operations.encode(result[key]);
6408
+ if (original === missingDefaultValue)
6409
+ delete result[key];
6410
+ else
6411
+ result[key] = original;
4598
6412
  }
4599
- : { props: runtimeProps });
6413
+ return source[encoderSymbol](result);
6414
+ };
6415
+ return createTypeNode("Object", parent, (value, options) => flatMapResult(source.fromUnknown(value, options), finish), output.is, output[outputValidationSymbol], createFromOperation(fromParent), to, createObjectRuntimeTypeIssues(props, recordType), {
6416
+ additionalProperties: recordType
6417
+ ? { props, record: recordType }
6418
+ : { props },
6419
+ });
4600
6420
  };
4601
6421
  // Read descriptors instead of spreading so accessors are not invoked,
4602
6422
  // non-enumerable declarations are retained, and later mutations are isolated.
4603
6423
  const snapshotObjectProps = (props, runtimeProps = createMutableRecord()) => {
4604
6424
  const errorMessage = "Object schema properties must be own string-keyed data properties.";
4605
6425
  assert(isPlainObject(props), errorMessage);
4606
- for (const key of Reflect.ownKeys(props)) {
6426
+ for (const key of getOwnKeys(props)) {
4607
6427
  const descriptor = globalThis.Object.getOwnPropertyDescriptor(props, key);
4608
6428
  assert(typeof key === "string" &&
4609
6429
  descriptor !== undefined &&
@@ -4613,7 +6433,12 @@ const snapshotObjectProps = (props, runtimeProps = createMutableRecord()) => {
4613
6433
  return runtimeProps;
4614
6434
  };
4615
6435
  const isOptionalProperty = (property) => optionalPropertySymbol in property;
4616
- const objectPropertyToType = (property) => (isOptionalProperty(property) ? property.type : property);
6436
+ const isDefaultProperty = (property) => defaultPropertySymbol in property;
6437
+ const objectPropertyToType = (property) => isDefaultProperty(property)
6438
+ ? property[defaultPropertySymbol].output
6439
+ : isOptionalProperty(property)
6440
+ ? property.type
6441
+ : property;
4617
6442
  const createRecordPropertyError = (issue) => ({
4618
6443
  type: "Record",
4619
6444
  reason: { kind: "Entries", issues: [issue] },
@@ -4622,7 +6447,8 @@ const createRecordPropertyError = (issue) => ({
4622
6447
  * Object {@link Type} with every property optional.
4623
6448
  *
4624
6449
  * No property is required, but every present property must still satisfy its
4625
- * Type.
6450
+ * Type. For {@link withDefault} properties, this disables the missing-property
6451
+ * default; defaults for present `null` or `undefined` values still apply.
4626
6452
  *
4627
6453
  * ### Example
4628
6454
  *
@@ -4650,9 +6476,11 @@ export const partial = (props, ..._validation) => {
4650
6476
  const partialProps = createMutableRecord();
4651
6477
  for (const key of globalThis.Object.keys(source)) {
4652
6478
  const property = source[key];
4653
- partialProps[key] = isOptionalProperty(property)
4654
- ? property
4655
- : createOptionalProperty(property);
6479
+ partialProps[key] = isDefaultProperty(property)
6480
+ ? property[defaultPropertySymbol].partial(property.type)
6481
+ : isOptionalProperty(property)
6482
+ ? property
6483
+ : createOptionalProperty(property);
4656
6484
  }
4657
6485
  return createObjectType(partialProps);
4658
6486
  };
@@ -4693,7 +6521,7 @@ export const nullableToOptional = (props, ..._validation) => {
4693
6521
  const optionalProps = createMutableRecord();
4694
6522
  for (const key of globalThis.Object.keys(source)) {
4695
6523
  const property = source[key];
4696
- if (isOptionalProperty(property)) {
6524
+ if (isOptionalProperty(property) || isDefaultProperty(property)) {
4697
6525
  optionalProps[key] = property;
4698
6526
  continue;
4699
6527
  }
@@ -4732,6 +6560,101 @@ export const omit = (objectType, ...keys) => {
4732
6560
  }
4733
6561
  return createObjectType(props, runtimeObjectType.record);
4734
6562
  };
6563
+ /**
6564
+ * Renames a strict {@link object} Type's encoded keys using a string codec.
6565
+ *
6566
+ * The object declaration uses semantic keys. Each key must be a valid Output of
6567
+ * the key Type. Its canonical encoding becomes the external property name.
6568
+ * Decoding accepts those exact names; aliases and unknown properties are
6569
+ * rejected. Values, optionality, and semantic Output stay with the object Type.
6570
+ * Only the outer keys change. Nested field codecs keep their own behavior.
6571
+ *
6572
+ * Construction rejects invalid schema keys, duplicate encodings, and key codecs
6573
+ * that do not decode their encodings back to the declared keys. These are
6574
+ * schema mistakes; invalid external values return normal Type errors. Error
6575
+ * paths use the external names, including missing required properties.
6576
+ *
6577
+ * ### Example
6578
+ *
6579
+ * ```ts
6580
+ * import {
6581
+ * assertEqual,
6582
+ * assertErr,
6583
+ * assertOk,
6584
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
6585
+ * object,
6586
+ * objectKeys,
6587
+ * optional,
6588
+ * PortFromString,
6589
+ * prefixed,
6590
+ * } from "@evolu/common";
6591
+ *
6592
+ * const Key = prefixed("APP_")(
6593
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
6594
+ * );
6595
+ * const Settings = objectKeys(Key)(
6596
+ * object({ port: optional(PortFromString) }),
6597
+ * );
6598
+ *
6599
+ * const result = Settings.fromUnknown({ APP_PORT: "04000" });
6600
+ * assertOk(result, { port: 4000 });
6601
+ *
6602
+ * assertEqual(Settings.to(result.value), { APP_PORT: "4000" });
6603
+ *
6604
+ * assertErr(Settings.fromUnknown({ APP_POTR: "4000" }));
6605
+ * ```
6606
+ *
6607
+ * @group Objects
6608
+ */
6609
+ export const objectKeys = (key) => (type) => {
6610
+ const runtimeKey = key;
6611
+ const runtimeOutput = type;
6612
+ const props = createMutableRecord();
6613
+ const outputKeyByInputKey = new Map();
6614
+ const inputKeyByOutputKey = new Map();
6615
+ for (const outputKey of globalThis.Object.keys(type.props)) {
6616
+ assert(runtimeKey.is(outputKey), `Invalid object schema key ${safelyStringifyUnknownValue(outputKey)} for ${key.name}.`);
6617
+ const inputKey = runtimeKey.to(outputKey);
6618
+ assert(!outputKeyByInputKey.has(inputKey), `Duplicate encoded object key ${safelyStringifyUnknownValue(inputKey)}.`);
6619
+ const decoded = runtimeKey.fromUnknown(inputKey, firstValidationOptions);
6620
+ assert(decoded.ok && decoded.value === outputKey, "An object key codec must decode its encoding to the declared key.");
6621
+ props[inputKey] = type.props[outputKey];
6622
+ outputKeyByInputKey.set(inputKey, outputKey);
6623
+ inputKeyByOutputKey.set(outputKey, inputKey);
6624
+ }
6625
+ const input = createObjectType(props);
6626
+ const fromUnknown = (value, options = firstValidationOptions) => {
6627
+ const result = input.fromUnknown(value, options);
6628
+ if (!result.ok)
6629
+ return err({ type: "ObjectKeys", error: result.error });
6630
+ const output = createMutableRecord();
6631
+ for (const [inputKey, value] of globalThis.Object.entries(result.value)) {
6632
+ output[outputKeyByInputKey.get(inputKey)] = value;
6633
+ }
6634
+ return ok(output);
6635
+ };
6636
+ const to = (value) => {
6637
+ const encoded = runtimeOutput[encoderSymbol](value);
6638
+ const input = createMutableRecord();
6639
+ for (const [key, value] of globalThis.Object.entries(encoded)) {
6640
+ input[inputKeyByOutputKey.get(key)] = value;
6641
+ }
6642
+ return input;
6643
+ };
6644
+ const validateOutput = (value, options) => {
6645
+ const result = runtimeOutput[outputValidationSymbol](value, options);
6646
+ return result.ok
6647
+ ? result
6648
+ : err({ type: "ObjectKeys", outputError: result.error });
6649
+ };
6650
+ const getTypeIssues = (error, mode, path) => {
6651
+ if ("outputError" in error) {
6652
+ return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode, path);
6653
+ }
6654
+ return input[getRuntimeTypeIssuesSymbol](error.error, mode, path);
6655
+ };
6656
+ return createTypeNode("ObjectKeys", Unknown, fromUnknown, runtimeOutput.is, validateOutput, createFromOperation(fromUnknown), to, getTypeIssues, { additionalProperties: { key, output: type } });
6657
+ };
4735
6658
  export function result(okType, errorType) {
4736
6659
  return discriminatedUnion("ok", createObjectType({
4737
6660
  ok: literal(true),
@@ -4788,21 +6711,28 @@ export function discriminatedUnion(...keyOrMembers) {
4788
6711
  membersByDiscriminator.set(value, member);
4789
6712
  }
4790
6713
  const routeUnknown = (value) => {
4791
- const objectResult = value === null || typeof value !== "object"
4792
- ? err({ type: "Object", reason: { kind: "NotObject", value } })
4793
- : !isPlainObject(value)
4794
- ? err({
4795
- type: "Object",
4796
- reason: { kind: "UnexpectedPrototype", value },
4797
- })
4798
- : ok(value);
4799
- if (!objectResult.ok) {
6714
+ if (value === null || typeof value !== "object") {
4800
6715
  return err({
4801
6716
  type: "DiscriminatedUnion",
4802
- reason: { kind: "Object", error: objectResult.error },
6717
+ reason: {
6718
+ kind: "Object",
6719
+ error: { type: "Object", reason: { kind: "NotObject", value } },
6720
+ },
6721
+ });
6722
+ }
6723
+ if (!isPlainObject(value)) {
6724
+ return err({
6725
+ type: "DiscriminatedUnion",
6726
+ reason: {
6727
+ kind: "Object",
6728
+ error: {
6729
+ type: "Object",
6730
+ reason: { kind: "UnexpectedPrototype", value },
6731
+ },
6732
+ },
4803
6733
  });
4804
6734
  }
4805
- const input = objectResult.value;
6735
+ const input = value;
4806
6736
  const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, key);
4807
6737
  let discriminator;
4808
6738
  if (descriptor === undefined) {
@@ -4843,16 +6773,15 @@ export function discriminatedUnion(...keyOrMembers) {
4843
6773
  }
4844
6774
  return ok(member);
4845
6775
  };
4846
- const wrapMemberResult = (member, result) => result.ok
4847
- ? result
4848
- : err({
4849
- type: "DiscriminatedUnion",
4850
- reason: {
4851
- kind: "Member",
4852
- discriminator: member.props[key].expected,
4853
- error: result.error,
4854
- },
4855
- });
6776
+ const createMemberError = (member, error) => ({
6777
+ type: "DiscriminatedUnion",
6778
+ reason: {
6779
+ kind: "Member",
6780
+ discriminator: member.props[key].expected,
6781
+ error,
6782
+ },
6783
+ });
6784
+ const wrapMemberResult = (member, result) => result.ok ? result : err(createMemberError(member, result.error));
4856
6785
  const validateUnknown = (value, options = firstValidationOptions, useParent, exactOutput) => {
4857
6786
  const routeResult = routeUnknown(value);
4858
6787
  if (!routeResult.ok)
@@ -4891,24 +6820,34 @@ export function discriminatedUnion(...keyOrMembers) {
4891
6820
  }
4892
6821
  };
4893
6822
  const defaultFormatter = formatError;
4894
- const getTypeIssues = (error, mode) => {
6823
+ const getTypeIssues = (error, mode, path) => {
4895
6824
  const discriminatedUnionError = error;
4896
6825
  if (discriminatedUnionError.reason.kind === "Member") {
4897
6826
  const reason = discriminatedUnionError.reason;
4898
6827
  const member = membersByDiscriminator.get(reason.discriminator);
4899
6828
  assertNonNullable(member);
4900
- return member[getRuntimeTypeIssuesSymbol](reason.error, mode);
6829
+ return member[getRuntimeTypeIssuesSymbol](reason.error, mode, path);
4901
6830
  }
4902
6831
  if (discriminatedUnionError.reason.kind === "PropertyAccess" ||
4903
6832
  discriminatedUnionError.reason.kind === "Discriminator") {
4904
- return singleRuntimeTypeIssue("DiscriminatedUnion", discriminatedUnionError, defaultFormatter, [discriminatedUnionError.reason.key]);
6833
+ return singleRuntimeTypeIssue("DiscriminatedUnion", discriminatedUnionError, defaultFormatter, appendIssuePath(path, discriminatedUnionError.reason.key));
4905
6834
  }
4906
- return singleRuntimeTypeIssue("DiscriminatedUnion", error, defaultFormatter);
6835
+ return singleRuntimeTypeIssue("DiscriminatedUnion", error, defaultFormatter, path);
4907
6836
  };
4908
6837
  const parentFromUnknown = (value, options) => validateUnknown(value, options, true, false);
4909
6838
  const parentValidateOutput = (value, options) => validateUnknown(value, options, true, true);
4910
6839
  const parentTo = (value) => route(value).parent[encoderSymbol](value);
4911
- const parent = createTypeNode("DiscriminatedUnion", null, parentFromUnknown, (value) => getOutputMember(value)?.parent.is(value) ?? false, parentValidateOutput, ok, parentTo, getTypeIssues, { key, members });
6840
+ const parent = createTypeNode("DiscriminatedUnion", null, parentFromUnknown, (value) => getOutputMember(value)?.parent.is(value) ?? false, parentValidateOutput, ok, parentTo, getTypeIssues, { additionalProperties: { key, members } });
6841
+ // Identity members return the input itself, and so does routing.
6842
+ const check = getRuntimeChecks(members) &&
6843
+ ((value, options) => {
6844
+ const routeResult = routeUnknown(value);
6845
+ if (!routeResult.ok)
6846
+ return routeResult.error;
6847
+ const member = routeResult.value;
6848
+ const error = member[checkSymbol](value, options);
6849
+ return error === undefined ? undefined : createMemberError(member, error);
6850
+ });
4912
6851
  const fromUnknown = (value, options) => validateUnknown(value, options, false, false);
4913
6852
  const validateOutput = (value, options) => validateUnknown(value, options, false, true);
4914
6853
  const fromParent = (value, options = firstValidationOptions) => {
@@ -4918,7 +6857,7 @@ export function discriminatedUnion(...keyOrMembers) {
4918
6857
  };
4919
6858
  const from = createFromOperation(fromParent);
4920
6859
  const to = (value) => route(value)[encoderSymbol](value);
4921
- return createTypeNode("DiscriminatedUnion", parent, fromUnknown, (value) => getOutputMember(value)?.is(value) ?? false, validateOutput, from, to, getTypeIssues, { key, members });
6860
+ return createTypeNode("DiscriminatedUnion", parent, fromUnknown, (value) => getOutputMember(value)?.is(value) ?? false, validateOutput, from, to, getTypeIssues, { additionalProperties: { key, members }, check });
4922
6861
  }
4923
6862
  export function lazy(getType) {
4924
6863
  let resolution = { state: "unresolved" };
@@ -4960,11 +6899,11 @@ export function lazy(getType) {
4960
6899
  };
4961
6900
  const parentFromUnknown = (value, options = firstValidationOptions) => resolve().root.fromUnknown(value, options);
4962
6901
  const parentTo = (value) => resolve().root[encoderSymbol](value);
4963
- const parent = createTypeNode("Lazy", null, parentFromUnknown, (value) => resolve().root.is(value), (value, options) => resolve().root[outputValidationSymbol](value, options), ok, parentTo, (error, mode) => resolve().root[getRuntimeTypeIssuesSymbol](error, mode));
6902
+ const parent = createTypeNode("Lazy", null, parentFromUnknown, (value) => resolve().root.is(value), (value, options) => resolve().root[outputValidationSymbol](value, options), ok, parentTo, (error, mode, path) => resolve().root[getRuntimeTypeIssuesSymbol](error, mode, path));
4964
6903
  const fromUnknown = (value, options = firstValidationOptions) => resolve().target.fromUnknown(value, options);
4965
6904
  const fromParent = (value, options = firstValidationOptions) => resolve().targetFromInput(value, options);
4966
6905
  const from = createFromOperation(fromParent);
4967
- const type = createTypeNode("Lazy", parent, fromUnknown, (value) => resolve().target.is(value), (value, options) => resolve().target[outputValidationSymbol](value, options), from, (value) => resolve().target[encoderSymbol](value), (error, mode) => resolve().target[getRuntimeTypeIssuesSymbol](error, mode));
6906
+ const type = createTypeNode("Lazy", parent, fromUnknown, (value) => resolve().target.is(value), (value, options) => resolve().target[outputValidationSymbol](value, options), from, (value) => resolve().target[encoderSymbol](value), (error, mode, path) => resolve().target[getRuntimeTypeIssuesSymbol](error, mode, path));
4968
6907
  lazyTypeNodes.add(parent);
4969
6908
  lazyTypeNodes.add(type);
4970
6909
  return type;
@@ -5035,10 +6974,17 @@ const validateData = (value, options = firstValidationOptions) => {
5035
6974
  const kind = getObjectKind(value);
5036
6975
  if (kind === "Array") {
5037
6976
  const array = value;
5038
- for (const key of Reflect.ownKeys(array)) {
6977
+ const keys = Reflect.ownKeys(array);
6978
+ for (let position = 0; position < keys.length; position++) {
6979
+ const key = keys[position];
5039
6980
  if (key === "length")
5040
6981
  continue;
5041
6982
  if (typeof key === "string") {
6983
+ // Ordinary arrays list their indexes first, in ascending order.
6984
+ // Comparing the key first reads `length` only for an index key.
6985
+ if (key === globalThis.String(position) && position < array.length) {
6986
+ continue;
6987
+ }
5042
6988
  const index = globalThis.Number(key) >>> 0;
5043
6989
  if (index < array.length && globalThis.String(index) === key) {
5044
6990
  continue;
@@ -5073,7 +7019,7 @@ const validateData = (value, options = firstValidationOptions) => {
5073
7019
  }
5074
7020
  }
5075
7021
  else if (kind === "Object") {
5076
- for (const key of Reflect.ownKeys(value)) {
7022
+ for (const key of getOwnKeys(value)) {
5077
7023
  const childPath = dataChildPath(path, key);
5078
7024
  if (typeof key === "symbol") {
5079
7025
  if (addIssue({
@@ -5187,7 +7133,7 @@ const validateData = (value, options = firstValidationOptions) => {
5187
7133
  },
5188
7134
  });
5189
7135
  };
5190
- const getDataRuntimeTypeIssues = (error, mode) => {
7136
+ const getDataRuntimeTypeIssues = (error, mode, path) => {
5191
7137
  const dataError = error;
5192
7138
  const issues = mode === "first"
5193
7139
  ? [dataError.reason.issues[0]]
@@ -5200,7 +7146,8 @@ const getDataRuntimeTypeIssues = (error, mode) => {
5200
7146
  type: "Data",
5201
7147
  reason: { kind: "Issues", issues: [issue] },
5202
7148
  },
5203
- path: issue.path,
7149
+ // A root issue shares the path array of its error.
7150
+ path: path.length === 0 ? issue.path : [...path, ...issue.path],
5204
7151
  formatError: ((error) => {
5205
7152
  const issue = error.reason.issues[0];
5206
7153
  switch (issue.kind) {
@@ -5244,7 +7191,12 @@ const getDataRuntimeTypeIssues = (error, mode) => {
5244
7191
  *
5245
7192
  * @group Base
5246
7193
  */
5247
- export const Data = /*#__PURE__*/ createTypeNode("Data", null, validateData, (value) => validateData(value).ok, validateData, ok, identity, getDataRuntimeTypeIssues);
7194
+ export const Data = /*#__PURE__*/ createTypeNode("Data", null, validateData, (value) => validateData(value).ok, validateData, ok, identity, getDataRuntimeTypeIssues, {
7195
+ check: (value, options) => {
7196
+ const result = validateData(value, options);
7197
+ return result.ok ? undefined : result.error;
7198
+ },
7199
+ });
5248
7200
  const emptyJsonValuePath =
5249
7201
  /*#__PURE__*/ globalThis.Object.freeze([]);
5250
7202
  const jsonValuePathToArray = (path) => {
@@ -5327,10 +7279,17 @@ const validateJsonValue = (value, options = firstValidationOptions) => {
5327
7279
  }
5328
7280
  const children = [];
5329
7281
  if (isArray) {
5330
- for (const key of Reflect.ownKeys(value)) {
7282
+ const keys = Reflect.ownKeys(value);
7283
+ for (let position = 0; position < keys.length; position++) {
7284
+ const key = keys[position];
5331
7285
  if (key === "length")
5332
7286
  continue;
5333
7287
  if (typeof key === "string") {
7288
+ // Ordinary arrays list their indexes first, in ascending order.
7289
+ // Comparing the key first reads `length` only for an index key.
7290
+ if (key === globalThis.String(position) && position < value.length) {
7291
+ continue;
7292
+ }
5334
7293
  const index = globalThis.Number(key) >>> 0;
5335
7294
  if (index < value.length && globalThis.String(index) === key) {
5336
7295
  continue;
@@ -5374,7 +7333,7 @@ const validateJsonValue = (value, options = firstValidationOptions) => {
5374
7333
  }
5375
7334
  }
5376
7335
  else {
5377
- for (const key of Reflect.ownKeys(value)) {
7336
+ for (const key of getOwnKeys(value)) {
5378
7337
  const childPath = jsonValueChildPath(path, key);
5379
7338
  if (typeof key === "symbol") {
5380
7339
  if (addIssue({
@@ -5429,7 +7388,7 @@ const validateJsonValue = (value, options = firstValidationOptions) => {
5429
7388
  },
5430
7389
  });
5431
7390
  };
5432
- const getJsonValueRuntimeTypeIssues = (error, mode) => {
7391
+ const getJsonValueRuntimeTypeIssues = (error, mode, path) => {
5433
7392
  const jsonValueError = error;
5434
7393
  const issues = mode === "first"
5435
7394
  ? [jsonValueError.reason.issues[0]]
@@ -5442,7 +7401,7 @@ const getJsonValueRuntimeTypeIssues = (error, mode) => {
5442
7401
  type: "JsonValue",
5443
7402
  reason: { kind: "Issues", issues: [issue] },
5444
7403
  },
5445
- path: issue.path,
7404
+ path: path.length === 0 ? issue.path : [...path, ...issue.path],
5446
7405
  formatError: ((error) => {
5447
7406
  const issue = error.reason.issues[0];
5448
7407
  switch (issue.kind) {
@@ -5559,7 +7518,12 @@ const stringifyJsonValue = (value) => {
5559
7518
  * @group JSON
5560
7519
  */
5561
7520
  export const JsonValue =
5562
- /*#__PURE__*/ createTypeNode("JsonValue", null, validateJsonValue, (value) => validateJsonValue(value).ok, validateJsonValue, ok, identity, getJsonValueRuntimeTypeIssues);
7521
+ /*#__PURE__*/ createTypeNode("JsonValue", null, validateJsonValue, (value) => validateJsonValue(value).ok, validateJsonValue, ok, identity, getJsonValueRuntimeTypeIssues, {
7522
+ check: (value, options) => {
7523
+ const result = validateJsonValue(value, options);
7524
+ return result.ok ? undefined : result.error;
7525
+ },
7526
+ });
5563
7527
  /**
5564
7528
  * Exact top-level JSON array {@link Type}.
5565
7529
  *
@@ -5695,9 +7659,9 @@ export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Js
5695
7659
  */
5696
7660
  export const json = (type, name, ..._validation) => {
5697
7661
  const runtimeType = type;
5698
- const getTypeIssues = (error, mode) => error.type === name
5699
- ? runtimeType[getRuntimeTypeIssuesSymbol](error.error, mode)
5700
- : Json[getRuntimeTypeIssuesSymbol](error, mode);
7662
+ const getTypeIssues = (error, mode, path) => error.type === name
7663
+ ? runtimeType[getRuntimeTypeIssuesSymbol](error.error, mode, path)
7664
+ : Json[getRuntimeTypeIssuesSymbol](error, mode, path);
5701
7665
  const jsonToTypeOutput = (value, options = firstValidationOptions) => runtimeType.fromUnknown(parseJson(value), options);
5702
7666
  const jsonStringToBrandedJson = (value, options) => {
5703
7667
  const jsonValueResult = jsonToJsonValueResult(value);
@@ -5746,7 +7710,7 @@ export const json = (type, name, ..._validation) => {
5746
7710
  const BrandedJson = {
5747
7711
  ...typeNode,
5748
7712
  from,
5749
- orThrow: mapRuntimeResult(fromInput, getOrThrow),
7713
+ orThrow: createRuntimeOrThrow(fromInput, typeNode.formatError),
5750
7714
  orNull: mapRuntimeResult(fromInput, getOrNull),
5751
7715
  };
5752
7716
  return [