@evolu/common 8.4.0 → 8.6.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 (353) hide show
  1. package/dist/src/Array.d.ts +191 -94
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +91 -40
  4. package/dist/src/Assert.d.ts +233 -30
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +151 -30
  7. package/dist/src/BigInt.d.ts +3 -3
  8. package/dist/src/BigInt.js +3 -3
  9. package/dist/src/Brand.d.ts +7 -7
  10. package/dist/src/Buffer.d.ts +12 -4
  11. package/dist/src/Buffer.d.ts.map +1 -1
  12. package/dist/src/Cache.d.ts +8 -3
  13. package/dist/src/Cache.d.ts.map +1 -1
  14. package/dist/src/Cache.js +8 -3
  15. package/dist/src/Callbacks.d.ts +8 -4
  16. package/dist/src/Callbacks.d.ts.map +1 -1
  17. package/dist/src/Console.d.ts +45 -30
  18. package/dist/src/Console.d.ts.map +1 -1
  19. package/dist/src/Console.js +32 -21
  20. package/dist/src/Crypto.d.ts +13 -8
  21. package/dist/src/Crypto.d.ts.map +1 -1
  22. package/dist/src/Crypto.js +6 -3
  23. package/dist/src/Eq.d.ts +107 -58
  24. package/dist/src/Eq.d.ts.map +1 -1
  25. package/dist/src/Eq.js +308 -112
  26. package/dist/src/Function.d.ts +54 -24
  27. package/dist/src/Function.d.ts.map +1 -1
  28. package/dist/src/Function.js +34 -17
  29. package/dist/src/Http.d.ts +52 -19
  30. package/dist/src/Http.d.ts.map +1 -1
  31. package/dist/src/Identicon.d.ts +9 -4
  32. package/dist/src/Identicon.d.ts.map +1 -1
  33. package/dist/src/Identicon.js +9 -4
  34. package/dist/src/LeakDetector.d.ts.map +1 -1
  35. package/dist/src/LeakDetector.js +4 -1
  36. package/dist/src/LockManager.d.ts +12 -10
  37. package/dist/src/LockManager.d.ts.map +1 -1
  38. package/dist/src/Lookup.d.ts +4 -2
  39. package/dist/src/Lookup.d.ts.map +1 -1
  40. package/dist/src/Lookup.js +5 -2
  41. package/dist/src/Number.d.ts +28 -24
  42. package/dist/src/Number.d.ts.map +1 -1
  43. package/dist/src/Number.js +16 -17
  44. package/dist/src/Object.d.ts +89 -47
  45. package/dist/src/Object.d.ts.map +1 -1
  46. package/dist/src/Object.js +88 -41
  47. package/dist/src/Option.d.ts +14 -5
  48. package/dist/src/Option.d.ts.map +1 -1
  49. package/dist/src/Option.js +14 -5
  50. package/dist/src/Order.d.ts +12 -12
  51. package/dist/src/Order.js +12 -12
  52. package/dist/src/Platform.d.ts +2 -2
  53. package/dist/src/Platform.js +1 -0
  54. package/dist/src/Random.d.ts +7 -4
  55. package/dist/src/Random.d.ts.map +1 -1
  56. package/dist/src/Redacted.d.ts +13 -6
  57. package/dist/src/Redacted.d.ts.map +1 -1
  58. package/dist/src/Redacted.js +4 -2
  59. package/dist/src/Ref.d.ts +4 -4
  60. package/dist/src/Relation.d.ts +5 -7
  61. package/dist/src/Relation.d.ts.map +1 -1
  62. package/dist/src/Relation.js +3 -3
  63. package/dist/src/Resource.d.ts +27 -12
  64. package/dist/src/Resource.d.ts.map +1 -1
  65. package/dist/src/Resource.js +9 -3
  66. package/dist/src/Result.d.ts +258 -114
  67. package/dist/src/Result.d.ts.map +1 -1
  68. package/dist/src/Result.js +74 -39
  69. package/dist/src/Schedule.d.ts +233 -116
  70. package/dist/src/Schedule.d.ts.map +1 -1
  71. package/dist/src/Schedule.js +204 -110
  72. package/dist/src/Set.d.ts +43 -24
  73. package/dist/src/Set.d.ts.map +1 -1
  74. package/dist/src/Set.js +25 -13
  75. package/dist/src/Sqlite.d.ts +4 -4
  76. package/dist/src/Sqlite.d.ts.map +1 -1
  77. package/dist/src/Sqlite.js +18 -15
  78. package/dist/src/Store.d.ts.map +1 -1
  79. package/dist/src/Store.js +3 -1
  80. package/dist/src/String.d.ts.map +1 -1
  81. package/dist/src/String.js +1 -1
  82. package/dist/src/Task.d.ts +555 -264
  83. package/dist/src/Task.d.ts.map +1 -1
  84. package/dist/src/Task.js +278 -168
  85. package/dist/src/Test.d.ts +10 -4
  86. package/dist/src/Test.d.ts.map +1 -1
  87. package/dist/src/Test.js +11 -4
  88. package/dist/src/Time.d.ts +27 -17
  89. package/dist/src/Time.d.ts.map +1 -1
  90. package/dist/src/Time.js +25 -8
  91. package/dist/src/Type.d.ts +1218 -372
  92. package/dist/src/Type.d.ts.map +1 -1
  93. package/dist/src/Type.js +1056 -379
  94. package/dist/src/Types.d.ts +95 -40
  95. package/dist/src/Types.d.ts.map +1 -1
  96. package/dist/src/Types.js +13 -4
  97. package/dist/src/WebSocket.d.ts +14 -6
  98. package/dist/src/WebSocket.d.ts.map +1 -1
  99. package/dist/src/WebSocket.js +8 -0
  100. package/dist/src/Worker.d.ts +10 -6
  101. package/dist/src/Worker.d.ts.map +1 -1
  102. package/dist/src/Worker.js +1 -1
  103. package/dist/src/intl/_en.d.ts +3 -1
  104. package/dist/src/intl/_en.d.ts.map +1 -1
  105. package/dist/src/intl/_en.js +17 -3
  106. package/dist/src/intl/ar.d.ts +3 -1
  107. package/dist/src/intl/ar.d.ts.map +1 -1
  108. package/dist/src/intl/ar.js +15 -2
  109. package/dist/src/intl/bn.d.ts +3 -1
  110. package/dist/src/intl/bn.d.ts.map +1 -1
  111. package/dist/src/intl/bn.js +16 -3
  112. package/dist/src/intl/ca.d.ts +3 -1
  113. package/dist/src/intl/ca.d.ts.map +1 -1
  114. package/dist/src/intl/ca.js +16 -3
  115. package/dist/src/intl/cs.d.ts +3 -1
  116. package/dist/src/intl/cs.d.ts.map +1 -1
  117. package/dist/src/intl/cs.js +16 -3
  118. package/dist/src/intl/da.d.ts +3 -1
  119. package/dist/src/intl/da.d.ts.map +1 -1
  120. package/dist/src/intl/da.js +16 -3
  121. package/dist/src/intl/de.d.ts +3 -1
  122. package/dist/src/intl/de.d.ts.map +1 -1
  123. package/dist/src/intl/de.js +16 -3
  124. package/dist/src/intl/el.d.ts +3 -1
  125. package/dist/src/intl/el.d.ts.map +1 -1
  126. package/dist/src/intl/el.js +16 -3
  127. package/dist/src/intl/es.d.ts +3 -1
  128. package/dist/src/intl/es.d.ts.map +1 -1
  129. package/dist/src/intl/es.js +16 -3
  130. package/dist/src/intl/fa.d.ts +3 -1
  131. package/dist/src/intl/fa.d.ts.map +1 -1
  132. package/dist/src/intl/fa.js +16 -3
  133. package/dist/src/intl/fi.d.ts +3 -1
  134. package/dist/src/intl/fi.d.ts.map +1 -1
  135. package/dist/src/intl/fi.js +16 -3
  136. package/dist/src/intl/fil.d.ts +3 -1
  137. package/dist/src/intl/fil.d.ts.map +1 -1
  138. package/dist/src/intl/fil.js +16 -3
  139. package/dist/src/intl/fr.d.ts +3 -1
  140. package/dist/src/intl/fr.d.ts.map +1 -1
  141. package/dist/src/intl/fr.js +16 -3
  142. package/dist/src/intl/he.d.ts +3 -1
  143. package/dist/src/intl/he.d.ts.map +1 -1
  144. package/dist/src/intl/he.js +16 -3
  145. package/dist/src/intl/hi.d.ts +3 -1
  146. package/dist/src/intl/hi.d.ts.map +1 -1
  147. package/dist/src/intl/hi.js +16 -3
  148. package/dist/src/intl/hr.d.ts +3 -1
  149. package/dist/src/intl/hr.d.ts.map +1 -1
  150. package/dist/src/intl/hr.js +16 -3
  151. package/dist/src/intl/hu.d.ts +2 -1
  152. package/dist/src/intl/hu.d.ts.map +1 -1
  153. package/dist/src/intl/hu.js +15 -3
  154. package/dist/src/intl/id.d.ts +3 -1
  155. package/dist/src/intl/id.d.ts.map +1 -1
  156. package/dist/src/intl/id.js +16 -3
  157. package/dist/src/intl/it.d.ts +3 -1
  158. package/dist/src/intl/it.d.ts.map +1 -1
  159. package/dist/src/intl/it.js +16 -3
  160. package/dist/src/intl/ja.d.ts +3 -1
  161. package/dist/src/intl/ja.d.ts.map +1 -1
  162. package/dist/src/intl/ja.js +16 -3
  163. package/dist/src/intl/ko.d.ts +3 -1
  164. package/dist/src/intl/ko.d.ts.map +1 -1
  165. package/dist/src/intl/ko.js +16 -3
  166. package/dist/src/intl/ml.d.ts +3 -1
  167. package/dist/src/intl/ml.d.ts.map +1 -1
  168. package/dist/src/intl/ml.js +16 -3
  169. package/dist/src/intl/mr.d.ts +3 -1
  170. package/dist/src/intl/mr.d.ts.map +1 -1
  171. package/dist/src/intl/mr.js +16 -3
  172. package/dist/src/intl/ms.d.ts +3 -1
  173. package/dist/src/intl/ms.d.ts.map +1 -1
  174. package/dist/src/intl/ms.js +15 -2
  175. package/dist/src/intl/nb.d.ts +2 -1
  176. package/dist/src/intl/nb.d.ts.map +1 -1
  177. package/dist/src/intl/nb.js +14 -2
  178. package/dist/src/intl/nl.d.ts +3 -1
  179. package/dist/src/intl/nl.d.ts.map +1 -1
  180. package/dist/src/intl/nl.js +16 -3
  181. package/dist/src/intl/pa.d.ts +3 -1
  182. package/dist/src/intl/pa.d.ts.map +1 -1
  183. package/dist/src/intl/pa.js +16 -3
  184. package/dist/src/intl/pl.d.ts +2 -0
  185. package/dist/src/intl/pl.d.ts.map +1 -1
  186. package/dist/src/intl/pl.js +15 -2
  187. package/dist/src/intl/pt-BR.d.ts +3 -1
  188. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  189. package/dist/src/intl/pt-BR.js +16 -3
  190. package/dist/src/intl/pt.d.ts +3 -1
  191. package/dist/src/intl/pt.d.ts.map +1 -1
  192. package/dist/src/intl/pt.js +15 -2
  193. package/dist/src/intl/ro.d.ts +3 -1
  194. package/dist/src/intl/ro.d.ts.map +1 -1
  195. package/dist/src/intl/ro.js +16 -3
  196. package/dist/src/intl/sk.d.ts +3 -1
  197. package/dist/src/intl/sk.d.ts.map +1 -1
  198. package/dist/src/intl/sk.js +15 -2
  199. package/dist/src/intl/sl.d.ts +3 -1
  200. package/dist/src/intl/sl.d.ts.map +1 -1
  201. package/dist/src/intl/sl.js +16 -3
  202. package/dist/src/intl/sv.d.ts +3 -1
  203. package/dist/src/intl/sv.d.ts.map +1 -1
  204. package/dist/src/intl/sv.js +16 -3
  205. package/dist/src/intl/sw.d.ts +1 -0
  206. package/dist/src/intl/sw.d.ts.map +1 -1
  207. package/dist/src/intl/sw.js +14 -2
  208. package/dist/src/intl/ta.d.ts +3 -1
  209. package/dist/src/intl/ta.d.ts.map +1 -1
  210. package/dist/src/intl/ta.js +16 -3
  211. package/dist/src/intl/te.d.ts +3 -1
  212. package/dist/src/intl/te.d.ts.map +1 -1
  213. package/dist/src/intl/te.js +16 -3
  214. package/dist/src/intl/th.d.ts +3 -1
  215. package/dist/src/intl/th.d.ts.map +1 -1
  216. package/dist/src/intl/th.js +16 -3
  217. package/dist/src/intl/tr.d.ts +3 -1
  218. package/dist/src/intl/tr.d.ts.map +1 -1
  219. package/dist/src/intl/tr.js +16 -3
  220. package/dist/src/intl/uk.d.ts +3 -1
  221. package/dist/src/intl/uk.d.ts.map +1 -1
  222. package/dist/src/intl/uk.js +16 -3
  223. package/dist/src/intl/ur.d.ts +3 -1
  224. package/dist/src/intl/ur.d.ts.map +1 -1
  225. package/dist/src/intl/ur.js +15 -2
  226. package/dist/src/intl/vi.d.ts +3 -1
  227. package/dist/src/intl/vi.d.ts.map +1 -1
  228. package/dist/src/intl/vi.js +16 -3
  229. package/dist/src/intl/zh-CN.d.ts +3 -1
  230. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  231. package/dist/src/intl/zh-CN.js +16 -3
  232. package/dist/src/intl/zh-TW.d.ts +3 -1
  233. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  234. package/dist/src/intl/zh-TW.js +16 -3
  235. package/dist/src/local-first/Db.js +3 -2
  236. package/dist/src/local-first/Evolu.d.ts +47 -24
  237. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  238. package/dist/src/local-first/Evolu.js +1 -0
  239. package/dist/src/local-first/Owner.d.ts +18 -14
  240. package/dist/src/local-first/Owner.d.ts.map +1 -1
  241. package/dist/src/local-first/Owner.js +14 -11
  242. package/dist/src/local-first/Protocol.d.ts +5 -5
  243. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  244. package/dist/src/local-first/Protocol.js +35 -29
  245. package/dist/src/local-first/Query.d.ts +34 -21
  246. package/dist/src/local-first/Query.d.ts.map +1 -1
  247. package/dist/src/local-first/Query.js +27 -14
  248. package/dist/src/local-first/Relay.d.ts +12 -5
  249. package/dist/src/local-first/Relay.d.ts.map +1 -1
  250. package/dist/src/local-first/Relay.js +1 -1
  251. package/dist/src/local-first/Schema.d.ts +19 -11
  252. package/dist/src/local-first/Schema.d.ts.map +1 -1
  253. package/dist/src/local-first/Schema.js +11 -6
  254. package/dist/src/local-first/Shared.d.ts.map +1 -1
  255. package/dist/src/local-first/Shared.js +4 -2
  256. package/dist/src/local-first/Storage.d.ts +4 -2
  257. package/dist/src/local-first/Storage.d.ts.map +1 -1
  258. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  259. package/dist/src/local-first/Timestamp.js +1 -1
  260. package/package.json +3 -4
  261. package/src/Array.ts +195 -95
  262. package/src/Assert.ts +337 -31
  263. package/src/BigInt.ts +3 -3
  264. package/src/Brand.ts +7 -7
  265. package/src/Buffer.ts +12 -4
  266. package/src/Cache.ts +8 -3
  267. package/src/Callbacks.ts +8 -4
  268. package/src/Console.ts +45 -30
  269. package/src/Crypto.ts +13 -8
  270. package/src/Eq.ts +450 -122
  271. package/src/Function.ts +54 -24
  272. package/src/Http.ts +52 -19
  273. package/src/Identicon.ts +9 -4
  274. package/src/LeakDetector.ts +4 -3
  275. package/src/LockManager.ts +12 -10
  276. package/src/Lookup.ts +5 -2
  277. package/src/Number.ts +28 -24
  278. package/src/Object.ts +115 -49
  279. package/src/Option.ts +14 -5
  280. package/src/Order.ts +12 -12
  281. package/src/Platform.ts +3 -2
  282. package/src/Random.ts +7 -4
  283. package/src/Redacted.ts +13 -6
  284. package/src/Ref.ts +4 -4
  285. package/src/Relation.ts +8 -10
  286. package/src/Resource.ts +36 -15
  287. package/src/Result.ts +263 -119
  288. package/src/Schedule.ts +233 -116
  289. package/src/Set.ts +43 -24
  290. package/src/Sqlite.ts +18 -15
  291. package/src/Store.ts +3 -1
  292. package/src/String.ts +1 -1
  293. package/src/Task.ts +566 -286
  294. package/src/Test.ts +11 -4
  295. package/src/Time.ts +36 -19
  296. package/src/Type.ts +2191 -590
  297. package/src/Types.ts +108 -40
  298. package/src/WebSocket.ts +22 -6
  299. package/src/Worker.ts +11 -7
  300. package/src/intl/_en.ts +20 -4
  301. package/src/intl/ar.ts +17 -2
  302. package/src/intl/bn.ts +19 -4
  303. package/src/intl/ca.ts +19 -4
  304. package/src/intl/cs.ts +19 -4
  305. package/src/intl/da.ts +19 -4
  306. package/src/intl/de.ts +19 -4
  307. package/src/intl/el.ts +19 -4
  308. package/src/intl/es.ts +19 -4
  309. package/src/intl/fa.ts +19 -4
  310. package/src/intl/fi.ts +19 -4
  311. package/src/intl/fil.ts +19 -4
  312. package/src/intl/fr.ts +19 -4
  313. package/src/intl/he.ts +19 -4
  314. package/src/intl/hi.ts +19 -4
  315. package/src/intl/hr.ts +19 -4
  316. package/src/intl/hu.ts +18 -4
  317. package/src/intl/id.ts +19 -4
  318. package/src/intl/it.ts +19 -4
  319. package/src/intl/ja.ts +19 -4
  320. package/src/intl/ko.ts +19 -4
  321. package/src/intl/ml.ts +19 -4
  322. package/src/intl/mr.ts +19 -4
  323. package/src/intl/ms.ts +17 -2
  324. package/src/intl/nb.ts +16 -2
  325. package/src/intl/nl.ts +19 -4
  326. package/src/intl/pa.ts +19 -4
  327. package/src/intl/pl.ts +18 -2
  328. package/src/intl/pt-BR.ts +19 -4
  329. package/src/intl/pt.ts +17 -2
  330. package/src/intl/ro.ts +19 -4
  331. package/src/intl/sk.ts +17 -2
  332. package/src/intl/sl.ts +19 -4
  333. package/src/intl/sv.ts +19 -4
  334. package/src/intl/sw.ts +15 -2
  335. package/src/intl/ta.ts +19 -4
  336. package/src/intl/te.ts +19 -4
  337. package/src/intl/th.ts +19 -4
  338. package/src/intl/tr.ts +19 -4
  339. package/src/intl/uk.ts +19 -4
  340. package/src/intl/ur.ts +17 -2
  341. package/src/intl/vi.ts +19 -3
  342. package/src/intl/zh-CN.ts +19 -4
  343. package/src/intl/zh-TW.ts +19 -4
  344. package/src/local-first/Db.ts +2 -1
  345. package/src/local-first/Evolu.ts +48 -24
  346. package/src/local-first/Owner.ts +18 -14
  347. package/src/local-first/Protocol.ts +45 -35
  348. package/src/local-first/Query.ts +40 -24
  349. package/src/local-first/Relay.ts +13 -6
  350. package/src/local-first/Schema.ts +19 -11
  351. package/src/local-first/Shared.ts +4 -2
  352. package/src/local-first/Storage.ts +4 -2
  353. package/src/local-first/Timestamp.ts +1 -1
package/src/Type.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  /**
2
- * ## Intro
3
- *
4
2
  * Runtime validation with precise TypeScript types and structured errors.
5
3
  *
6
4
  * Evolu {@link Type} is a pure, synchronous codec for defining semantic domains.
@@ -49,6 +47,11 @@
49
47
  *
50
48
  * ```ts
51
49
  * import {
50
+ * assertEqual,
51
+ * Data,
52
+ * assertErr,
53
+ * assertOk,
54
+ * assertType,
52
55
  * Number,
53
56
  * NonEmptyTrimmedString100,
54
57
  * brand,
@@ -73,14 +76,15 @@
73
76
  * const Age = brand("Age", lessThan(200)(NonNegativeInt));
74
77
  * type Age = typeof Age.Output;
75
78
  *
76
- * expectTypeOf<Age>().toEqualTypeOf<
79
+ * assertType<
77
80
  * number &
78
81
  * Brand<"NonNaN"> &
79
82
  * Brand<"Finite"> &
80
83
  * Brand<"Int"> &
81
84
  * Brand<"NonNegative"> &
82
85
  * Brand<"LessThan200"> &
83
- * Brand<"Age">
86
+ * Brand<"Age">,
87
+ * Age
84
88
  * >();
85
89
  *
86
90
  * const User = object({
@@ -92,12 +96,16 @@
92
96
  * const value: unknown = { name: "Ada", age: 37 };
93
97
  * const user = User.fromUnknown(value);
94
98
  *
95
- * expectOk(user, { name: "Ada", age: 37 });
96
- * expectTypeOf(user.value).toExtend<User>();
99
+ * assertOk(user, { name: "Ada", age: 37 });
100
+ * assertType<typeof User.Output, typeof user.value>();
97
101
  *
98
102
  * const invalidUser = User.fromUnknown({ name: "Ada", age: 37.5 });
99
103
  *
100
- * expectErr(invalidUser, {
104
+ * assertErr(invalidUser);
105
+ * // InferErrors includes every structured error User.fromUnknown can return.
106
+ * assertType<InferErrors<typeof User>, typeof invalidUser.error>();
107
+ * assertType(Data, invalidUser.error);
108
+ * assertEqual(invalidUser.error, {
101
109
  * type: "Object",
102
110
  * reason: {
103
111
  * kind: "Properties",
@@ -106,22 +114,26 @@
106
114
  * },
107
115
  * },
108
116
  * });
109
- *
110
- * // InferErrors includes every structured error User.fromUnknown can return.
111
- * expectTypeOf(invalidUser.error).toEqualTypeOf<
112
- * InferErrors<typeof User>
113
- * >();
114
117
  * ```
115
118
  *
116
119
  * A Type can format its structured errors into user-facing messages:
117
120
  *
118
121
  * ```ts
119
- * import { Age } from "@evolu/common";
122
+ * import {
123
+ * assertEqual,
124
+ * assertType,
125
+ * Data,
126
+ * assertErr,
127
+ * Age,
128
+ * } from "@evolu/common";
120
129
  *
121
130
  * const age = Age.fromUnknown(37.5);
122
131
  *
123
- * expectErr(age, { type: "Int", value: 37.5 });
124
- * expect(Age.formatError(age.error)).toBe(
132
+ * assertErr(age);
133
+ * assertType(Data, age.error);
134
+ * assertEqual(age.error, { type: "Int", value: 37.5 });
135
+ * assertEqual(
136
+ * Age.formatError(age.error),
125
137
  * "The value 37.5 must be a safe integer.",
126
138
  * );
127
139
  * ```
@@ -138,6 +150,8 @@
138
150
  *
139
151
  * ```ts
140
152
  * import {
153
+ * assertOk,
154
+ * assertType,
141
155
  * NonEmptyTrimmedString100,
142
156
  * NonEmptyTrimmedString1000,
143
157
  * object,
@@ -152,7 +166,7 @@
152
166
  *
153
167
  * // This is type-checked: Todo.from expects NonEmptyTrimmedString100.
154
168
  * const title = NonEmptyTrimmedString100.orThrow("Buy milk");
155
- * expectOk(Todo.from({ title }), { title });
169
+ * assertOk(Todo.from({ title }), { title });
156
170
  *
157
171
  * // Imagine the UI input component is changed to allow longer titles.
158
172
  * // TypeScript rejects the mismatch, so users never see a save error
@@ -170,13 +184,14 @@
170
184
  * );
171
185
  *
172
186
  * // No "not a string" or "not trimmed" errors: the input guarantees both.
173
- * expectTypeOf(validatedTitle).toEqualTypeOf<
187
+ * assertType<
174
188
  * Result<
175
189
  * NonEmptyTrimmedString100,
176
190
  * MaxLengthError<100> | MinLengthError<1>
177
- * >
191
+ * >,
192
+ * typeof validatedTitle
178
193
  * >();
179
- * expectOk(validatedTitle, "Buy milk");
194
+ * assertOk(validatedTitle, "Buy milk");
180
195
  * ```
181
196
  *
182
197
  * Evolu includes dozens of predefined Types and Type factories. Use Types such
@@ -203,6 +218,10 @@
203
218
  * cannot express. They are correctness checks, not defenses against malicious
204
219
  * code.
205
220
  *
221
+ * Evolu does not support subclassing native JavaScript objects. Such subclasses
222
+ * can be classified as their reported built-in representation, but their
223
+ * behavior is unspecified.
224
+ *
206
225
  * ## FAQ
207
226
  *
208
227
  * ### What does a Type represent?
@@ -378,16 +397,19 @@
378
397
  * unknown values, {@link object} defines a closed plain-object representation,
379
398
  * and {@link record} defines a plain-dictionary representation whose complete
380
399
  * set of own properties are its entries. Their realm-neutral plain-object rule
381
- * accepts a `null` prototype or an immediate root prototype whose own prototype
382
- * is `null`; ordinary class instances and deeper prototype chains are rejected.
383
- * Every property must be an enumerable data property; inherited members are not
384
- * entries, while accessors and hidden properties are invalid instead of being
385
- * invoked or ignored. {@link array} similarly defines a dense sequence whose
386
- * only own properties are `length` and its indexed data properties;
387
- * {@link tuple} applies the same representation rules with a fixed length and a
388
- * distinct Type for each position. Only an explicit {@link transform} changes
389
- * the representation. Consequently, `is` tests exact Output membership and `to`
390
- * stays total for valid Outputs.
400
+ * uses {@link isPlainObject}: it accepts a `null` prototype or an immediate root
401
+ * prototype with own `hasOwnProperty` and `isPrototypeOf` properties. A custom
402
+ * root prototype with the same shape can therefore be classified as plain;
403
+ * other custom prototypes and class instances are rejected. This heuristic
404
+ * assumes trusted JavaScript and is not a security boundary. Every property
405
+ * must be an enumerable data property; inherited members are not entries, while
406
+ * accessors and hidden properties are invalid instead of being invoked or
407
+ * ignored. {@link array} similarly defines a dense sequence whose only own
408
+ * properties are `length` and its indexed data properties; {@link tuple} applies
409
+ * the same representation rules with a fixed length and a distinct Type for
410
+ * each position. Only an explicit {@link transform} changes the representation.
411
+ * Consequently, `is` tests exact Output membership and `to` stays total for
412
+ * valid Outputs.
391
413
  *
392
414
  * ### Why is JsonValue stricter than JSON.stringify?
393
415
  *
@@ -432,15 +454,16 @@ import { sha256 } from "@noble/hashes/sha2.js";
432
454
  import * as bip39 from "@scure/bip39";
433
455
  import { wordlist } from "@scure/bip39/wordlists/english.js";
434
456
  import type { StandardSchemaV1 } from "@standard-schema/spec";
435
- import type {
436
- AtLeastTwoReadonlyArray,
437
- NonEmptyReadonlyArray,
457
+ import {
458
+ createMutableArray,
459
+ type AtLeastTwoReadonlyArray,
460
+ type NonEmptyReadonlyArray,
438
461
  } from "./Array.ts";
439
462
  import { assert, assertNonNullable } from "./Assert.ts";
440
463
  import type { Brand } from "./Brand.ts";
441
464
  import type { RandomBytesDep } from "./Crypto.ts";
442
465
  import { identity, type Thunk } from "./Function.ts";
443
- import { createMutableRecord } from "./Object.ts";
466
+ import { createMutableRecord, getObjectKind, isPlainObject } from "./Object.ts";
444
467
  import { hasNodeBuffer } from "./Platform.ts";
445
468
  import {
446
469
  err,
@@ -460,6 +483,7 @@ import {
460
483
  isInstance,
461
484
  type CompileTimeError,
462
485
  type Instance,
486
+ type IsSameType,
463
487
  type IsUnion,
464
488
  type Literal,
465
489
  type Simplify,
@@ -571,12 +595,12 @@ export interface Type<
571
595
  * ### Example
572
596
  *
573
597
  * ```ts
574
- * import { PositiveInt } from "@evolu/common";
598
+ * import { assertOk, PositiveInt } from "@evolu/common";
575
599
  *
576
600
  * const value: unknown = 42;
577
601
  * const result = PositiveInt.fromUnknown(value);
578
602
  *
579
- * expectOk(result, 42);
603
+ * assertOk(result, 42);
580
604
  * ```
581
605
  */
582
606
  readonly fromUnknown: (
@@ -596,12 +620,25 @@ export interface Type<
596
620
  * ### Example
597
621
  *
598
622
  * ```ts
599
- * import { String } from "@evolu/common";
623
+ * import {
624
+ * assertEqual,
625
+ * assertType,
626
+ * Data,
627
+ * assertErr,
628
+ * String,
629
+ * } from "@evolu/common";
600
630
  *
601
631
  * const result = String.fromUnknown(42);
602
632
  *
603
- * expectErr(result, { type: "TypeOf", expected: "String", value: 42 });
604
- * expect(String.formatError(result.error)).toBe(
633
+ * assertErr(result);
634
+ * assertType(Data, result.error);
635
+ * assertEqual(result.error, {
636
+ * type: "TypeOf",
637
+ * expected: "String",
638
+ * value: 42,
639
+ * });
640
+ * assertEqual(
641
+ * String.formatError(result.error),
605
642
  * "A value 42 is not a string.",
606
643
  * );
607
644
  * ```
@@ -618,14 +655,20 @@ export interface Type<
618
655
  * ### Example
619
656
  *
620
657
  * ```ts
621
- * import { Int64FromInt64String, type Int64 } from "@evolu/common";
658
+ * import {
659
+ * assertFalse,
660
+ * assertType,
661
+ * assertTrue,
662
+ * Int64FromInt64String,
663
+ * type Int64,
664
+ * } from "@evolu/common";
622
665
  *
623
666
  * const values: ReadonlyArray<unknown> = [42n, "42", null];
624
667
  * const integers = values.filter(Int64FromInt64String.is);
625
668
  *
626
- * expectTypeOf(integers).toEqualTypeOf<globalThis.Array<Int64>>();
627
- * expect(Int64FromInt64String.is(42n)).toBe(true);
628
- * expect(Int64FromInt64String.is("42")).toBe(false);
669
+ * assertType<Array<Int64>, typeof integers>();
670
+ * assertTrue(Int64FromInt64String.is(42n));
671
+ * assertFalse(Int64FromInt64String.is("42"));
629
672
  * ```
630
673
  */
631
674
  readonly is: (value: unknown) => value is Output;
@@ -651,6 +694,8 @@ export interface Type<
651
694
  *
652
695
  * ```ts
653
696
  * import {
697
+ * assertOk,
698
+ * assertType,
654
699
  * flatMapResult,
655
700
  * NonEmptyTrimmedString100,
656
701
  * object,
@@ -683,10 +728,11 @@ export interface Type<
683
728
  * const note = TrimmedString.orThrow("Remember oat milk");
684
729
  * const result = saveTodo(title, note);
685
730
  *
686
- * expectTypeOf(result).toEqualTypeOf<
687
- * Result<typeof Todo.Output, MaxLengthError<100> | MinLengthError<1>>
731
+ * assertType<
732
+ * Result<typeof Todo.Output, MaxLengthError<100> | MinLengthError<1>>,
733
+ * typeof result
688
734
  * >();
689
- * expectOk(result, { title, note });
735
+ * assertOk(result, { title, note });
690
736
  * ```
691
737
  */
692
738
  readonly from: [CustomFrom] extends [never]
@@ -706,11 +752,15 @@ export interface Type<
706
752
  * ### Example
707
753
  *
708
754
  * ```ts
709
- * import { Int64, Int64FromInt64String } from "@evolu/common";
755
+ * import {
756
+ * assertEqual,
757
+ * Int64,
758
+ * Int64FromInt64String,
759
+ * } from "@evolu/common";
710
760
  *
711
761
  * const value = Int64.orThrow(42n);
712
762
  *
713
- * expect(Int64FromInt64String.to(value)).toBe("42");
763
+ * assertEqual(Int64FromInt64String.to(value), "42");
714
764
  * ```
715
765
  */
716
766
  readonly to: [Parent] extends [infer P extends TypeNode]
@@ -736,7 +786,12 @@ export interface Type<
736
786
  * ### Example
737
787
  *
738
788
  * ```ts
739
- * import { getOrThrow, minLength, String } from "@evolu/common";
789
+ * import {
790
+ * assertEqual,
791
+ * getOrThrow,
792
+ * minLength,
793
+ * String,
794
+ * } from "@evolu/common";
740
795
  *
741
796
  * const NonEmptyString = minLength(1)(String);
742
797
  *
@@ -745,8 +800,8 @@ export interface Type<
745
800
  * // Equivalent because `from.parent` is this Type's deepest `from` operation:
746
801
  * const sameValue = getOrThrow(NonEmptyString.from.parent("Evolu"));
747
802
  *
748
- * expect(value).toBe("Evolu");
749
- * expect(sameValue).toBe(value);
803
+ * assertEqual(value, "Evolu");
804
+ * assertEqual(sameValue, value);
750
805
  * ```
751
806
  */
752
807
  readonly orThrow: TypeOperationFn<"orThrow", Input, Output, never>;
@@ -770,7 +825,12 @@ export interface Type<
770
825
  * ### Example
771
826
  *
772
827
  * ```ts
773
- * import { getOrNull, minLength, String } from "@evolu/common";
828
+ * import {
829
+ * assertEqual,
830
+ * getOrNull,
831
+ * minLength,
832
+ * String,
833
+ * } from "@evolu/common";
774
834
  *
775
835
  * const NonEmptyString = minLength(1)(String);
776
836
  *
@@ -779,9 +839,9 @@ export interface Type<
779
839
  * // Equivalent because `from.parent` is this Type's deepest `from` operation:
780
840
  * const sameValue = getOrNull(NonEmptyString.from.parent("Evolu"));
781
841
  *
782
- * expect(value).toBe("Evolu");
783
- * expect(sameValue).toBe(value);
784
- * expect(NonEmptyString.orNull("")).toBeNull();
842
+ * assertEqual(value, "Evolu");
843
+ * assertEqual(sameValue, value);
844
+ * assertEqual(NonEmptyString.orNull(""), null);
785
845
  * ```
786
846
  */
787
847
  readonly orNull: TypeOperationFn<"orNull", Input, Output, never>;
@@ -975,12 +1035,18 @@ const formatDefaultRuntimeTypeIssue: RuntimeFormatTypeIssue = (issue) =>
975
1035
  issue.formatError(issue.error);
976
1036
 
977
1037
  /**
978
- * Asserts that a value belongs to a {@link Type} Output domain.
1038
+ * Asserts exact compile-time type equality or that a value belongs to a
1039
+ * {@link Type} Output domain.
979
1040
  *
980
- * Use this for internal invariants, not external input. Validate external input
981
- * with `Type.fromUnknown` so validation failures remain typed values. A failed
982
- * assertion uses the Type name for its message and preserves the exact Output
983
- * validation error as the thrown Error's cause.
1041
+ * - `assertType<Expected, Actual>()` requires compiler-identical types without
1042
+ * evaluating a value.
1043
+ * - `assertType(type, value)` validates and narrows a runtime value to the Type's
1044
+ * Output.
1045
+ *
1046
+ * Use the runtime form for internal invariants, not external input. Validate
1047
+ * external input with `Type.fromUnknown` so validation failures remain typed
1048
+ * values. A failed runtime assertion uses the Type name for its message and
1049
+ * preserves the exact Output validation error as the thrown Error's cause.
984
1050
  *
985
1051
  * ### Example
986
1052
  *
@@ -993,17 +1059,37 @@ const formatDefaultRuntimeTypeIssue: RuntimeFormatTypeIssue = (issue) =>
993
1059
  *
994
1060
  * const value: unknown = "Evolu";
995
1061
  * assertType(NonEmptyTrimmedString100, value);
996
- * expectTypeOf(value).toEqualTypeOf<
997
- * string & Brand<"Trimmed"> & Brand<"MinLength1"> & Brand<"MaxLength100">
1062
+ * assertType<
1063
+ * string &
1064
+ * Brand<"Trimmed"> &
1065
+ * Brand<"MinLength1"> &
1066
+ * Brand<"MaxLength100">,
1067
+ * typeof value
998
1068
  * >();
999
1069
  * ```
1000
1070
  *
1001
1071
  * @group Core
1002
1072
  */
1003
- export const assertType: <T extends TypeNode>(
1073
+ export function assertType<Expected, Actual>(
1074
+ ...error: IsSameType<Expected, Actual> extends true
1075
+ ? []
1076
+ : [
1077
+ error: CompileTimeError<
1078
+ "assertType",
1079
+ "Expected and actual types must be identical"
1080
+ >,
1081
+ ]
1082
+ ): void;
1083
+ export function assertType<T extends TypeNode>(
1004
1084
  type: T,
1005
1085
  value: unknown,
1006
- ) => asserts value is T["Output"] = (type, value) => {
1086
+ ): asserts value is T["Output"];
1087
+ export function assertType<T extends TypeNode>(
1088
+ type?: T,
1089
+ value?: unknown,
1090
+ ): void {
1091
+ if (type === undefined) return;
1092
+
1007
1093
  // TODO: Make assert prepend "Expected " and accept an optional third cause
1008
1094
  // argument. Then use it here and migrate every other assertion to pass an
1009
1095
  // expectation fragment.
@@ -1013,7 +1099,7 @@ export const assertType: <T extends TypeNode>(
1013
1099
  if (!result.ok) {
1014
1100
  throw new Error(`Expected ${runtimeType.name}.`, { cause: result.error });
1015
1101
  }
1016
- };
1102
+ }
1017
1103
 
1018
1104
  const assertTypeOutput = <Error extends TypeError>(
1019
1105
  name: TypeName,
@@ -1058,13 +1144,22 @@ const assertTypeOutput = <Error extends TypeError>(
1058
1144
  * every locale an app supports also allows language changes without a network
1059
1145
  * connection.
1060
1146
  *
1061
- * The selected Type map, locale map, and formatter maps must be plain objects
1062
- * with own enumerable string-keyed data properties.
1147
+ * The selected Type map, locale map, and formatter maps must satisfy the
1148
+ * realm-neutral structural heuristic described by {@link isPlainObject} and have
1149
+ * own enumerable string-keyed data properties.
1063
1150
  *
1064
1151
  * ### Example
1065
1152
  *
1066
1153
  * ```ts
1067
- * import { String, localizeTypes, minLength } from "@evolu/common";
1154
+ * import {
1155
+ * assertEqual,
1156
+ * Data,
1157
+ * assertErr,
1158
+ * assertType,
1159
+ * String,
1160
+ * localizeTypes,
1161
+ * minLength,
1162
+ * } from "@evolu/common";
1068
1163
  * import { cs } from "@evolu/common/intl";
1069
1164
  *
1070
1165
  * const Label = minLength(1)(String);
@@ -1079,13 +1174,14 @@ const assertTypeOutput = <Error extends TypeError>(
1079
1174
  * },
1080
1175
  * );
1081
1176
  *
1082
- * expectTypeOf<typeof typesByLocale.cs.Label>().toEqualTypeOf<
1083
- * typeof Label
1084
- * >();
1177
+ * assertType<typeof Label, typeof typesByLocale.cs.Label>();
1085
1178
  *
1086
1179
  * const result = typesByLocale.cs.Label.fromUnknown("");
1087
- * expectErr(result, { type: "MinLength1", min: 1, value: "" });
1088
- * expect(typesByLocale.cs.Label.formatError(result.error)).toBe(
1180
+ * assertErr(result);
1181
+ * assertType(Data, result.error);
1182
+ * assertEqual(result.error, { type: "MinLength1", min: 1, value: "" });
1183
+ * assertEqual(
1184
+ * typesByLocale.cs.Label.formatError(result.error),
1089
1185
  * "Text nesmí být prázdný.",
1090
1186
  * );
1091
1187
  * ```
@@ -1199,12 +1295,7 @@ export const localizeTypes = ((
1199
1295
  const getLocalizationMapKeys = (value: unknown): ReadonlyArray<string> => {
1200
1296
  const errorMessage =
1201
1297
  "localizeTypes maps must be plain objects with own enumerable string-keyed data properties.";
1202
- assert(value !== null && typeof value === "object", errorMessage);
1203
- const prototype: unknown = globalThis.Object.getPrototypeOf(value);
1204
- assert(
1205
- prototype === null || globalThis.Object.getPrototypeOf(prototype) === null,
1206
- errorMessage,
1207
- );
1298
+ assert(isPlainObject(value), errorMessage);
1208
1299
 
1209
1300
  const keys = Reflect.ownKeys(value);
1210
1301
  for (const key of keys) {
@@ -1297,14 +1388,12 @@ const localizeTypeReflection = (
1297
1388
  localizedTypeBySource,
1298
1389
  );
1299
1390
  }
1300
- if (globalThis.Array.isArray(value)) {
1391
+ if (Array.isArray(value)) {
1301
1392
  return (value as ReadonlyArray<unknown>).map((value) =>
1302
1393
  localizeTypeReflection(value, formatIssue, localizedTypeBySource),
1303
1394
  );
1304
1395
  }
1305
- if (value === null || typeof value !== "object" || !isPlainObject(value)) {
1306
- return value;
1307
- }
1396
+ if (!isPlainObject(value)) return value;
1308
1397
 
1309
1398
  const localized = globalThis.Object.create(
1310
1399
  globalThis.Object.getPrototypeOf(value) as object | null,
@@ -1349,9 +1438,8 @@ type ValidateFormatErrorByTypeByLocale<
1349
1438
  ? unknown
1350
1439
  : never;
1351
1440
 
1352
- /* eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style --
1353
- This mapped validation type is cheaper than the equivalent Record. Changes are
1354
- measured by `pnpm bench:type`. */
1441
+ // This mapped validation type is cheaper than the equivalent Record. Changes
1442
+ // are measured by `pnpm bench:type`.
1355
1443
  type NoNonStringKeys<Value> = {
1356
1444
  readonly [Key in Exclude<keyof Value, string>]: never;
1357
1445
  };
@@ -1390,7 +1478,7 @@ type LocalizedErrorEntry<
1390
1478
  ? LocalizedDiscriminatedUnionErrorEntry<Error, Seen | Error>
1391
1479
  : Error extends {
1392
1480
  readonly type: infer Name extends
1393
- "Array" | "Tuple" | "Record" | "Set";
1481
+ "Array" | "Map" | "Tuple" | "Record" | "Set";
1394
1482
  readonly reason: infer Reason;
1395
1483
  }
1396
1484
  ? | LocalizedErrorEntryValue<Name, Error>
@@ -1492,6 +1580,8 @@ declare const identityEncodingSymbol: unique symbol;
1492
1580
  *
1493
1581
  * ```ts
1494
1582
  * import {
1583
+ * assertEqual,
1584
+ * assertType,
1495
1585
  * NonEmptyTrimmedString100,
1496
1586
  * PositiveInt,
1497
1587
  * object,
@@ -1507,8 +1597,8 @@ declare const identityEncodingSymbol: unique symbol;
1507
1597
  *
1508
1598
  * const user = User.orThrow({ name: "Ada", age: 37 });
1509
1599
  *
1510
- * expectTypeOf(user).toExtend<User>();
1511
- * expect(user.name).toBe("Ada");
1600
+ * assertType<typeof User.Output, typeof user>();
1601
+ * assertEqual(user.name, "Ada");
1512
1602
  * ```
1513
1603
  *
1514
1604
  * @group Core
@@ -1847,7 +1937,12 @@ type ConcreteChildTypeNameError = CompileTimeError<
1847
1937
  *
1848
1938
  * ```ts
1849
1939
  * import {
1940
+ * assertEqual,
1941
+ * assertErr,
1942
+ * assertOk,
1943
+ * assertType,
1850
1944
  * createType,
1945
+ * Data,
1851
1946
  * err,
1852
1947
  * ok,
1853
1948
  * type Result,
@@ -1865,8 +1960,11 @@ type ConcreteChildTypeNameError = CompileTimeError<
1865
1960
  * () => "Expected text.",
1866
1961
  * );
1867
1962
  *
1868
- * expectOk(Text.fromUnknown("Evolu"), "Evolu");
1869
- * expectErr(Text.fromUnknown(42), { type: "Text", value: 42 });
1963
+ * assertOk(Text.fromUnknown("Evolu"), "Evolu");
1964
+ * const invalid = Text.fromUnknown(42);
1965
+ * assertErr(invalid);
1966
+ * assertType(Data, invalid.error);
1967
+ * assertEqual(invalid.error, { type: "Text", value: 42 });
1870
1968
  * ```
1871
1969
  *
1872
1970
  * @group Construction
@@ -1891,6 +1989,7 @@ export function createType<
1891
1989
  parent: ValidateParent<ParentType>,
1892
1990
  fromParent: (
1893
1991
  value: ParentType["Output"],
1992
+ // oxlint-disable-next-line typescript/no-unnecessary-type-arguments -- Explicit defaults in recursive Type declarations are benchmarked to avoid extra compiler work.
1894
1993
  ) => Result<ParentType["Output"], never>,
1895
1994
  ): Type<
1896
1995
  Name,
@@ -2148,7 +2247,15 @@ const createChildType = <
2148
2247
  * ### Example
2149
2248
  *
2150
2249
  * ```ts
2151
- * import { Boolean, literal, ok, transform, union } from "@evolu/common";
2250
+ * import {
2251
+ * assertEqual,
2252
+ * assertOk,
2253
+ * Boolean,
2254
+ * literal,
2255
+ * ok,
2256
+ * transform,
2257
+ * union,
2258
+ * } from "@evolu/common";
2152
2259
  *
2153
2260
  * const BooleanString = union(literal("false"), literal("true"));
2154
2261
  * const BooleanFromString = transform(
@@ -2161,8 +2268,8 @@ const createChildType = <
2161
2268
  * },
2162
2269
  * );
2163
2270
  *
2164
- * expectOk(BooleanFromString.from.parent("true"), true);
2165
- * expect(BooleanFromString.to(false)).toBe("false");
2271
+ * assertOk(BooleanFromString.from.parent("true"), true);
2272
+ * assertEqual(BooleanFromString.to(false), "false");
2166
2273
  * ```
2167
2274
  *
2168
2275
  * @group Construction
@@ -2179,12 +2286,66 @@ export function transform<
2179
2286
  operations: {
2180
2287
  readonly from: (
2181
2288
  value: ParentType["Output"],
2289
+ // oxlint-disable-next-line typescript/no-unnecessary-type-arguments -- Explicit defaults in recursive Type declarations are benchmarked to avoid extra compiler work.
2182
2290
  ) => Result<OutputType["Input"], never>;
2183
2291
  readonly to: (value: CanonicalInputOf<OutputType>) => ToOutput;
2184
2292
  },
2185
2293
  ): TransformType<ParentType, OutputType, Name, never, ToOutput>;
2186
2294
 
2187
- /** Creates a fallible transformed Type with its own error formatter. */
2295
+ /**
2296
+ * Creates a fallible transformed Type with its own error formatter.
2297
+ *
2298
+ * ### Example
2299
+ *
2300
+ * ```ts
2301
+ * import {
2302
+ * assertEqual,
2303
+ * assertType,
2304
+ * Data,
2305
+ * assertErr,
2306
+ * assertOk,
2307
+ * Boolean,
2308
+ * String,
2309
+ * err,
2310
+ * ok,
2311
+ * transform,
2312
+ * type Result,
2313
+ * type TypeError,
2314
+ * } from "@evolu/common";
2315
+ *
2316
+ * interface BooleanFromStringError extends TypeError<"BooleanFromString"> {
2317
+ * readonly value: string;
2318
+ * }
2319
+ *
2320
+ * const BooleanFromString = transform(
2321
+ * "BooleanFromString",
2322
+ * String,
2323
+ * Boolean,
2324
+ * {
2325
+ * from: (value): Result<boolean, BooleanFromStringError> =>
2326
+ * value === "true"
2327
+ * ? ok(true)
2328
+ * : value === "false"
2329
+ * ? ok(false)
2330
+ * : err({ type: "BooleanFromString", value }),
2331
+ * to: (value) => (value ? "true" : "false"),
2332
+ * },
2333
+ * () => 'Expected "true" or "false".',
2334
+ * );
2335
+ *
2336
+ * assertOk(BooleanFromString.fromUnknown("true"), true);
2337
+ * assertEqual(BooleanFromString.to(false), "false");
2338
+ *
2339
+ * const invalid = BooleanFromString.fromUnknown("yes");
2340
+ * assertErr(invalid);
2341
+ * assertType(Data, invalid.error);
2342
+ * assertEqual(invalid.error, { type: "BooleanFromString", value: "yes" });
2343
+ * assertEqual(
2344
+ * BooleanFromString.formatError(invalid.error),
2345
+ * 'Expected "true" or "false".',
2346
+ * );
2347
+ * ```
2348
+ */
2188
2349
  export function transform<
2189
2350
  Name extends TypeName,
2190
2351
  ParentType extends ConcreteTypeNode,
@@ -2473,7 +2634,7 @@ function getTerminalRuntimeNode<Value>(
2473
2634
  ): RuntimeOperation<Value>;
2474
2635
  function getTerminalRuntimeNode(node: RuntimeTypeNode): RuntimeTypeNode;
2475
2636
  function getTerminalRuntimeNode(node: { readonly parent?: unknown }): unknown {
2476
- while (node.parent) node = node.parent;
2637
+ while (node.parent != null) node = node.parent;
2477
2638
 
2478
2639
  return node;
2479
2640
  }
@@ -2698,16 +2859,20 @@ export interface TypeOfError<
2698
2859
  * and significant whitespace:
2699
2860
  *
2700
2861
  * ```ts
2701
- * import { String, maxLength, type Brand } from "@evolu/common";
2862
+ * import {
2863
+ * assertOk,
2864
+ * assertType,
2865
+ * String,
2866
+ * maxLength,
2867
+ * type Brand,
2868
+ * } from "@evolu/common";
2702
2869
  *
2703
2870
  * const WireValue100 = maxLength(100)(String);
2704
2871
  * type WireValue100 = typeof WireValue100.Output;
2705
2872
  *
2706
- * expectTypeOf<WireValue100>().toEqualTypeOf<
2707
- * string & Brand<"MaxLength100">
2708
- * >();
2709
- * expectOk(WireValue100.fromUnknown(""), "");
2710
- * expectOk(WireValue100.fromUnknown(" value "), " value ");
2873
+ * assertType<string & Brand<"MaxLength100">, WireValue100>();
2874
+ * assertOk(WireValue100.fromUnknown(""), "");
2875
+ * assertOk(WireValue100.fromUnknown(" value "), " value ");
2711
2876
  * ```
2712
2877
  *
2713
2878
  * @group String
@@ -2734,7 +2899,12 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
2734
2899
  *
2735
2900
  * ```ts
2736
2901
  * import {
2902
+ * assertEqual,
2903
+ * assertErr,
2904
+ * assertOk,
2905
+ * assertType,
2737
2906
  * Age,
2907
+ * Data,
2738
2908
  * FiniteNumber,
2739
2909
  * Int,
2740
2910
  * NonNaNNumber,
@@ -2744,36 +2914,41 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
2744
2914
  * } from "@evolu/common";
2745
2915
  *
2746
2916
  * // Note how every additional constraint accumulates its Brand.
2747
- * expectTypeOf<typeof Number.Output>().toEqualTypeOf<number>();
2748
- * expectTypeOf<typeof NonNaNNumber.Output>().toEqualTypeOf<
2749
- * number & Brand<"NonNaN">
2917
+ * assertType<number, typeof Number.Output>();
2918
+ * assertType<number & Brand<"NonNaN">, typeof NonNaNNumber.Output>();
2919
+ * assertType<
2920
+ * number & Brand<"NonNaN"> & Brand<"Finite">,
2921
+ * typeof FiniteNumber.Output
2750
2922
  * >();
2751
- * expectTypeOf<typeof FiniteNumber.Output>().toEqualTypeOf<
2752
- * number & Brand<"NonNaN"> & Brand<"Finite">
2923
+ * assertType<
2924
+ * number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int">,
2925
+ * typeof Int.Output
2753
2926
  * >();
2754
- * expectTypeOf<typeof Int.Output>().toEqualTypeOf<
2755
- * number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int">
2756
- * >();
2757
- * expectTypeOf<typeof NonNegativeInt.Output>().toEqualTypeOf<
2927
+ * assertType<
2758
2928
  * number &
2759
2929
  * Brand<"NonNaN"> &
2760
2930
  * Brand<"Finite"> &
2761
2931
  * Brand<"Int"> &
2762
- * Brand<"NonNegative">
2932
+ * Brand<"NonNegative">,
2933
+ * typeof NonNegativeInt.Output
2763
2934
  * >();
2764
2935
  *
2765
- * expectTypeOf<typeof Age.Output>().toEqualTypeOf<
2936
+ * assertType<
2766
2937
  * number &
2767
2938
  * Brand<"NonNaN"> &
2768
2939
  * Brand<"Finite"> &
2769
2940
  * Brand<"Int"> &
2770
2941
  * Brand<"NonNegative"> &
2771
2942
  * Brand<"LessThan200"> &
2772
- * Brand<"Age">
2943
+ * Brand<"Age">,
2944
+ * typeof Age.Output
2773
2945
  * >();
2774
2946
  *
2775
- * expectOk(Age.fromUnknown(122), 122);
2776
- * expectErr(Age.fromUnknown(200), {
2947
+ * assertOk(Age.fromUnknown(122), 122);
2948
+ * const invalid = Age.fromUnknown(200);
2949
+ * assertErr(invalid);
2950
+ * assertType(Data, invalid.error);
2951
+ * assertEqual(invalid.error, {
2777
2952
  * type: "LessThan200",
2778
2953
  * value: 200,
2779
2954
  * max: 200,
@@ -2926,12 +3101,12 @@ interface ObjectTagOutputByName {
2926
3101
  * ### Example
2927
3102
  *
2928
3103
  * ```ts
2929
- * import { objectTag } from "@evolu/common";
3104
+ * import { assertOk, objectTag } from "@evolu/common";
2930
3105
  *
2931
3106
  * const DateType = objectTag("Date");
2932
- * const date = new globalThis.Date("2025-01-01T00:00:00.000Z");
3107
+ * const date = new Date("2025-01-01T00:00:00.000Z");
2933
3108
  *
2934
- * expectOk(DateType.fromUnknown(date), date);
3109
+ * assertOk(DateType.fromUnknown(date), date);
2935
3110
  * ```
2936
3111
  *
2937
3112
  * @group Base
@@ -2949,7 +3124,41 @@ export function objectTag<Name extends keyof ObjectTagOutputByName>(
2949
3124
  ObjectTagOutputByName[Name]
2950
3125
  >;
2951
3126
 
2952
- /** Creates an object-tag Type by refining an existing object Type. */
3127
+ /**
3128
+ * Creates an object-tag Type by refining an existing object Type.
3129
+ *
3130
+ * ### Example
3131
+ *
3132
+ * ```ts
3133
+ * import {
3134
+ * assertEqual,
3135
+ * assertOk,
3136
+ * assertSame,
3137
+ * assertType,
3138
+ * instanceOf,
3139
+ * objectTag,
3140
+ * } from "@evolu/common";
3141
+ *
3142
+ * class TaggedValue {
3143
+ * readonly [Symbol.toStringTag] = "TaggedValue";
3144
+ * }
3145
+ *
3146
+ * const TaggedValueType = objectTag(
3147
+ * "TaggedValue",
3148
+ * instanceOf(TaggedValue),
3149
+ * );
3150
+ * const value = new TaggedValue();
3151
+ * const result = TaggedValueType.fromUnknown(value);
3152
+ *
3153
+ * assertOk(result);
3154
+ * assertSame(result.value, value);
3155
+ * assertType<
3156
+ * true,
3157
+ * typeof result.value extends TaggedValue ? true : false
3158
+ * >();
3159
+ * assertEqual(TaggedValueType.expected, "TaggedValue");
3160
+ * ```
3161
+ */
2953
3162
  export function objectTag<
2954
3163
  Name extends TypeName,
2955
3164
  OutputType extends ConcreteTypeNode & { readonly Output: object },
@@ -3040,7 +3249,7 @@ export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
3040
3249
  * ### Example
3041
3250
  *
3042
3251
  * ```ts
3043
- * import { instanceOf } from "@evolu/common";
3252
+ * import { assertFalse, assertTrue, instanceOf } from "@evolu/common";
3044
3253
  *
3045
3254
  * class User {
3046
3255
  * readonly name: string;
@@ -3052,8 +3261,8 @@ export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
3052
3261
  *
3053
3262
  * const UserInstance = instanceOf(User);
3054
3263
  *
3055
- * assert(UserInstance.is(new User("Ada")));
3056
- * assert(!UserInstance.is({ name: "Ada" }));
3264
+ * assertTrue(UserInstance.is(new User("Ada")));
3265
+ * assertFalse(UserInstance.is({ name: "Ada" }));
3057
3266
  * ```
3058
3267
  *
3059
3268
  * @group Base
@@ -3155,13 +3364,23 @@ type InstanceConstructorCompileTimeError = CompileTimeError<
3155
3364
  * ### Example
3156
3365
  *
3157
3366
  * ```ts
3158
- * import { literal } from "@evolu/common";
3367
+ * import {
3368
+ * assertEqual,
3369
+ * assertErr,
3370
+ * assertOk,
3371
+ * assertType,
3372
+ * Data,
3373
+ * literal,
3374
+ * } from "@evolu/common";
3159
3375
  *
3160
3376
  * const Ready = literal("ready");
3161
3377
  *
3162
- * expectTypeOf<typeof Ready.Output>().toEqualTypeOf<"ready">();
3163
- * expectOk(Ready.fromUnknown("ready"), "ready");
3164
- * expectErr(Ready.fromUnknown("pending"), {
3378
+ * assertType<"ready", typeof Ready.Output>();
3379
+ * assertOk(Ready.fromUnknown("ready"), "ready");
3380
+ * const invalid = Ready.fromUnknown("pending");
3381
+ * assertErr(invalid);
3382
+ * assertType(Data, invalid.error);
3383
+ * assertEqual(invalid.error, {
3165
3384
  * type: "Literal",
3166
3385
  * expected: "ready",
3167
3386
  * value: "pending",
@@ -3318,17 +3537,29 @@ export const Null = /*#__PURE__*/ literal(null);
3318
3537
  * ### Example
3319
3538
  *
3320
3539
  * ```ts
3321
- * import { Number, String, union } from "@evolu/common";
3540
+ * import {
3541
+ * assertEqual,
3542
+ * assertErr,
3543
+ * assertOk,
3544
+ * assertType,
3545
+ * Data,
3546
+ * Number,
3547
+ * String,
3548
+ * union,
3549
+ * } from "@evolu/common";
3322
3550
  *
3323
3551
  * const Status = union("draft", "published");
3324
3552
  * const StatusOrCode = union("draft", "published", Number);
3325
3553
  *
3326
- * expectOk(Status.fromUnknown("draft"), "draft");
3327
- * expectOk(StatusOrCode.fromUnknown(42), 42);
3554
+ * assertOk(Status.fromUnknown("draft"), "draft");
3555
+ * assertOk(StatusOrCode.fromUnknown(42), 42);
3328
3556
  *
3329
3557
  * const TextOrNumber = union(String, Number);
3330
3558
  *
3331
- * expectErr(TextOrNumber.fromUnknown(true, { errors: "all" }), {
3559
+ * const invalid = TextOrNumber.fromUnknown(true, { errors: "all" });
3560
+ * assertErr(invalid);
3561
+ * assertType(Data, invalid.error);
3562
+ * assertEqual(invalid.error, {
3332
3563
  * type: "Union",
3333
3564
  * errors: [
3334
3565
  * {
@@ -3486,11 +3717,11 @@ const createUnionValidation =
3486
3717
  * ### Example
3487
3718
  *
3488
3719
  * ```ts
3489
- * import { String, undefinedOr } from "@evolu/common";
3720
+ * import { assertOk, String, undefinedOr } from "@evolu/common";
3490
3721
  *
3491
3722
  * const StringOrUndefined = undefinedOr(String);
3492
3723
  *
3493
- * expectOk(StringOrUndefined.fromUnknown(undefined), undefined);
3724
+ * assertOk(StringOrUndefined.fromUnknown(undefined), undefined);
3494
3725
  * ```
3495
3726
  *
3496
3727
  * @group Unions
@@ -3505,11 +3736,11 @@ export const undefinedOr = <ValueType extends TypeNode>(
3505
3736
  * ### Example
3506
3737
  *
3507
3738
  * ```ts
3508
- * import { String, nullOr } from "@evolu/common";
3739
+ * import { assertOk, String, nullOr } from "@evolu/common";
3509
3740
  *
3510
3741
  * const NullableString = nullOr(String);
3511
3742
  *
3512
- * expectOk(NullableString.fromUnknown(null), null);
3743
+ * assertOk(NullableString.fromUnknown(null), null);
3513
3744
  * ```
3514
3745
  *
3515
3746
  * @group Unions
@@ -3524,12 +3755,12 @@ export const nullOr = <ValueType extends TypeNode>(
3524
3755
  * ### Example
3525
3756
  *
3526
3757
  * ```ts
3527
- * import { String, nullishOr } from "@evolu/common";
3758
+ * import { assertOk, String, nullishOr } from "@evolu/common";
3528
3759
  *
3529
3760
  * const NullishString = nullishOr(String);
3530
3761
  *
3531
- * expectOk(NullishString.fromUnknown(undefined), undefined);
3532
- * expectOk(NullishString.fromUnknown(null), null);
3762
+ * assertOk(NullishString.fromUnknown(undefined), undefined);
3763
+ * assertOk(NullishString.fromUnknown(null), null);
3533
3764
  * ```
3534
3765
  *
3535
3766
  * @group Unions
@@ -3604,9 +3835,7 @@ interface RuntimeUnionTypeNode extends RuntimeTypeNode {
3604
3835
  const isRuntimeUnionTypeNode = (
3605
3836
  type: RuntimeTypeNode,
3606
3837
  ): type is RuntimeUnionTypeNode =>
3607
- type.name === "Union" &&
3608
- "members" in type &&
3609
- globalThis.Array.isArray(type.members);
3838
+ type.name === "Union" && "members" in type && Array.isArray(type.members);
3610
3839
 
3611
3840
  /**
3612
3841
  * A root {@link Type} validating the encoded Inputs accepted by {@link union}.
@@ -3767,7 +3996,16 @@ interface UnionErrorValue<
3767
3996
  * the structured data decoded from it:
3768
3997
  *
3769
3998
  * ```ts
3770
- * import { templateLiteralParser, union } from "@evolu/common";
3999
+ * import {
4000
+ * assertFalse,
4001
+ * assertEqual,
4002
+ * assertErr,
4003
+ * assertOk,
4004
+ * assertType,
4005
+ * Data,
4006
+ * templateLiteralParser,
4007
+ * union,
4008
+ * } from "@evolu/common";
3771
4009
  *
3772
4010
  * const Language = union("en", "cs");
3773
4011
  * const Region = union("US", "CZ");
@@ -3777,36 +4015,39 @@ interface UnionErrorValue<
3777
4015
  *
3778
4016
  * // Output is the decoded language and region.
3779
4017
  * type SupportedLocale = typeof SupportedLocale.Output;
3780
- * expectTypeOf<SupportedLocale>().toEqualTypeOf<
3781
- * readonly ["en" | "cs", "US" | "CZ"]
3782
- * >();
4018
+ * assertType<readonly ["en" | "cs", "US" | "CZ"], SupportedLocale>();
3783
4019
  *
3784
4020
  * // The parent Output is the canonical locale string.
3785
4021
  * type SupportedLocaleLiteral = typeof SupportedLocale.parent.Output;
3786
- * expectTypeOf<SupportedLocaleLiteral>().toEqualTypeOf<
3787
- * "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
4022
+ * assertType<
4023
+ * "en-US" | "en-CZ" | "cs-US" | "cs-CZ",
4024
+ * SupportedLocaleLiteral
3788
4025
  * >();
3789
4026
  *
3790
4027
  * // Parse an unknown string into structured data.
3791
4028
  * const result = SupportedLocale.fromUnknown("cs-CZ");
3792
- * expectOk(result, ["cs", "CZ"]);
4029
+ * assertOk(result, ["cs", "CZ"]);
3793
4030
  * const locale = result.value;
3794
- * expectTypeOf(locale).toEqualTypeOf<SupportedLocale>();
3795
- * expectErr(SupportedLocale.fromUnknown("cs/CZ"), {
4031
+ * assertType<SupportedLocale, typeof locale>();
4032
+ * const invalid = SupportedLocale.fromUnknown("cs/CZ");
4033
+ * assertErr(invalid);
4034
+ * assertType(Data, invalid.error);
4035
+ * const error: Data = invalid.error;
4036
+ * assertEqual(error, {
3796
4037
  * type: "TemplateLiteral",
3797
4038
  * value: "cs/CZ",
3798
4039
  * });
3799
4040
  *
3800
4041
  * // Encode structured data into its canonical string.
3801
4042
  * const localeLiteral = SupportedLocale.to(locale);
3802
- * expectTypeOf(localeLiteral).toEqualTypeOf<SupportedLocaleLiteral>();
3803
- * expect(localeLiteral).toBe("cs-CZ");
4043
+ * assertType<SupportedLocaleLiteral, typeof localeLiteral>();
4044
+ * assertEqual(localeLiteral, "cs-CZ");
3804
4045
  *
3805
4046
  * // Validate a string configuration value.
3806
4047
  * const configValue: unknown = "cs-CZ";
3807
- * assert(SupportedLocale.parent.is(configValue));
3808
- * expectTypeOf(configValue).toEqualTypeOf<SupportedLocaleLiteral>();
3809
- * expect(SupportedLocale.parent.is("fr-CZ")).toBe(false);
4048
+ * assertType(SupportedLocale.parent, configValue);
4049
+ * assertType<SupportedLocaleLiteral, typeof configValue>();
4050
+ * assertFalse(SupportedLocale.parent.is("fr-CZ"));
3810
4051
  * ```
3811
4052
  *
3812
4053
  * `SupportedLocale` is structured data for application code.
@@ -3819,6 +4060,8 @@ interface UnionErrorValue<
3819
4060
  *
3820
4061
  * ```ts
3821
4062
  * import {
4063
+ * assertEqual,
4064
+ * assertOk,
3822
4065
  * NonNegativeDecimalString,
3823
4066
  * templateLiteralParser,
3824
4067
  * } from "@evolu/common";
@@ -3831,8 +4074,8 @@ interface UnionErrorValue<
3831
4074
  * // DecimalText.to requires a validated NonNegativeDecimalString.
3832
4075
  * const zero = NonNegativeDecimalString.orThrow("0");
3833
4076
  *
3834
- * expectOk(DecimalText.fromUnknown("decimal:0"), [zero]);
3835
- * expect(DecimalText.to([zero])).toBe("decimal:0");
4077
+ * assertOk(DecimalText.fromUnknown("decimal:0"), [zero]);
4078
+ * assertEqual(DecimalText.to([zero]), "decimal:0");
3836
4079
  * ```
3837
4080
  *
3838
4081
  * Capture Types (the Type arguments passed to `templateLiteralParser`) can use
@@ -3840,6 +4083,9 @@ interface UnionErrorValue<
3840
4083
  *
3841
4084
  * ```ts
3842
4085
  * import {
4086
+ * assertEqual,
4087
+ * assertOk,
4088
+ * assertType,
3843
4089
  * Int64FromInt64String,
3844
4090
  * templateLiteralParser,
3845
4091
  * } from "@evolu/common";
@@ -3850,24 +4096,29 @@ interface UnionErrorValue<
3850
4096
  *
3851
4097
  * // Decode the string into structured data.
3852
4098
  * const result = ItemId.fromUnknown("item-42");
3853
- * expectOk(result, [42n]);
4099
+ * assertOk(result, [42n]);
3854
4100
  * const itemId = result.value;
3855
- * expectTypeOf(itemId).toEqualTypeOf<ItemId>();
4101
+ * assertType<ItemId, typeof itemId>();
3856
4102
  *
3857
4103
  * // Encode the structured data into its canonical string.
3858
4104
  * const itemIdLiteral = ItemId.to(itemId);
3859
- * expectTypeOf(itemIdLiteral).toEqualTypeOf<ItemIdLiteral>();
3860
- * expect(itemIdLiteral).toBe("item-42");
4105
+ * assertType<ItemIdLiteral, typeof itemIdLiteral>();
4106
+ * assertEqual(itemIdLiteral, "item-42");
3861
4107
  *
3862
4108
  * // TypeScript cannot prove from the literal alone that "42" is a valid Int64 encoding.
3863
4109
  * // @ts-expect-error Validate it with ItemId.parent or create it with ItemId.to.
3864
- * const invalidItemIdLiteral: ItemIdLiteral = "item-42";
4110
+ * const _invalidItemIdLiteral: ItemIdLiteral = "item-42";
3865
4111
  * ```
3866
4112
  *
3867
4113
  * Fixed-width captures can be adjacent:
3868
4114
  *
3869
4115
  * ```ts
3870
- * import { templateLiteralParser, union } from "@evolu/common";
4116
+ * import {
4117
+ * assertEqual,
4118
+ * assertOk,
4119
+ * templateLiteralParser,
4120
+ * union,
4121
+ * } from "@evolu/common";
3871
4122
  *
3872
4123
  * const Digit = union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
3873
4124
  * const TwoDigits = templateLiteralParser(Digit, Digit);
@@ -3877,10 +4128,10 @@ interface UnionErrorValue<
3877
4128
  * const twoDigits: TwoDigits = ["4", "2"];
3878
4129
  * const twoDigitsLiteral: TwoDigitsLiteral = "42";
3879
4130
  * // @ts-expect-error TwoDigitsLiteral requires exactly two digits.
3880
- * const threeDigitsLiteral: TwoDigitsLiteral = "123";
4131
+ * const _threeDigitsLiteral: TwoDigitsLiteral = "123";
3881
4132
  *
3882
- * expectOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
3883
- * expect(TwoDigits.to(twoDigits)).toBe(twoDigitsLiteral);
4133
+ * assertOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
4134
+ * assertEqual(TwoDigits.to(twoDigits), twoDigitsLiteral);
3884
4135
  * ```
3885
4136
  *
3886
4137
  * TypeScript rejects multiple variable-width captures because their encoded
@@ -4145,6 +4396,7 @@ export interface TemplateLiteralType<
4145
4396
  TypeOfError<"String"> | TemplateLiteralParseError<Parts>,
4146
4397
  never,
4147
4398
  TemplateLiteralStringOutput<Parts>,
4399
+ // oxlint-disable-next-line typescript/no-unnecessary-type-arguments -- TemplateLiteralType explicitly documents every Type invariant, including identity encoding.
4148
4400
  true
4149
4401
  > {
4150
4402
  readonly [templateLiteralSyntaxSymbol]: true;
@@ -4165,17 +4417,24 @@ export interface TemplateLiteralType<
4165
4417
  * ### Example
4166
4418
  *
4167
4419
  * ```ts
4168
- * import { templateLiteral, union } from "@evolu/common";
4420
+ * import {
4421
+ * assertFalse,
4422
+ * assertOk,
4423
+ * assertType,
4424
+ * templateLiteral,
4425
+ * union,
4426
+ * } from "@evolu/common";
4169
4427
  *
4170
4428
  * const Language = union("en", "cs");
4171
4429
  * const Region = union("US", "CZ");
4172
4430
  * const Locale = templateLiteral(Language, "-", Region);
4173
4431
  *
4174
- * expectTypeOf<typeof Locale.Output>().toEqualTypeOf<
4175
- * "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
4432
+ * assertType<
4433
+ * "en-US" | "en-CZ" | "cs-US" | "cs-CZ",
4434
+ * typeof Locale.Output
4176
4435
  * >();
4177
- * expectOk(Locale.fromUnknown("cs-CZ"), "cs-CZ");
4178
- * expect(Locale.is("fr-CZ")).toBe(false);
4436
+ * assertOk(Locale.fromUnknown("cs-CZ"), "cs-CZ");
4437
+ * assertFalse(Locale.is("fr-CZ"));
4179
4438
  * ```
4180
4439
  *
4181
4440
  * @group Template literals
@@ -4445,7 +4704,7 @@ const compileTemplateLiteralParser = <Parts extends TemplateLiteralParts>(
4445
4704
  });
4446
4705
 
4447
4706
  return (input) => {
4448
- const inputCodePoints = globalThis.Array.from(input);
4707
+ const inputCodePoints = Array.from(input);
4449
4708
  const variableWidth = inputCodePoints.length - fixedPartsWidth;
4450
4709
  if (variableWidth < 0) {
4451
4710
  return err({ type: "TemplateLiteral", value: input });
@@ -4521,7 +4780,7 @@ const getStringTemplateLiteralFraming = (
4521
4780
  const lastCodeUnit = value.charCodeAt(value.length - 1);
4522
4781
 
4523
4782
  return {
4524
- width: globalThis.Array.from(value).length,
4783
+ width: Array.from(value).length,
4525
4784
  canBeEmpty: value.length === 0,
4526
4785
  canStartWithLowSurrogate:
4527
4786
  firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff,
@@ -4606,8 +4865,13 @@ const getTemplateLiteralPartFraming = (
4606
4865
  *
4607
4866
  * ```ts
4608
4867
  * import {
4868
+ * assertEqual,
4869
+ * assertErr,
4870
+ * assertOk,
4871
+ * assertType,
4609
4872
  * BigInt,
4610
4873
  * brand,
4874
+ * Data,
4611
4875
  * err,
4612
4876
  * ok,
4613
4877
  * type Brand,
@@ -4626,14 +4890,17 @@ const getTemplateLiteralPartFraming = (
4626
4890
  * type Int64 = typeof Int64.Output;
4627
4891
  *
4628
4892
  * // Note the Brand.
4629
- * expectTypeOf<Int64>().toEqualTypeOf<bigint & Brand<"Int64">>();
4893
+ * assertType<bigint & Brand<"Int64">, Int64>();
4630
4894
  *
4631
4895
  * interface Int64Error extends TypeError<"Int64"> {
4632
4896
  * readonly value: bigint;
4633
4897
  * }
4634
4898
  *
4635
- * expectOk(Int64.fromUnknown(42n), 42n);
4636
- * expectErr(Int64.fromUnknown(2n ** 63n), {
4899
+ * assertOk(Int64.fromUnknown(42n), 42n);
4900
+ * const invalid = Int64.fromUnknown(2n ** 63n);
4901
+ * assertErr(invalid);
4902
+ * assertType(Data, invalid.error);
4903
+ * assertEqual(invalid.error, {
4637
4904
  * type: "Int64",
4638
4905
  * value: 2n ** 63n,
4639
4906
  * });
@@ -4650,6 +4917,7 @@ export function brand<
4650
4917
  >(
4651
4918
  name: ValidateConcreteTypeName<Name>,
4652
4919
  parent: ValidateParent<ParentType>,
4920
+ // oxlint-disable-next-line typescript/no-unnecessary-type-arguments -- Explicit defaults in recursive Type declarations are benchmarked to avoid extra compiler work.
4653
4921
  validate?: (value: ParentType["Output"]) => Result<void, never>,
4654
4922
  ): BrandType<ParentType, Name, never>;
4655
4923
 
@@ -4716,13 +4984,21 @@ export interface BrandType<
4716
4984
  * ### Example
4717
4985
  *
4718
4986
  * ```ts
4719
- * import { DateIso } from "@evolu/common";
4987
+ * import {
4988
+ * assertEqual,
4989
+ * assertErr,
4990
+ * assertOk,
4991
+ * assertType,
4992
+ * Data,
4993
+ * DateIso,
4994
+ * } from "@evolu/common";
4720
4995
  *
4721
- * expectOk(
4722
- * DateIso.fromUnknown("2023-01-01T12:00:00.000Z"),
4723
- * "2023-01-01T12:00:00.000Z",
4724
- * );
4725
- * expectErr(DateIso.fromUnknown("2023-01-01"), {
4996
+ * const value = "2023-01-01T12:00:00.000Z";
4997
+ * assertOk(DateIso.fromUnknown(value), value);
4998
+ * const invalid = DateIso.fromUnknown("2023-01-01");
4999
+ * assertErr(invalid);
5000
+ * assertType(Data, invalid.error);
5001
+ * assertEqual(invalid.error, {
4726
5002
  * type: "DateIso",
4727
5003
  * value: "2023-01-01",
4728
5004
  * });
@@ -4757,13 +5033,13 @@ export interface DateIsoError extends TypeError<"DateIso"> {
4757
5033
  * ### Example
4758
5034
  *
4759
5035
  * ```ts
4760
- * import { DateIsoFromDate } from "@evolu/common";
5036
+ * import { assertEqual, assertOk, DateIsoFromDate } from "@evolu/common";
4761
5037
  *
4762
- * const date = new globalThis.Date("2025-01-01T12:00:00.000Z");
5038
+ * const date = new Date("2025-01-01T12:00:00.000Z");
4763
5039
  * const result = DateIsoFromDate.fromUnknown(date);
4764
5040
  *
4765
- * expectOk(result, "2025-01-01T12:00:00.000Z");
4766
- * expect(DateIsoFromDate.to(result.value)).toEqual(date);
5041
+ * assertOk(result, "2025-01-01T12:00:00.000Z");
5042
+ * assertEqual(DateIsoFromDate.to(result.value), date);
4767
5043
  * ```
4768
5044
  *
4769
5045
  * @group String
@@ -4856,6 +5132,11 @@ export interface UInt64Error extends TypeError<"UInt64"> {
4856
5132
  *
4857
5133
  * ```ts
4858
5134
  * import {
5135
+ * assertEqual,
5136
+ * assertErr,
5137
+ * assertOk,
5138
+ * assertType,
5139
+ * Data,
4859
5140
  * String,
4860
5141
  * brand,
4861
5142
  * err,
@@ -4881,14 +5162,17 @@ export interface UInt64Error extends TypeError<"UInt64"> {
4881
5162
  * const TrimmedString = trimmed(String);
4882
5163
  * type TrimmedString = typeof TrimmedString.Output;
4883
5164
  *
4884
- * expectTypeOf<TrimmedString>().toEqualTypeOf<string & Brand<"Trimmed">>();
5165
+ * assertType<string & Brand<"Trimmed">, TrimmedString>();
4885
5166
  *
4886
5167
  * interface TrimmedError extends TypeError<"Trimmed"> {
4887
5168
  * readonly value: string;
4888
5169
  * }
4889
5170
  *
4890
- * expectOk(TrimmedString.fromUnknown("Evolu"), "Evolu");
4891
- * expectErr(TrimmedString.fromUnknown(" Evolu"), {
5171
+ * assertOk(TrimmedString.fromUnknown("Evolu"), "Evolu");
5172
+ * const invalid = TrimmedString.fromUnknown(" Evolu");
5173
+ * assertErr(invalid);
5174
+ * assertType(Data, invalid.error);
5175
+ * assertEqual(invalid.error, {
4892
5176
  * type: "Trimmed",
4893
5177
  * value: " Evolu",
4894
5178
  * });
@@ -4920,6 +5204,7 @@ export type BrandFactory<
4920
5204
  *
4921
5205
  * ```ts
4922
5206
  * import {
5207
+ * assertType,
4923
5208
  * Number,
4924
5209
  * brand,
4925
5210
  * err,
@@ -4951,10 +5236,8 @@ export type BrandFactory<
4951
5236
  * const LessThan100 = lessThan(100)(Number);
4952
5237
  * type LessThan100 = typeof LessThan100.Output;
4953
5238
  *
4954
- * expectTypeOf(LessThan100.name).toEqualTypeOf<"LessThan100">();
4955
- * expectTypeOf<LessThan100>().toEqualTypeOf<
4956
- * number & Brand<"LessThan100">
4957
- * >();
5239
+ * assertType<"LessThan100", typeof LessThan100.name>();
5240
+ * assertType<number & Brand<"LessThan100">, LessThan100>();
4958
5241
  *
4959
5242
  * interface LessThanError<
4960
5243
  * Max extends number,
@@ -4989,17 +5272,27 @@ type BrandFactoryNumberError = CompileTimeError<
4989
5272
  * ### Example
4990
5273
  *
4991
5274
  * ```ts
4992
- * import { String, capitalized, type Brand } from "@evolu/common";
5275
+ * import {
5276
+ * assertEqual,
5277
+ * assertErr,
5278
+ * assertOk,
5279
+ * assertType,
5280
+ * Data,
5281
+ * String,
5282
+ * capitalized,
5283
+ * type Brand,
5284
+ * } from "@evolu/common";
4993
5285
  *
4994
5286
  * const CapitalizedString = capitalized(String);
4995
5287
  * type CapitalizedString = typeof CapitalizedString.Output;
4996
5288
  *
4997
- * expectTypeOf<CapitalizedString>().toEqualTypeOf<
4998
- * string & Brand<"Capitalized">
4999
- * >();
5289
+ * assertType<string & Brand<"Capitalized">, CapitalizedString>();
5000
5290
  *
5001
- * expectOk(CapitalizedString.fromUnknown("Evolu"), "Evolu");
5002
- * expectErr(CapitalizedString.fromUnknown("evolu"), {
5291
+ * assertOk(CapitalizedString.fromUnknown("Evolu"), "Evolu");
5292
+ * const invalid = CapitalizedString.fromUnknown("evolu");
5293
+ * assertErr(invalid);
5294
+ * assertType(Data, invalid.error);
5295
+ * assertEqual(invalid.error, {
5003
5296
  * type: "Capitalized",
5004
5297
  * value: "evolu",
5005
5298
  * });
@@ -5049,11 +5342,11 @@ export type CapitalizedString = typeof CapitalizedString.Output;
5049
5342
  * ### Example
5050
5343
  *
5051
5344
  * ```ts
5052
- * import { String, trimmed } from "@evolu/common";
5345
+ * import { assertOk, String, trimmed } from "@evolu/common";
5053
5346
  *
5054
5347
  * const Trimmed = trimmed(String);
5055
5348
  *
5056
- * expectOk(Trimmed.fromUnknown("Evolu"), "Evolu");
5349
+ * assertOk(Trimmed.fromUnknown("Evolu"), "Evolu");
5057
5350
  * ```
5058
5351
  *
5059
5352
  * @group String
@@ -5101,9 +5394,9 @@ export type TrimmedString = typeof TrimmedString.Output;
5101
5394
  * ### Example
5102
5395
  *
5103
5396
  * ```ts
5104
- * import { trim } from "@evolu/common";
5397
+ * import { assertEqual, trim } from "@evolu/common";
5105
5398
  *
5106
- * expect(trim(" Evolu ")).toBe("Evolu");
5399
+ * assertEqual(trim(" Evolu "), "Evolu");
5107
5400
  * ```
5108
5401
  *
5109
5402
  * @group String
@@ -5117,13 +5410,13 @@ export const trim = (value: string): TrimmedString =>
5117
5410
  * ### Example
5118
5411
  *
5119
5412
  * ```ts
5120
- * import { String, array, minLength } from "@evolu/common";
5413
+ * import { assertOk, String, array, minLength } from "@evolu/common";
5121
5414
  *
5122
5415
  * const AtLeastThreeCharacters = minLength(3)(String);
5123
5416
  * const AtLeastTwoItems = minLength(2)(array(String));
5124
5417
  *
5125
- * expectOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
5126
- * expectOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5418
+ * assertOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
5419
+ * assertOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5127
5420
  * ```
5128
5421
  *
5129
5422
  * @group String
@@ -5183,13 +5476,13 @@ export type NonEmptyTrimmedString = typeof NonEmptyTrimmedString.Output;
5183
5476
  * ### Example
5184
5477
  *
5185
5478
  * ```ts
5186
- * import { String, array, maxLength } from "@evolu/common";
5479
+ * import { assertOk, String, array, maxLength } from "@evolu/common";
5187
5480
  *
5188
5481
  * const AtMostThreeCharacters = maxLength(3)(String);
5189
5482
  * const AtMostTwoItems = maxLength(2)(array(String));
5190
5483
  *
5191
- * expectOk(AtMostThreeCharacters.fromUnknown("abc"), "abc");
5192
- * expectOk(AtMostTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5484
+ * assertOk(AtMostThreeCharacters.fromUnknown("abc"), "abc");
5485
+ * assertOk(AtMostTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5193
5486
  * ```
5194
5487
  *
5195
5488
  * @group String
@@ -5252,13 +5545,13 @@ export type NonEmptyTrimmedString1000 = typeof NonEmptyTrimmedString1000.Output;
5252
5545
  * ### Example
5253
5546
  *
5254
5547
  * ```ts
5255
- * import { String, array, length } from "@evolu/common";
5548
+ * import { assertOk, String, array, length } from "@evolu/common";
5256
5549
  *
5257
5550
  * const ThreeCharacters = length(3)(String);
5258
5551
  * const TwoItems = length(2)(array(String));
5259
5552
  *
5260
- * expectOk(ThreeCharacters.fromUnknown("abc"), "abc");
5261
- * expectOk(TwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5553
+ * assertOk(ThreeCharacters.fromUnknown("abc"), "abc");
5554
+ * assertOk(TwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5262
5555
  * ```
5263
5556
  *
5264
5557
  * @group String
@@ -5303,21 +5596,34 @@ export interface LengthError<
5303
5596
  * A non-empty string using the URL-safe alphabet:
5304
5597
  *
5305
5598
  * ```ts
5306
- * import { String, regex, type Brand } from "@evolu/common";
5599
+ * import {
5600
+ * assertEqual,
5601
+ * assertErr,
5602
+ * assertOk,
5603
+ * assertType,
5604
+ * Data,
5605
+ * String,
5606
+ * regex,
5607
+ * type Brand,
5608
+ * } from "@evolu/common";
5307
5609
  *
5308
- * const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
5610
+ * const UrlSafeString = regex(
5611
+ * "UrlSafeString",
5612
+ * /^[A-Za-z0-9_-]+$/u,
5613
+ * )(String);
5309
5614
  * type UrlSafeString = typeof UrlSafeString.Output;
5310
5615
  *
5311
- * expectTypeOf<UrlSafeString>().toEqualTypeOf<
5312
- * string & Brand<"UrlSafeString">
5313
- * >();
5616
+ * assertType<string & Brand<"UrlSafeString">, UrlSafeString>();
5314
5617
  *
5315
- * expectOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
5316
- * expectErr(UrlSafeString.fromUnknown("not safe"), {
5618
+ * assertOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
5619
+ * const invalid = UrlSafeString.fromUnknown("not safe");
5620
+ * assertErr(invalid);
5621
+ * assertType(Data, invalid.error);
5622
+ * assertEqual(invalid.error, {
5317
5623
  * type: "UrlSafeString",
5318
5624
  * value: "not safe",
5319
5625
  * source: "^[A-Za-z0-9_-]+$",
5320
- * flags: "",
5626
+ * flags: "u",
5321
5627
  * });
5322
5628
  * ```
5323
5629
  *
@@ -5378,7 +5684,7 @@ export interface RegexError<
5378
5684
  */
5379
5685
  export const UrlSafeString = /*#__PURE__*/ regex(
5380
5686
  "UrlSafeString",
5381
- /^[A-Za-z0-9_-]+$/,
5687
+ /^[A-Za-z0-9_-]+$/u,
5382
5688
  )(String);
5383
5689
  export type UrlSafeString = typeof UrlSafeString.Output;
5384
5690
 
@@ -5396,12 +5702,12 @@ const uint8ArrayToBase64UrlString = (bytes: Uint8Array): string => {
5396
5702
  return bytes.toBase64(base64UrlOptions);
5397
5703
  }
5398
5704
 
5399
- const binaryString = globalThis.Array.from(bytes, (byte) =>
5705
+ const binaryString = Array.from(bytes, (byte) =>
5400
5706
  globalThis.String.fromCodePoint(byte),
5401
5707
  ).join("");
5402
- const base64 = globalThis.btoa(binaryString);
5708
+ const base64 = btoa(binaryString);
5403
5709
 
5404
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
5710
+ return base64.replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
5405
5711
  };
5406
5712
 
5407
5713
  const base64UrlStringToUint8Array = (value: string): Uint8Array => {
@@ -5414,10 +5720,10 @@ const base64UrlStringToUint8Array = (value: string): Uint8Array => {
5414
5720
  return globalThis.Uint8Array.fromBase64(value, base64UrlOptions);
5415
5721
  }
5416
5722
 
5417
- let base64 = value.replace(/-/g, "+").replace(/_/g, "/");
5723
+ let base64 = value.replaceAll("-", "+").replaceAll("_", "/");
5418
5724
  while (base64.length % 4 !== 0) base64 += "=";
5419
5725
 
5420
- const binaryString = globalThis.atob(base64);
5726
+ const binaryString = atob(base64);
5421
5727
  return globalThis.Uint8Array.from(binaryString, (character) =>
5422
5728
  character.charCodeAt(0),
5423
5729
  );
@@ -5461,9 +5767,10 @@ export interface Base64UrlError extends TypeError<"Base64Url"> {
5461
5767
  * ### Example
5462
5768
  *
5463
5769
  * ```ts
5464
- * import { uint8ArrayToBase64Url } from "@evolu/common";
5770
+ * import { assertEqual, uint8ArrayToBase64Url } from "@evolu/common";
5465
5771
  *
5466
- * expect(uint8ArrayToBase64Url(new Uint8Array([0, 1, 2, 255]))).toBe(
5772
+ * assertEqual(
5773
+ * uint8ArrayToBase64Url(new Uint8Array([0, 1, 2, 255])),
5467
5774
  * "AAEC_w",
5468
5775
  * );
5469
5776
  * ```
@@ -5479,11 +5786,16 @@ export const uint8ArrayToBase64Url = (bytes: Uint8Array): Base64Url =>
5479
5786
  * ### Example
5480
5787
  *
5481
5788
  * ```ts
5482
- * import { Base64Url, base64UrlToUint8Array } from "@evolu/common";
5789
+ * import {
5790
+ * assertEqual,
5791
+ * Base64Url,
5792
+ * base64UrlToUint8Array,
5793
+ * } from "@evolu/common";
5483
5794
  *
5484
5795
  * const value = Base64Url.orThrow("AAEC_w");
5485
5796
  *
5486
- * expect(base64UrlToUint8Array(value)).toEqual(
5797
+ * assertEqual(
5798
+ * base64UrlToUint8Array(value),
5487
5799
  * new Uint8Array([0, 1, 2, 255]),
5488
5800
  * );
5489
5801
  * ```
@@ -5602,6 +5914,8 @@ export interface IdError extends TypeError<"Id"> {
5602
5914
  *
5603
5915
  * ```ts
5604
5916
  * import {
5917
+ * assertTrue,
5918
+ * assertType,
5605
5919
  * Id,
5606
5920
  * createId,
5607
5921
  * createRandomBytes,
@@ -5610,8 +5924,8 @@ export interface IdError extends TypeError<"Id"> {
5610
5924
  *
5611
5925
  * const userId = createId<"User">({ randomBytes: createRandomBytes() });
5612
5926
  *
5613
- * expect(Id.is(userId)).toBe(true);
5614
- * expectTypeOf(userId).toEqualTypeOf<Id & Brand<"User">>();
5927
+ * assertTrue(Id.is(userId));
5928
+ * assertType<Id & Brand<"User">, typeof userId>();
5615
5929
  * ```
5616
5930
  *
5617
5931
  * @group String
@@ -5632,14 +5946,20 @@ export const createId = <B extends string = never>(
5632
5946
  * ### Example
5633
5947
  *
5634
5948
  * ```ts
5635
- * import { createIdFromString, type Brand, type Id } from "@evolu/common";
5949
+ * import {
5950
+ * assertEqual,
5951
+ * assertType,
5952
+ * createIdFromString,
5953
+ * type Brand,
5954
+ * type Id,
5955
+ * } from "@evolu/common";
5636
5956
  *
5637
5957
  * const first = createIdFromString("external-user-123");
5638
5958
  * const second = createIdFromString("external-user-123");
5639
5959
  * const todoId = createIdFromString<"Todo">("external-todo-456");
5640
5960
  *
5641
- * expect(first).toBe(second);
5642
- * expectTypeOf(todoId).toEqualTypeOf<Id & Brand<"Todo">>();
5961
+ * assertEqual(first, second);
5962
+ * assertType<Id & Brand<"Todo">, typeof todoId>();
5643
5963
  * ```
5644
5964
  *
5645
5965
  * @group String
@@ -5664,6 +5984,7 @@ export const createIdFromString = <B extends string = never>(
5664
5984
  *
5665
5985
  * ```ts
5666
5986
  * import {
5987
+ * assertEqual,
5667
5988
  * createIdAsUuidv7,
5668
5989
  * createRandomBytes,
5669
5990
  * createTime,
@@ -5676,8 +5997,8 @@ export const createIdFromString = <B extends string = never>(
5676
5997
  * });
5677
5998
  * const bytes = idToIdBytes(value);
5678
5999
  *
5679
- * expect(bytes[6] >> 4).toBe(0x7);
5680
- * expect(bytes[8] & 0xc0).toBe(0x80);
6000
+ * assertEqual(bytes[6] >> 4, 0x7);
6001
+ * assertEqual(bytes[8] & 0xc0, 0x80);
5681
6002
  * ```
5682
6003
  *
5683
6004
  * @group String
@@ -5708,6 +6029,7 @@ export const createIdAsUuidv7 = <B extends string = never>(
5708
6029
  *
5709
6030
  * ```ts
5710
6031
  * import {
6032
+ * assertType,
5711
6033
  * createIdFromString,
5712
6034
  * id,
5713
6035
  * type Brand,
@@ -5717,7 +6039,7 @@ export const createIdAsUuidv7 = <B extends string = never>(
5717
6039
  * const TodoId = id("Todo");
5718
6040
  * const todoId = TodoId.orThrow(createIdFromString("todo"));
5719
6041
  *
5720
- * expectTypeOf(todoId).toEqualTypeOf<Id & Brand<"Todo">>();
6042
+ * assertType<Id & Brand<"Todo">, typeof todoId>();
5721
6043
  * ```
5722
6044
  *
5723
6045
  * @group String
@@ -5756,6 +6078,7 @@ export interface TableId<Table extends TypeName> extends Type<
5756
6078
  TableIdError<Table> | InferErrors<typeof String>,
5757
6079
  ChildCustomFrom<typeof String, Id & Brand<Table>, TableIdError<Table>>,
5758
6080
  CanonicalInputForChild<typeof String, Id & Brand<Table>>,
6081
+ // oxlint-disable-next-line typescript/no-unnecessary-type-arguments -- TableId must track its parent Type's identity encoding instead of hard-coding Type's default.
5759
6082
  IdentityEncodingOf<typeof String>
5760
6083
  > {
5761
6084
  readonly table: Table;
@@ -5830,11 +6153,15 @@ export const idBytesTypeValueLength = 16 as NonNegativeInt;
5830
6153
  * ### Example
5831
6154
  *
5832
6155
  * ```ts
5833
- * import { createIdFromString, idToIdBytes } from "@evolu/common";
6156
+ * import {
6157
+ * assertEqual,
6158
+ * createIdFromString,
6159
+ * idToIdBytes,
6160
+ * } from "@evolu/common";
5834
6161
  *
5835
6162
  * const bytes = idToIdBytes(createIdFromString("todo"));
5836
6163
  *
5837
- * expect(bytes).toHaveLength(16);
6164
+ * assertEqual(bytes.length, 16);
5838
6165
  * ```
5839
6166
  *
5840
6167
  * @group String
@@ -5849,6 +6176,7 @@ export const idToIdBytes = (value: Id): IdBytes =>
5849
6176
  *
5850
6177
  * ```ts
5851
6178
  * import {
6179
+ * assertEqual,
5852
6180
  * createIdFromString,
5853
6181
  * idBytesToId,
5854
6182
  * idToIdBytes,
@@ -5856,7 +6184,7 @@ export const idToIdBytes = (value: Id): IdBytes =>
5856
6184
  *
5857
6185
  * const value = createIdFromString("todo");
5858
6186
  *
5859
- * expect(idBytesToId(idToIdBytes(value))).toBe(value);
6187
+ * assertEqual(idBytesToId(idToIdBytes(value)), value);
5860
6188
  * ```
5861
6189
  *
5862
6190
  * @group String
@@ -5877,7 +6205,7 @@ export const Int64String = /*#__PURE__*/ brand(
5877
6205
 
5878
6206
  if (
5879
6207
  value.length > (negative ? 20 : 19) ||
5880
- !/^(?:0|-?[1-9]\d*)$/.test(value)
6208
+ !/^(?:0|-?[1-9]\d*)$/u.test(value)
5881
6209
  ) {
5882
6210
  return err<Int64StringError>({ type: "Int64String", value });
5883
6211
  }
@@ -5913,12 +6241,17 @@ export interface Int64StringError extends TypeError<"Int64String"> {
5913
6241
  * ### Example
5914
6242
  *
5915
6243
  * ```ts
5916
- * import { Int64FromInt64String } from "@evolu/common";
6244
+ * import {
6245
+ * assertEqual,
6246
+ * assertOk,
6247
+ * Int64FromInt64String,
6248
+ * } from "@evolu/common";
5917
6249
  *
5918
6250
  * const result = Int64FromInt64String.fromUnknown("9223372036854775807");
5919
6251
  *
5920
- * expectOk(result, 9223372036854775807n);
5921
- * expect(Int64FromInt64String.to(result.value)).toBe(
6252
+ * assertOk(result, 9223372036854775807n);
6253
+ * assertEqual(
6254
+ * Int64FromInt64String.to(result.value),
5922
6255
  * "9223372036854775807",
5923
6256
  * );
5924
6257
  * ```
@@ -5941,11 +6274,11 @@ export const Int64FromInt64String = /*#__PURE__*/ transform(
5941
6274
  * ### Example
5942
6275
  *
5943
6276
  * ```ts
5944
- * import { Number, nonNegative } from "@evolu/common";
6277
+ * import { assertOk, Number, nonNegative } from "@evolu/common";
5945
6278
  *
5946
6279
  * const NonNegative = nonNegative(Number);
5947
6280
  *
5948
- * expectOk(NonNegative.fromUnknown(0), 0);
6281
+ * assertOk(NonNegative.fromUnknown(0), 0);
5949
6282
  * ```
5950
6283
  *
5951
6284
  * @group Number
@@ -5987,11 +6320,11 @@ export type NonNegativeNumber = typeof NonNegativeNumber.Output;
5987
6320
  * ### Example
5988
6321
  *
5989
6322
  * ```ts
5990
- * import { Number, positive } from "@evolu/common";
6323
+ * import { assertOk, Number, positive } from "@evolu/common";
5991
6324
  *
5992
6325
  * const Positive = positive(Number);
5993
6326
  *
5994
- * expectOk(Positive.fromUnknown(1), 1);
6327
+ * assertOk(Positive.fromUnknown(1), 1);
5995
6328
  * ```
5996
6329
  *
5997
6330
  * @group Number
@@ -6034,11 +6367,11 @@ export type PositiveNumber = typeof PositiveNumber.Output;
6034
6367
  * ### Example
6035
6368
  *
6036
6369
  * ```ts
6037
- * import { Number, nonPositive } from "@evolu/common";
6370
+ * import { assertOk, Number, nonPositive } from "@evolu/common";
6038
6371
  *
6039
6372
  * const NonPositive = nonPositive(Number);
6040
6373
  *
6041
- * expectOk(NonPositive.fromUnknown(0), 0);
6374
+ * assertOk(NonPositive.fromUnknown(0), 0);
6042
6375
  * ```
6043
6376
  *
6044
6377
  * @group Number
@@ -6080,11 +6413,11 @@ export type NonPositiveNumber = typeof NonPositiveNumber.Output;
6080
6413
  * ### Example
6081
6414
  *
6082
6415
  * ```ts
6083
- * import { Number, negative } from "@evolu/common";
6416
+ * import { assertOk, Number, negative } from "@evolu/common";
6084
6417
  *
6085
6418
  * const Negative = negative(Number);
6086
6419
  *
6087
- * expectOk(Negative.fromUnknown(-1), -1);
6420
+ * assertOk(Negative.fromUnknown(-1), -1);
6088
6421
  * ```
6089
6422
  *
6090
6423
  * @group Number
@@ -6127,11 +6460,11 @@ export type NegativeNumber = typeof NegativeNumber.Output;
6127
6460
  * ### Example
6128
6461
  *
6129
6462
  * ```ts
6130
- * import { Number, nonNaN } from "@evolu/common";
6463
+ * import { assertOk, Number, nonNaN } from "@evolu/common";
6131
6464
  *
6132
6465
  * const NonNaN = nonNaN(Number);
6133
6466
  *
6134
- * expectOk(NonNaN.fromUnknown(Infinity), Infinity);
6467
+ * assertOk(NonNaN.fromUnknown(Infinity), Infinity);
6135
6468
  * ```
6136
6469
  *
6137
6470
  * @group Number
@@ -6175,11 +6508,11 @@ export type NonNaNNumber = typeof NonNaNNumber.Output;
6175
6508
  * ### Example
6176
6509
  *
6177
6510
  * ```ts
6178
- * import { Number, finite } from "@evolu/common";
6511
+ * import { assertOk, Number, finite } from "@evolu/common";
6179
6512
  *
6180
6513
  * const Finite = finite(Number);
6181
6514
  *
6182
- * expectOk(Finite.fromUnknown(42), 42);
6515
+ * assertOk(Finite.fromUnknown(42), 42);
6183
6516
  * ```
6184
6517
  *
6185
6518
  * @group Number
@@ -6240,15 +6573,28 @@ export type PositiveFiniteNumber = typeof PositiveFiniteNumber.Output;
6240
6573
  * ### Example
6241
6574
  *
6242
6575
  * ```ts
6243
- * import { Number, int, type Brand } from "@evolu/common";
6576
+ * import {
6577
+ * assertEqual,
6578
+ * assertErr,
6579
+ * assertOk,
6580
+ * assertType,
6581
+ * Data,
6582
+ * Number,
6583
+ * int,
6584
+ * type Brand,
6585
+ * } from "@evolu/common";
6244
6586
  *
6245
6587
  * const Int = int(Number);
6246
6588
  * type Int = typeof Int.Output;
6247
6589
  *
6248
- * expectTypeOf<Int>().toEqualTypeOf<number & Brand<"Int">>();
6590
+ * assertType<number & Brand<"Int">, Int>();
6249
6591
  *
6250
- * expectOk(Int.fromUnknown(42), 42);
6251
- * expectErr(Int.fromUnknown(1.5), { type: "Int", value: 1.5 });
6592
+ * assertOk(Int.fromUnknown(42), 42);
6593
+ * const invalid = Int.fromUnknown(1.5);
6594
+ * assertErr(invalid);
6595
+ * assertType(Data, invalid.error);
6596
+ * const error: Data = invalid.error;
6597
+ * assertEqual(error, { type: "Int", value: 1.5 });
6252
6598
  * ```
6253
6599
  *
6254
6600
  * @group Number
@@ -6348,11 +6694,11 @@ export type NegativeInt = typeof NegativeInt.Output;
6348
6694
  * ### Example
6349
6695
  *
6350
6696
  * ```ts
6351
- * import { Number, greaterThan } from "@evolu/common";
6697
+ * import { assertOk, Number, greaterThan } from "@evolu/common";
6352
6698
  *
6353
6699
  * const GreaterThanTen = greaterThan(10)(Number);
6354
6700
  *
6355
- * expectOk(GreaterThanTen.fromUnknown(11), 11);
6701
+ * assertOk(GreaterThanTen.fromUnknown(11), 11);
6356
6702
  * ```
6357
6703
  *
6358
6704
  * @group Number
@@ -6394,11 +6740,11 @@ export interface GreaterThanError<
6394
6740
  * ### Example
6395
6741
  *
6396
6742
  * ```ts
6397
- * import { Number, greaterThanOrEqualTo } from "@evolu/common";
6743
+ * import { assertOk, Number, greaterThanOrEqualTo } from "@evolu/common";
6398
6744
  *
6399
6745
  * const AtLeastTen = greaterThanOrEqualTo(10)(Number);
6400
6746
  *
6401
- * expectOk(AtLeastTen.fromUnknown(10), 10);
6747
+ * assertOk(AtLeastTen.fromUnknown(10), 10);
6402
6748
  * ```
6403
6749
  *
6404
6750
  * @group Number
@@ -6448,11 +6794,11 @@ export interface GreaterThanOrEqualToError<
6448
6794
  * ### Example
6449
6795
  *
6450
6796
  * ```ts
6451
- * import { Number, lessThan } from "@evolu/common";
6797
+ * import { assertOk, Number, lessThan } from "@evolu/common";
6452
6798
  *
6453
6799
  * const LessThanTen = lessThan(10)(Number);
6454
6800
  *
6455
- * expectOk(LessThanTen.fromUnknown(9), 9);
6801
+ * assertOk(LessThanTen.fromUnknown(9), 9);
6456
6802
  * ```
6457
6803
  *
6458
6804
  * @group Number
@@ -6505,11 +6851,11 @@ export type Age = typeof Age.Output;
6505
6851
  * ### Example
6506
6852
  *
6507
6853
  * ```ts
6508
- * import { Number, lessThanOrEqualTo } from "@evolu/common";
6854
+ * import { assertOk, Number, lessThanOrEqualTo } from "@evolu/common";
6509
6855
  *
6510
6856
  * const AtMostTen = lessThanOrEqualTo(10)(Number);
6511
6857
  *
6512
- * expectOk(AtMostTen.fromUnknown(10), 10);
6858
+ * assertOk(AtMostTen.fromUnknown(10), 10);
6513
6859
  * ```
6514
6860
  *
6515
6861
  * @group Number
@@ -6589,13 +6935,23 @@ export type Ratio = typeof Ratio.Output;
6589
6935
  * ### Example
6590
6936
  *
6591
6937
  * ```ts
6592
- * import { DecimalString } from "@evolu/common";
6938
+ * import {
6939
+ * assertEqual,
6940
+ * assertErr,
6941
+ * assertOk,
6942
+ * assertType,
6943
+ * Data,
6944
+ * DecimalString,
6945
+ * } from "@evolu/common";
6593
6946
  *
6594
- * expectOk(DecimalString.fromUnknown("-10.25"), "-10.25");
6595
- * expectOk(DecimalString.fromUnknown("0"), "0");
6596
- * expectOk(DecimalString.fromUnknown("10.25"), "10.25");
6947
+ * assertOk(DecimalString.fromUnknown("-10.25"), "-10.25");
6948
+ * assertOk(DecimalString.fromUnknown("0"), "0");
6949
+ * assertOk(DecimalString.fromUnknown("10.25"), "10.25");
6597
6950
  *
6598
- * expectErr(DecimalString.fromUnknown("10.250"), {
6951
+ * const invalid = DecimalString.fromUnknown("10.250");
6952
+ * assertErr(invalid);
6953
+ * assertType(Data, invalid.error);
6954
+ * assertEqual(invalid.error, {
6599
6955
  * type: "DecimalString",
6600
6956
  * value: "10.250",
6601
6957
  * });
@@ -6607,7 +6963,7 @@ export const DecimalString = /*#__PURE__*/ brand(
6607
6963
  "DecimalString",
6608
6964
  String,
6609
6965
  (value) =>
6610
- /^(?:0|-?(?:[1-9]\d*|(?:0|[1-9]\d*)\.\d*[1-9]))$/.test(value)
6966
+ /^(?:0|-?(?:[1-9]\d*|(?:0|[1-9]\d*)\.\d*[1-9]))$/u.test(value)
6611
6967
  ? ok()
6612
6968
  : err<DecimalStringError>({ type: "DecimalString", value }),
6613
6969
  (error) =>
@@ -6630,11 +6986,15 @@ export interface DecimalStringError extends TypeError<"DecimalString"> {
6630
6986
  * ### Example
6631
6987
  *
6632
6988
  * ```ts
6633
- * import { DecimalString, nonNegativeDecimalString } from "@evolu/common";
6989
+ * import {
6990
+ * assertOk,
6991
+ * DecimalString,
6992
+ * nonNegativeDecimalString,
6993
+ * } from "@evolu/common";
6634
6994
  *
6635
6995
  * const NonNegative = nonNegativeDecimalString(DecimalString);
6636
6996
  *
6637
- * expectOk(NonNegative.fromUnknown("0.5"), "0.5");
6997
+ * assertOk(NonNegative.fromUnknown("0.5"), "0.5");
6638
6998
  * ```
6639
6999
  *
6640
7000
  * @group Number
@@ -6682,11 +7042,15 @@ export type NonNegativeDecimalString = typeof NonNegativeDecimalString.Output;
6682
7042
  * ### Example
6683
7043
  *
6684
7044
  * ```ts
6685
- * import { DecimalString, positiveDecimalString } from "@evolu/common";
7045
+ * import {
7046
+ * assertOk,
7047
+ * DecimalString,
7048
+ * positiveDecimalString,
7049
+ * } from "@evolu/common";
6686
7050
  *
6687
7051
  * const Positive = positiveDecimalString(DecimalString);
6688
7052
  *
6689
- * expectOk(Positive.fromUnknown("0.5"), "0.5");
7053
+ * assertOk(Positive.fromUnknown("0.5"), "0.5");
6690
7054
  * ```
6691
7055
  *
6692
7056
  * @group Number
@@ -6738,11 +7102,15 @@ export type PositiveDecimalString = typeof PositiveDecimalString.Output;
6738
7102
  * ### Example
6739
7103
  *
6740
7104
  * ```ts
6741
- * import { DecimalString, nonPositiveDecimalString } from "@evolu/common";
7105
+ * import {
7106
+ * assertOk,
7107
+ * DecimalString,
7108
+ * nonPositiveDecimalString,
7109
+ * } from "@evolu/common";
6742
7110
  *
6743
7111
  * const NonPositive = nonPositiveDecimalString(DecimalString);
6744
7112
  *
6745
- * expectOk(NonPositive.fromUnknown("-0.5"), "-0.5");
7113
+ * assertOk(NonPositive.fromUnknown("-0.5"), "-0.5");
6746
7114
  * ```
6747
7115
  *
6748
7116
  * @group Number
@@ -6790,11 +7158,15 @@ export type NonPositiveDecimalString = typeof NonPositiveDecimalString.Output;
6790
7158
  * ### Example
6791
7159
  *
6792
7160
  * ```ts
6793
- * import { DecimalString, negativeDecimalString } from "@evolu/common";
7161
+ * import {
7162
+ * assertOk,
7163
+ * DecimalString,
7164
+ * negativeDecimalString,
7165
+ * } from "@evolu/common";
6794
7166
  *
6795
7167
  * const Negative = negativeDecimalString(DecimalString);
6796
7168
  *
6797
- * expectOk(Negative.fromUnknown("-0.5"), "-0.5");
7169
+ * assertOk(Negative.fromUnknown("-0.5"), "-0.5");
6798
7170
  * ```
6799
7171
  *
6800
7172
  * @group Number
@@ -6854,16 +7226,29 @@ export type NegativeDecimalString = typeof NegativeDecimalString.Output;
6854
7226
  * ### Example
6855
7227
  *
6856
7228
  * ```ts
6857
- * import { FiniteNumber, multipleOf, type Brand } from "@evolu/common";
7229
+ * import {
7230
+ * assertEqual,
7231
+ * assertErr,
7232
+ * assertOk,
7233
+ * assertType,
7234
+ * Data,
7235
+ * FiniteNumber,
7236
+ * multipleOf,
7237
+ * type Brand,
7238
+ * } from "@evolu/common";
6858
7239
  *
6859
7240
  * const Tenths = multipleOf("0.1")(FiniteNumber);
6860
7241
  *
6861
- * expectTypeOf<typeof Tenths.Output>().toEqualTypeOf<
6862
- * FiniteNumber & Brand<"MultipleOf0.1">
7242
+ * assertType<
7243
+ * FiniteNumber & Brand<"MultipleOf0.1">,
7244
+ * typeof Tenths.Output
6863
7245
  * >();
6864
7246
  *
6865
- * expectOk(Tenths.fromUnknown(0.3), 0.3);
6866
- * expectErr(Tenths.fromUnknown(0.31), {
7247
+ * assertOk(Tenths.fromUnknown(0.3), 0.3);
7248
+ * const invalid = Tenths.fromUnknown(0.31);
7249
+ * assertErr(invalid);
7250
+ * assertType(Data, invalid.error);
7251
+ * assertEqual(invalid.error, {
6867
7252
  * type: "MultipleOf0.1",
6868
7253
  * value: 0.31,
6869
7254
  * divisor: "0.1",
@@ -7000,11 +7385,11 @@ const decimalStringToParts = (value: string): DecimalParts => {
7000
7385
  * ### Example
7001
7386
  *
7002
7387
  * ```ts
7003
- * import { Number, between } from "@evolu/common";
7388
+ * import { assertOk, Number, between } from "@evolu/common";
7004
7389
  *
7005
7390
  * const Percentage = between(0, 100)(Number);
7006
7391
  *
7007
- * expectOk(Percentage.fromUnknown(75), 75);
7392
+ * assertOk(Percentage.fromUnknown(75), 75);
7008
7393
  * ```
7009
7394
  *
7010
7395
  * @group Number
@@ -7074,6 +7459,11 @@ export interface BetweenError<
7074
7459
  *
7075
7460
  * ```ts
7076
7461
  * import {
7462
+ * assertEqual,
7463
+ * assertErr,
7464
+ * assertOk,
7465
+ * assertType,
7466
+ * Data,
7077
7467
  * String,
7078
7468
  * array,
7079
7469
  * brand,
@@ -7085,12 +7475,16 @@ export interface BetweenError<
7085
7475
  * const UserIds = array(UserId);
7086
7476
  * const result = UserIds.from.parent(["ada", "grace"]);
7087
7477
  *
7088
- * expectTypeOf(result).toEqualTypeOf<
7089
- * Result<ReadonlyArray<string & Brand<"UserId">>, never>
7478
+ * assertType<
7479
+ * Result<ReadonlyArray<string & Brand<"UserId">>>,
7480
+ * typeof result
7090
7481
  * >();
7091
- * expectOk(result, ["ada", "grace"]);
7092
- * expectOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
7093
- * expectErr(UserIds.fromUnknown("ada"), {
7482
+ * assertOk(result, ["ada", "grace"]);
7483
+ * assertOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
7484
+ * const invalid = UserIds.fromUnknown("ada");
7485
+ * assertErr(invalid);
7486
+ * assertType(Data, invalid.error);
7487
+ * assertEqual(invalid.error, {
7094
7488
  * type: "Array",
7095
7489
  * reason: { kind: "NotArray", value: "ada" },
7096
7490
  * });
@@ -7494,7 +7888,7 @@ const copyArrayPrefix = (
7494
7888
  value: ReadonlyArray<unknown>,
7495
7889
  endIndex: number,
7496
7890
  ): Array<unknown> => {
7497
- const output = new Array<unknown>(value.length);
7891
+ const output = createMutableArray<unknown>(value.length);
7498
7892
 
7499
7893
  for (let index = 0; index < endIndex; index++) {
7500
7894
  output[index] = value[index];
@@ -7506,20 +7900,20 @@ const copyArrayPrefix = (
7506
7900
  /**
7507
7901
  * Set {@link Type} whose every element must match one Type.
7508
7902
  *
7509
- * Direct Sets from this or another realm are accepted. Set subclasses are
7510
- * rejected. A Set must have no own properties; its elements are validated in
7511
- * iteration order. Classification uses the realm-neutral object tag and
7512
- * prototype structure under Evolu Type's trusted JavaScript policy.
7903
+ * Sets from this or another realm are accepted. A Set must have no own
7904
+ * properties; its elements are validated in iteration order. Classification
7905
+ * uses the realm-neutral object tag under Evolu Type's trusted JavaScript
7906
+ * policy.
7513
7907
  *
7514
7908
  * ### Example
7515
7909
  *
7516
7910
  * ```ts
7517
- * import { String, set } from "@evolu/common";
7911
+ * import { assertOk, String, set } from "@evolu/common";
7518
7912
  *
7519
7913
  * const Tags = set(String);
7520
7914
  * const tags = new Set(["local-first", "offline"]);
7521
7915
  *
7522
- * expectOk(Tags.fromUnknown(tags), tags);
7916
+ * assertOk(Tags.fromUnknown(tags), tags);
7523
7917
  * ```
7524
7918
  *
7525
7919
  * @group Collection
@@ -7608,15 +8002,12 @@ type SetNodeError<ElementType extends TypeNode> = [
7608
8002
  : SetError<ElementType["Error"]>;
7609
8003
 
7610
8004
  /**
7611
- * Error returned by {@link set} for a non-Set value, an invalid Set prototype,
7612
- * or invalid Set items.
8005
+ * Error returned by {@link set} for a non-Set value or invalid Set items.
7613
8006
  *
7614
8007
  * @group Collection
7615
8008
  */
7616
8009
  export type SetError<Error extends TypeError = TypeError> =
7617
- | SetNotSetError
7618
- | SetUnexpectedPrototypeError
7619
- | SetItemsErrorValue<Error, true>;
8010
+ SetNotSetError | SetItemsErrorValue<Error, true>;
7620
8011
 
7621
8012
  /**
7622
8013
  * Error returned when a {@link set} input is not a Set.
@@ -7630,18 +8021,6 @@ export interface SetNotSetError extends TypeError<"Set"> {
7630
8021
  };
7631
8022
  }
7632
8023
 
7633
- /**
7634
- * An error returned when a {@link set} input is a Set subclass.
7635
- *
7636
- * @group Collection
7637
- */
7638
- export interface SetUnexpectedPrototypeError extends TypeError<"Set"> {
7639
- readonly reason: {
7640
- readonly kind: "UnexpectedPrototype";
7641
- readonly value: ReadonlySet<unknown>;
7642
- };
7643
- }
7644
-
7645
8024
  /**
7646
8025
  * An own property found on a Set value.
7647
8026
  *
@@ -7795,91 +8174,54 @@ const createHomogeneousCollectionType = <Collection>(
7795
8174
 
7796
8175
  const setTypeByElement = /*#__PURE__*/ new WeakMap<TypeNode, TypeNode>();
7797
8176
 
7798
- const validateSetCollection = (
7799
- value: unknown,
7800
- validateElement: RuntimeOutputValidation,
7801
- options: ValidationOptions,
7802
- ): Result<ReadonlySet<unknown>, SetError> => {
7803
- if (!hasObjectTag(value, "Set")) {
7804
- return err({
7805
- type: "Set",
7806
- reason: { kind: "NotSet", value },
7807
- });
7808
- }
7809
- if (!hasDirectSetPrototype(value as object)) {
7810
- return err({
7811
- type: "Set",
7812
- reason: {
7813
- kind: "UnexpectedPrototype",
7814
- value: value as ReadonlySet<unknown>,
7815
- },
7816
- });
7817
- }
7818
-
7819
- return validateSetItems(
7820
- value as ReadonlySet<unknown>,
7821
- validateElement,
7822
- options,
7823
- true,
7824
- );
7825
- };
7826
-
7827
- const encodeSetCollection = (
7828
- value: ReadonlySet<unknown>,
7829
- encodeElement: RuntimeEncoder,
7830
- ): ReadonlySet<unknown> => {
7831
- let changed = false;
7832
- const output = new globalThis.Set<unknown>();
7833
-
7834
- for (const item of value) {
7835
- const encoded = encodeElement(item as never);
7836
- if (!globalThis.Object.is(encoded, item)) changed = true;
7837
- output.add(encoded);
7838
- }
7839
-
7840
- return changed ? output : value;
7841
- };
7842
-
7843
- const isSetCollection = (
7844
- value: unknown,
7845
- isElement: (value: unknown) => boolean,
7846
- ): boolean => {
7847
- if (!hasObjectTag(value, "Set")) return false;
7848
- if (!hasDirectSetPrototype(value as object)) return false;
7849
- if (Reflect.ownKeys(value as object).length !== 0) return false;
7850
- for (const item of value as ReadonlySet<unknown>) {
7851
- if (!isElement(item)) return false;
7852
- }
7853
- return true;
7854
- };
7855
-
7856
- const hasDirectSetPrototype = (value: object): boolean => {
7857
- const prototype: unknown = globalThis.Object.getPrototypeOf(value);
7858
- if (prototype === null) return false;
7859
-
7860
- const objectPrototype: unknown = globalThis.Object.getPrototypeOf(prototype);
7861
-
7862
- return (
7863
- objectPrototype !== null &&
7864
- globalThis.Object.getPrototypeOf(objectPrototype) === null
7865
- );
7866
- };
7867
-
7868
8177
  const setRuntimeConfig: HomogeneousCollectionRuntimeConfig<
7869
8178
  ReadonlySet<unknown>
7870
8179
  > = {
7871
8180
  name: "Set",
7872
8181
  typeByElement: setTypeByElement,
7873
- validate: validateSetCollection,
8182
+ validate: (value, validateElement, options) => {
8183
+ if (!hasObjectTag(value, "Set")) {
8184
+ return err({
8185
+ type: "Set",
8186
+ reason: { kind: "NotSet", value },
8187
+ });
8188
+ }
8189
+ return validateSetItems(
8190
+ value as ReadonlySet<unknown>,
8191
+ validateElement,
8192
+ options,
8193
+ true,
8194
+ );
8195
+ },
7874
8196
  validateItems: (value, validateElement, options) =>
7875
8197
  validateSetItems(value, validateElement, options, false),
7876
- encode: encodeSetCollection,
7877
- is: isSetCollection,
8198
+ encode: (value, encodeElement) => {
8199
+ let changed = false;
8200
+ const output = new Set<unknown>();
8201
+
8202
+ for (const item of value) {
8203
+ const encoded = encodeElement(item as never);
8204
+ if (!globalThis.Object.is(encoded, item)) changed = true;
8205
+ output.add(encoded);
8206
+ }
8207
+
8208
+ return changed ? output : value;
8209
+ },
8210
+ is: (value, isElement) => {
8211
+ if (
8212
+ !hasObjectTag(value, "Set") ||
8213
+ Reflect.ownKeys(value as object).length !== 0
8214
+ ) {
8215
+ return false;
8216
+ }
8217
+ for (const item of value as ReadonlySet<unknown>) {
8218
+ if (!isElement(item)) return false;
8219
+ }
8220
+ return true;
8221
+ },
7878
8222
  formatError: ((error: SetError) => {
7879
8223
  if (error.reason.kind === "NotSet")
7880
8224
  return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Set.`;
7881
- if (error.reason.kind === "UnexpectedPrototype")
7882
- return "The value is an instance of a Set subclass, but a Set Output must be a direct Set instance.";
7883
8225
  const issue = error.reason.issues[0];
7884
8226
  switch (issue.kind) {
7885
8227
  case "ExcessProperty":
@@ -7899,7 +8241,7 @@ const validateSetItems = (
7899
8241
  let issues:
7900
8242
  Array<SetStructuralIssue | SetElementIssue<TypeError>> | undefined;
7901
8243
  let changed = false;
7902
- const output = new globalThis.Set<unknown>();
8244
+ const output = new Set<unknown>();
7903
8245
 
7904
8246
  if (checkStructure) {
7905
8247
  for (const key of Reflect.ownKeys(value)) {
@@ -7936,14 +8278,530 @@ const validateSetItems = (
7936
8278
  };
7937
8279
 
7938
8280
  /**
7939
- * Tuple {@link Type}.
8281
+ * Map {@link Type} whose keys and values must match their respective Types.
7940
8282
  *
7941
- * Use `tuple(First, Second, ...)` for a fixed-length readonly array in which
7942
- * every position has its own Type.
8283
+ * Maps from this or another realm are accepted. A Map must have no own
8284
+ * properties; its entries are validated in iteration order. When distinct input
8285
+ * keys decode to the same output key, validation fails instead of discarding
8286
+ * one associated value. Classification uses the realm-neutral object tag under
8287
+ * Evolu Type's trusted JavaScript policy.
7943
8288
  *
7944
- * `fromUnknown` validates the Tuple representation and runs every element's
7945
- * complete Type pipeline. By default, it returns the first issue. Pass `{
7946
- * errors: "all" }` to collect issues across the whole Tuple.
8289
+ * ### Example
8290
+ *
8291
+ * ```ts
8292
+ * import { assertOk, PositiveInt, String, map } from "@evolu/common";
8293
+ *
8294
+ * const Scores = map(String, PositiveInt);
8295
+ * const scores = new Map([
8296
+ * ["Ada", 10],
8297
+ * ["Grace", 20],
8298
+ * ]);
8299
+ *
8300
+ * assertOk(Scores.fromUnknown(scores), scores);
8301
+ * ```
8302
+ *
8303
+ * @group Collection
8304
+ */
8305
+ export const map = <
8306
+ KeyType extends ConcreteTypeNode,
8307
+ ValueType extends ConcreteTypeNode,
8308
+ >(
8309
+ key: ValidateMapKeyType<KeyType>,
8310
+ value: ValidateMapValueType<ValueType>,
8311
+ ): MapType<KeyType, ValueType> => {
8312
+ const typeKey = key as unknown as KeyType & RuntimeTypeNode;
8313
+ const typeValue = value as unknown as ValueType & RuntimeTypeNode;
8314
+
8315
+ let typeByValue = mapTypeByValueByKey.get(typeKey);
8316
+ const cached = typeByValue?.get(typeValue);
8317
+ if (cached) return cached as MapType<KeyType, ValueType>;
8318
+
8319
+ const validate = (
8320
+ input: unknown,
8321
+ validateKey: RuntimeOutputValidation,
8322
+ validateValue: RuntimeOutputValidation,
8323
+ options: ValidationOptions,
8324
+ ): Result<ReadonlyMap<unknown, unknown>, MapError> => {
8325
+ if (!hasObjectTag(input, "Map")) {
8326
+ return err({
8327
+ type: "Map",
8328
+ reason: { kind: "NotMap", value: input },
8329
+ });
8330
+ }
8331
+ return validateMapEntries(
8332
+ input as ReadonlyMap<unknown, unknown>,
8333
+ validateKey,
8334
+ validateValue,
8335
+ options,
8336
+ true,
8337
+ );
8338
+ };
8339
+ const fromUnknown = (
8340
+ input: unknown,
8341
+ options: ValidationOptions = firstValidationOptions,
8342
+ ) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
8343
+ const validateOutput = (
8344
+ input: unknown,
8345
+ options: ValidationOptions = firstValidationOptions,
8346
+ ) =>
8347
+ validate(
8348
+ input,
8349
+ typeKey[outputValidationSymbol],
8350
+ typeValue[outputValidationSymbol],
8351
+ options,
8352
+ );
8353
+ const formatError: TypeErrorFormatter<MapStructuralError> = (error) => {
8354
+ if (error.reason.kind === "NotMap")
8355
+ return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Map.`;
8356
+ const issue = error.reason.issues[0];
8357
+ switch (issue.kind) {
8358
+ case "ExcessProperty":
8359
+ return `An excess Map property ${safelyStringifyUnknownValue(issue.key)} is not allowed.`;
8360
+ case "Collision":
8361
+ return `Map keys at indexes ${issue.previousIndex} and ${issue.index} decode to the same key ${safelyStringifyUnknownValue(issue.outputKey)}.`;
8362
+ }
8363
+ };
8364
+ const rootKey = getTerminalRuntimeNode(typeKey);
8365
+ const rootValue = getTerminalRuntimeNode(typeValue);
8366
+ const parent =
8367
+ rootKey !== typeKey || rootValue !== typeValue
8368
+ ? (
8369
+ map as unknown as (
8370
+ key: ConcreteTypeNode,
8371
+ value: ConcreteTypeNode,
8372
+ ) => TypeNode
8373
+ )(
8374
+ rootKey as unknown as ConcreteTypeNode,
8375
+ rootValue as unknown as ConcreteTypeNode,
8376
+ )
8377
+ : null;
8378
+ const fromParent = parent
8379
+ ? (input: never, options: ValidationOptions = firstValidationOptions) =>
8380
+ validateMapEntries(
8381
+ input,
8382
+ getTerminalRuntimeNode(
8383
+ typeKey[fromSymbol],
8384
+ ) as RuntimeOutputValidation,
8385
+ getTerminalRuntimeNode(
8386
+ typeValue[fromSymbol],
8387
+ ) as RuntimeOutputValidation,
8388
+ options,
8389
+ false,
8390
+ )
8391
+ : undefined;
8392
+ const from = createFromOperation(fromParent);
8393
+ const encodeKey = typeKey[encoderSymbol];
8394
+ const encodeValue = typeValue[encoderSymbol];
8395
+ const to: RuntimeEncoder =
8396
+ encodeKey === identity && encodeValue === identity
8397
+ ? identity
8398
+ : (input: ReadonlyMap<unknown, unknown>) => {
8399
+ const output = new Map<unknown, unknown>();
8400
+ let changed = false;
8401
+
8402
+ for (const [inputKey, inputValue] of input) {
8403
+ const outputKey = encodeKey(inputKey as never);
8404
+ const outputValue = encodeValue(inputValue as never);
8405
+
8406
+ assert(
8407
+ !output.has(outputKey),
8408
+ "Map key Type encoding must not produce duplicate keys.",
8409
+ );
8410
+ output.set(outputKey, outputValue);
8411
+
8412
+ if (
8413
+ !globalThis.Object.is(inputKey, outputKey) ||
8414
+ !globalThis.Object.is(inputValue, outputValue)
8415
+ ) {
8416
+ changed = true;
8417
+ }
8418
+ }
8419
+
8420
+ return changed ? output : input;
8421
+ };
8422
+ const is = (input: unknown): boolean => {
8423
+ if (!hasObjectTag(input, "Map")) {
8424
+ return false;
8425
+ }
8426
+ if (Reflect.ownKeys(input as object).length !== 0) return false;
8427
+
8428
+ for (const [inputKey, inputValue] of input as ReadonlyMap<
8429
+ unknown,
8430
+ unknown
8431
+ >) {
8432
+ if (!typeKey.is(inputKey) || !typeValue.is(inputValue)) return false;
8433
+ }
8434
+
8435
+ return true;
8436
+ };
8437
+ const getTypeIssues: RuntimeGetTypeIssues = (error, mode) => {
8438
+ const mapError = error as MapError;
8439
+ if (mapError.reason.kind !== "Entries") {
8440
+ return singleRuntimeTypeIssue(
8441
+ "Map",
8442
+ error,
8443
+ formatError as TypeErrorFormatter<TypeError>,
8444
+ );
8445
+ }
8446
+
8447
+ const allIssues = mapError.reason.issues;
8448
+ const issues = mode === "first" ? ([allIssues[0]] as const) : allIssues;
8449
+
8450
+ return issues.flatMap((issue): ReadonlyArray<RuntimeTypeIssue> => {
8451
+ if (issue.kind === "Key" || issue.kind === "Value") {
8452
+ return prependRuntimeTypeIssuePath(
8453
+ issue.index,
8454
+ prependRuntimeTypeIssuePath(
8455
+ issue.kind === "Key" ? "key" : "value",
8456
+ (issue.kind === "Key" ? typeKey : typeValue)[
8457
+ getRuntimeTypeIssuesSymbol
8458
+ ](issue.error, mode),
8459
+ ),
8460
+ );
8461
+ }
8462
+
8463
+ return singleRuntimeTypeIssue(
8464
+ "Map",
8465
+ mode === "first"
8466
+ ? error
8467
+ : ({
8468
+ type: "Map",
8469
+ reason: { kind: "Entries", issues: [issue] },
8470
+ } as TypeError),
8471
+ formatError as TypeErrorFormatter<TypeError>,
8472
+ [issue.kind === "ExcessProperty" ? issue.key : issue.index],
8473
+ );
8474
+ }) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>;
8475
+ };
8476
+ const type = createTypeNode<MapType<KeyType, ValueType>>(
8477
+ "Map",
8478
+ parent,
8479
+ fromUnknown,
8480
+ is,
8481
+ validateOutput,
8482
+ from,
8483
+ to,
8484
+ getTypeIssues,
8485
+ { key: typeKey, value: typeValue },
8486
+ );
8487
+
8488
+ if (typeByValue === undefined) {
8489
+ typeByValue = new WeakMap();
8490
+ mapTypeByValueByKey.set(typeKey, typeByValue);
8491
+ }
8492
+ typeByValue.set(typeValue, type);
8493
+
8494
+ return type;
8495
+ };
8496
+
8497
+ const mapTypeByValueByKey = /*#__PURE__*/ new WeakMap<
8498
+ TypeNode,
8499
+ WeakMap<TypeNode, TypeNode>
8500
+ >();
8501
+
8502
+ /**
8503
+ * The readonly-map {@link Type} returned by {@link map}.
8504
+ *
8505
+ * @group Collection
8506
+ */
8507
+ export interface MapType<
8508
+ KeyType extends TypeNode,
8509
+ ValueType extends TypeNode,
8510
+ > extends Type<
8511
+ "Map",
8512
+ ReadonlyMap<KeyType["Input"], ValueType["Input"]>,
8513
+ ReadonlyMap<KeyType["Output"], ValueType["Output"]>,
8514
+ MapNodeError<KeyType, ValueType>,
8515
+ MapParent<KeyType, ValueType>,
8516
+ MapError<
8517
+ InferErrors<KeyType>,
8518
+ InferErrors<ValueType>,
8519
+ MapCollisionFor<KeyType>
8520
+ >,
8521
+ never,
8522
+ CanonicalInputSubset<
8523
+ ReadonlyMap<KeyType["Input"], ValueType["Input"]>,
8524
+ ReadonlyMap<CanonicalInputOf<KeyType>, CanonicalInputOf<ValueType>>
8525
+ >,
8526
+ AllTypesUseIdentityEncoding<KeyType | ValueType>
8527
+ > {
8528
+ readonly [reflectedTypesSymbol]?: KeyType | ValueType;
8529
+ readonly key: KeyType;
8530
+ readonly value: ValueType;
8531
+ }
8532
+
8533
+ type MapParent<KeyType extends TypeNode, ValueType extends TypeNode> = [
8534
+ KeyType["parent"] | ValueType["parent"],
8535
+ ] extends [null]
8536
+ ? null
8537
+ : MapType<RootType<KeyType>, RootType<ValueType>>;
8538
+
8539
+ type MapNodeError<KeyType extends TypeNode, ValueType extends TypeNode> = [
8540
+ KeyType["parent"] | ValueType["parent"],
8541
+ ] extends [null]
8542
+ ? MapError<InferErrors<KeyType>, InferErrors<ValueType>, never>
8543
+ : MapEntriesError<
8544
+ TypeFromError<KeyType>,
8545
+ TypeFromError<ValueType>,
8546
+ MapCollisionFor<KeyType>
8547
+ >;
8548
+
8549
+ type MapCollisionFor<KeyType extends TypeNode> = [KeyType["parent"]] extends [
8550
+ TypeNode,
8551
+ ]
8552
+ ? MapKeyCollisionIssue
8553
+ : never;
8554
+
8555
+ type ValidateMapKeyType<T extends ConcreteTypeNode> =
8556
+ IsUnion<T> extends false
8557
+ ? T
8558
+ : CompileTimeError<
8559
+ "Type",
8560
+ "Map key must use one concrete Type node. Pass a Union Type node instead of a union of Type nodes."
8561
+ >;
8562
+
8563
+ type ValidateMapValueType<T extends ConcreteTypeNode> =
8564
+ IsUnion<T> extends false
8565
+ ? T
8566
+ : CompileTimeError<
8567
+ "Type",
8568
+ "Map value must use one concrete Type node. Pass a Union Type node instead of a union of Type nodes."
8569
+ >;
8570
+
8571
+ /**
8572
+ * Error returned while validating a {@link map} and its entries.
8573
+ *
8574
+ * @group Collection
8575
+ */
8576
+ export type MapError<
8577
+ KeyError extends TypeError = TypeError,
8578
+ ValueError extends TypeError = TypeError,
8579
+ Collision extends MapKeyCollisionIssue = MapKeyCollisionIssue,
8580
+ > =
8581
+ | MapNotMapError
8582
+ | MapEntriesErrorValue<
8583
+ KeyError,
8584
+ ValueError,
8585
+ Collision | MapExcessPropertyIssue
8586
+ >;
8587
+
8588
+ /**
8589
+ * Error returned when a {@link map} input is not a Map.
8590
+ *
8591
+ * @group Collection
8592
+ */
8593
+ export interface MapNotMapError extends TypeError<"Map"> {
8594
+ readonly reason: {
8595
+ readonly kind: "NotMap";
8596
+ readonly value: unknown;
8597
+ };
8598
+ }
8599
+
8600
+ /**
8601
+ * An own property found on a Map value.
8602
+ *
8603
+ * @group Collection
8604
+ */
8605
+ export interface MapExcessPropertyIssue {
8606
+ readonly kind: "ExcessProperty";
8607
+ readonly key: string | symbol;
8608
+ }
8609
+
8610
+ /**
8611
+ * An invalid key and its entry index in a {@link map}.
8612
+ *
8613
+ * @group Collection
8614
+ */
8615
+ export type MapKeyIssue<Error extends TypeError> = Error extends TypeError
8616
+ ? {
8617
+ readonly kind: "Key";
8618
+ readonly index: number;
8619
+ readonly key: unknown;
8620
+ readonly error: Error;
8621
+ }
8622
+ : never;
8623
+
8624
+ /**
8625
+ * An invalid value and its entry index in a {@link map}.
8626
+ *
8627
+ * @group Collection
8628
+ */
8629
+ export type MapValueIssue<Error extends TypeError> = Error extends TypeError
8630
+ ? {
8631
+ readonly kind: "Value";
8632
+ readonly index: number;
8633
+ readonly key: unknown;
8634
+ readonly error: Error;
8635
+ }
8636
+ : never;
8637
+
8638
+ /**
8639
+ * Two {@link map} keys that decode to the same output key.
8640
+ *
8641
+ * @group Collection
8642
+ */
8643
+ export interface MapKeyCollisionIssue {
8644
+ readonly kind: "Collision";
8645
+ readonly index: number;
8646
+ readonly key: unknown;
8647
+ readonly previousIndex: number;
8648
+ readonly previousKey: unknown;
8649
+ readonly outputKey: unknown;
8650
+ }
8651
+
8652
+ type MapStructuralIssue = MapExcessPropertyIssue | MapKeyCollisionIssue;
8653
+
8654
+ type MapStructuralError =
8655
+ MapNotMapError | MapEntriesErrorValue<never, never, MapStructuralIssue>;
8656
+
8657
+ /**
8658
+ * An invalid key, value, or structure in a {@link map}.
8659
+ *
8660
+ * @group Collection
8661
+ */
8662
+ export type MapIssue<
8663
+ KeyError extends TypeError,
8664
+ ValueError extends TypeError,
8665
+ StructuralIssue extends MapStructuralIssue = MapKeyCollisionIssue,
8666
+ > = MapKeyIssue<KeyError> | MapValueIssue<ValueError> | StructuralIssue;
8667
+
8668
+ /**
8669
+ * Entry errors returned by a {@link map} operation.
8670
+ *
8671
+ * @group Collection
8672
+ */
8673
+ export type MapEntriesError<
8674
+ KeyError extends TypeError,
8675
+ ValueError extends TypeError,
8676
+ StructuralIssue extends MapStructuralIssue = MapKeyCollisionIssue,
8677
+ > = [KeyError | ValueError | StructuralIssue] extends [never]
8678
+ ? never
8679
+ : MapEntriesErrorValue<KeyError, ValueError, StructuralIssue>;
8680
+
8681
+ interface MapEntriesErrorValue<
8682
+ KeyError extends TypeError,
8683
+ ValueError extends TypeError,
8684
+ StructuralIssue extends MapStructuralIssue,
8685
+ > extends TypeError<"Map"> {
8686
+ readonly reason: {
8687
+ readonly kind: "Entries";
8688
+ readonly issues: NonEmptyReadonlyArray<
8689
+ MapKeyIssue<KeyError> | MapValueIssue<ValueError> | StructuralIssue
8690
+ >;
8691
+ };
8692
+ }
8693
+
8694
+ const validateMapEntries = (
8695
+ input: ReadonlyMap<unknown, unknown>,
8696
+ validateKey: RuntimeOutputValidation,
8697
+ validateValue: RuntimeOutputValidation,
8698
+ options: ValidationOptions,
8699
+ checkStructure: boolean,
8700
+ ): Result<
8701
+ ReadonlyMap<unknown, unknown>,
8702
+ MapEntriesErrorValue<TypeError, TypeError, MapStructuralIssue>
8703
+ > => {
8704
+ let issues:
8705
+ Array<MapIssue<TypeError, TypeError, MapStructuralIssue>> | undefined;
8706
+ const output = new Map<unknown, unknown>();
8707
+ const inputByOutputKey = new Map<
8708
+ unknown,
8709
+ { readonly index: number; readonly key: unknown }
8710
+ >();
8711
+ let changed = false;
8712
+
8713
+ if (checkStructure) {
8714
+ for (const key of Reflect.ownKeys(input)) {
8715
+ (issues ??= []).push({ kind: "ExcessProperty", key });
8716
+ if (options.errors === "first") break;
8717
+ }
8718
+ }
8719
+
8720
+ let index = 0;
8721
+ for (const [inputKey, inputValue] of input) {
8722
+ if (issues !== undefined && options.errors === "first") break;
8723
+
8724
+ const keyResult = validateKey(inputKey, options);
8725
+ if (!keyResult.ok) {
8726
+ (issues ??= []).push({
8727
+ kind: "Key",
8728
+ index,
8729
+ key: inputKey,
8730
+ error: keyResult.error,
8731
+ });
8732
+ if (options.errors === "first") break;
8733
+ }
8734
+
8735
+ const valueResult = validateValue(inputValue, options);
8736
+ if (!valueResult.ok) {
8737
+ (issues ??= []).push({
8738
+ kind: "Value",
8739
+ index,
8740
+ key: inputKey,
8741
+ error: valueResult.error,
8742
+ });
8743
+ if (options.errors === "first") break;
8744
+ }
8745
+
8746
+ if (!keyResult.ok) {
8747
+ index++;
8748
+ continue;
8749
+ }
8750
+ const outputKey = keyResult.value;
8751
+ const previous = inputByOutputKey.get(outputKey);
8752
+
8753
+ if (previous !== undefined) {
8754
+ (issues ??= []).push({
8755
+ kind: "Collision",
8756
+ index,
8757
+ key: inputKey,
8758
+ previousIndex: previous.index,
8759
+ previousKey: previous.key,
8760
+ outputKey,
8761
+ });
8762
+ if (options.errors === "first") break;
8763
+ index++;
8764
+ continue;
8765
+ }
8766
+
8767
+ inputByOutputKey.set(outputKey, { index, key: inputKey });
8768
+ if (!valueResult.ok) {
8769
+ index++;
8770
+ continue;
8771
+ }
8772
+
8773
+ output.set(outputKey, valueResult.value);
8774
+ if (
8775
+ !globalThis.Object.is(inputKey, outputKey) ||
8776
+ !globalThis.Object.is(inputValue, valueResult.value)
8777
+ ) {
8778
+ changed = true;
8779
+ }
8780
+ index++;
8781
+ }
8782
+
8783
+ return issues === undefined
8784
+ ? ok(changed ? output : input)
8785
+ : err({
8786
+ type: "Map",
8787
+ reason: {
8788
+ kind: "Entries",
8789
+ issues: issues as unknown as NonEmptyReadonlyArray<
8790
+ MapIssue<TypeError, TypeError, MapStructuralIssue>
8791
+ >,
8792
+ },
8793
+ });
8794
+ };
8795
+
8796
+ /**
8797
+ * Tuple {@link Type}.
8798
+ *
8799
+ * Use `tuple(First, Second, ...)` for a fixed-length readonly array in which
8800
+ * every position has its own Type.
8801
+ *
8802
+ * `fromUnknown` validates the Tuple representation and runs every element's
8803
+ * complete Type pipeline. By default, it returns the first issue. Pass `{
8804
+ * errors: "all" }` to collect issues across the whole Tuple.
7947
8805
  *
7948
8806
  * `from` accepts the Tuple Output. When any element Type has a parent,
7949
8807
  * `from.parent` accepts a Tuple of root element Outputs and runs all remaining
@@ -7952,20 +8810,24 @@ const validateSetItems = (
7952
8810
  *
7953
8811
  * A Tuple must be recognized by `Array.isArray`, have exactly the declared
7954
8812
  * length, be dense, and have no own properties other than `length` and the
7955
- * indexed data properties for its elements. Array subclasses, custom-prototype
7956
- * arrays, and foreign-realm arrays are accepted when their own data
7957
- * representation is valid. Sparse arrays, accessor elements, and excess
7958
- * properties are rejected.
8813
+ * indexed data properties for its elements. Foreign-realm arrays are accepted
8814
+ * when their own data representation is valid. Sparse arrays, accessor
8815
+ * elements, and excess properties are rejected.
7959
8816
  *
7960
8817
  * ### Example
7961
8818
  *
7962
8819
  * ```ts
7963
- * import { Int64FromInt64String, String, tuple } from "@evolu/common";
8820
+ * import {
8821
+ * assertOk,
8822
+ * Int64FromInt64String,
8823
+ * String,
8824
+ * tuple,
8825
+ * } from "@evolu/common";
7964
8826
  *
7965
8827
  * const Entry = tuple(String, Int64FromInt64String);
7966
8828
  *
7967
- * expectOk(Entry.fromUnknown(["count", "1"]), ["count", 1n]);
7968
- * expectOk(Entry.from.parent(["count", "1"]), ["count", 1n]);
8829
+ * assertOk(Entry.fromUnknown(["count", "1"]), ["count", 1n]);
8830
+ * assertOk(Entry.from.parent(["count", "1"]), ["count", 1n]);
7969
8831
  * ```
7970
8832
  *
7971
8833
  * @group Collection
@@ -8552,11 +9414,12 @@ type PlainObjectError = ObjectError<
8552
9414
  * A {@link Type} for readonly plain objects with unknown property values.
8553
9415
  *
8554
9416
  * `Object` is the runtime counterpart of a `Readonly<Record<string, unknown>>`
8555
- * data boundary. Its realm-neutral prototype rule accepts a `null` prototype or
8556
- * an immediate root prototype whose own prototype is `null`. Every own property
8557
- * must have a string key and be an enumerable data property. It rejects
8558
- * ordinary class instances, deeper prototype chains, accessors, non-enumerable
8559
- * properties, and symbol properties without reading their values.
9417
+ * data boundary. Its prototype rule uses the realm-neutral structural heuristic
9418
+ * described by {@link isPlainObject}. A matching custom root prototype can be
9419
+ * classified as plain; other custom prototypes and class instances are
9420
+ * rejected. Every own property must have a string key and be an enumerable data
9421
+ * property. Accessors, non-enumerable properties, and symbol properties are
9422
+ * rejected without reading their values.
8560
9423
  *
8561
9424
  * Use {@link object} when property names are fixed, {@link record} when keys and
8562
9425
  * values have their own Types, and {@link instanceOf} when an instance belongs
@@ -8631,7 +9494,7 @@ const _Object: Type<
8631
9494
  }
8632
9495
 
8633
9496
  return errors === undefined
8634
- ? ok(value as Readonly<Record<string, unknown>>)
9497
+ ? ok(value)
8635
9498
  : err({
8636
9499
  type: "Object",
8637
9500
  reason: { kind: "Properties", errors },
@@ -8692,13 +9555,6 @@ const _Object: Type<
8692
9555
  // https://github.com/expo/expo/issues/31167
8693
9556
  export { _Object as Object };
8694
9557
 
8695
- const isPlainObject = (value: object): boolean => {
8696
- const prototype: unknown = globalThis.Object.getPrototypeOf(value);
8697
- return (
8698
- prototype === null || globalThis.Object.getPrototypeOf(prototype) === null
8699
- );
8700
- };
8701
-
8702
9558
  /**
8703
9559
  * Record {@link Type}.
8704
9560
  *
@@ -8715,12 +9571,13 @@ const isPlainObject = (value: object): boolean => {
8715
9571
  * By default, validation returns the first issue. Pass `{ errors: "all" }` to
8716
9572
  * collect issues across the whole Record.
8717
9573
  *
8718
- * A Record must have a `null` prototype or an immediate root prototype whose
8719
- * own prototype is `null`. Ordinary class instances and deeper prototype chains
8720
- * are rejected. Every own property must be an enumerable data property whose
8721
- * key and value satisfy their Types. When decoding or encoding changes an
8722
- * entry, the constructed Record uses a `null` prototype so keys such as
8723
- * `__proto__` remain ordinary data.
9574
+ * A Record must satisfy the realm-neutral structural heuristic described by
9575
+ * {@link isPlainObject}. A matching custom root prototype can be classified as
9576
+ * plain; other custom prototypes and class instances are rejected. Every own
9577
+ * property must be an enumerable data property whose key and value satisfy
9578
+ * their Types. When decoding or encoding changes an entry, the constructed
9579
+ * Record uses a `null` prototype so keys such as `__proto__` remain ordinary
9580
+ * data.
8724
9581
  *
8725
9582
  * If transformed keys collide, validation fails instead of overwriting an
8726
9583
  * entry.
@@ -8729,6 +9586,8 @@ const isPlainObject = (value: object): boolean => {
8729
9586
  *
8730
9587
  * ```ts
8731
9588
  * import {
9589
+ * assertOk,
9590
+ * assertType,
8732
9591
  * Int64FromInt64String,
8733
9592
  * String,
8734
9593
  * record,
@@ -8744,21 +9603,22 @@ const isPlainObject = (value: object): boolean => {
8744
9603
  * grace: "20",
8745
9604
  * });
8746
9605
  *
8747
- * expectOk(scoresFromUnknown, { ada: 10n, grace: 20n });
9606
+ * assertOk(scoresFromUnknown, { ada: 10n, grace: 20n });
8748
9607
  *
8749
9608
  * // Validate keys and values with their root Types.
8750
9609
  * const scoresInput = ScoresByUser.parent.fromUnknown({
8751
9610
  * ada: "10",
8752
9611
  * grace: "20",
8753
9612
  * });
8754
- * expectOk(scoresInput, { ada: "10", grace: "20" });
9613
+ * assertOk(scoresInput, { ada: "10", grace: "20" });
8755
9614
  *
8756
9615
  * // Run the remaining key and value stages.
8757
9616
  * const scoresFromInput = ScoresByUser.from.parent(scoresInput.value);
8758
9617
  *
8759
- * expectOk(scoresFromInput, { ada: 10n, grace: 20n });
8760
- * expectTypeOf(scoresFromInput.value).toEqualTypeOf<
8761
- * Readonly<Partial<Record<string, Int64>>>
9618
+ * assertOk(scoresFromInput, { ada: 10n, grace: 20n });
9619
+ * assertType<
9620
+ * Readonly<Partial<Record<string, Int64>>>,
9621
+ * typeof scoresFromInput.value
8762
9622
  * >();
8763
9623
  * ```
8764
9624
  *
@@ -8767,34 +9627,64 @@ const isPlainObject = (value: object): boolean => {
8767
9627
  * as a Record entry:
8768
9628
  *
8769
9629
  * ```ts
9630
+ * import {
9631
+ * assertEqual,
9632
+ * assertErr,
9633
+ * assertTrue,
9634
+ * trySync,
9635
+ * } from "@evolu/common";
9636
+ *
8770
9637
  * type Values = Partial<Record<"toString", number>>;
8771
9638
  * const values: Values = {};
8772
9639
  *
8773
9640
  * // TypeScript treats the inherited function as `number | undefined`.
8774
9641
  * const value: number | undefined = values.toString;
9642
+ * const valueType = typeof value;
8775
9643
  *
8776
- * expect(typeof value).toBe("function");
8777
- * expect(() => {
8778
- * if (value !== undefined) value.toFixed();
8779
- * }).toThrow(TypeError);
9644
+ * assertEqual(valueType, "function");
9645
+ * const called = trySync(
9646
+ * () => {
9647
+ * if (value !== undefined) value.toFixed(0);
9648
+ * },
9649
+ * (error) => error,
9650
+ * );
9651
+ * assertErr(called);
9652
+ * assertTrue(called.error instanceof TypeError);
8780
9653
  * ```
8781
9654
  *
8782
9655
  * Evolu Record Outputs use the same TypeScript Record representation.
8783
9656
  *
8784
9657
  * ```ts
8785
- * import { Number, literal, record } from "@evolu/common";
9658
+ * import {
9659
+ * assertEqual,
9660
+ * assertErr,
9661
+ * assertOk,
9662
+ * assertTrue,
9663
+ * trySync,
9664
+ * Number,
9665
+ * literal,
9666
+ * record,
9667
+ * type Data,
9668
+ * } from "@evolu/common";
8786
9669
  *
8787
9670
  * const Values = record(literal("toString"), Number);
8788
9671
  * const result = Values.fromUnknown({});
9672
+ * const emptyValues: Data = {};
8789
9673
  *
8790
- * expectOk(result, {});
9674
+ * assertOk(result, emptyValues);
8791
9675
  *
8792
9676
  * const value: number | undefined = result.value.toString;
9677
+ * const valueType = typeof value;
8793
9678
  *
8794
- * expect(typeof value).toBe("function");
8795
- * expect(() => {
8796
- * if (value !== undefined) value.toFixed();
8797
- * }).toThrow(TypeError);
9679
+ * assertEqual(valueType, "function");
9680
+ * const called = trySync(
9681
+ * () => {
9682
+ * if (value !== undefined) value.toFixed(0);
9683
+ * },
9684
+ * (error) => error,
9685
+ * );
9686
+ * assertErr(called);
9687
+ * assertTrue(called.error instanceof TypeError);
8798
9688
  * ```
8799
9689
  *
8800
9690
  * In other words, treat Record Outputs as string-keyed data rather than calling
@@ -8832,7 +9722,7 @@ export const record = <
8832
9722
  }
8833
9723
 
8834
9724
  return validateRecordEntries(
8835
- input as Readonly<Record<string, unknown>>,
9725
+ input,
8836
9726
  validateKey as (
8837
9727
  value: unknown,
8838
9728
  options: ValidationOptions,
@@ -8911,7 +9801,7 @@ export const record = <
8911
9801
  encodeKey === identity && encodeValue === identity
8912
9802
  ? identity
8913
9803
  : (input: Readonly<Record<string, unknown>>) => {
8914
- const output = createMutableRecord<string, unknown>();
9804
+ const output = createMutableRecord();
8915
9805
  let changed = false;
8916
9806
 
8917
9807
  for (const inputKey of globalThis.Object.keys(input)) {
@@ -9269,7 +10159,7 @@ const validateRecordEntries = (
9269
10159
  > => {
9270
10160
  let issues:
9271
10161
  Array<RecordIssue<TypeError, TypeError, RecordStructuralIssue>> | undefined;
9272
- const output = createMutableRecord<string, unknown>();
10162
+ const output = createMutableRecord();
9273
10163
  const inputKeyByOutputKey = createMutableRecord<string, string | symbol>();
9274
10164
  let changed = false;
9275
10165
 
@@ -9376,7 +10266,13 @@ export interface OptionalProperty<T extends TypeNode> {
9376
10266
  * ### Example
9377
10267
  *
9378
10268
  * ```ts
9379
- * import { String, object, optional, undefinedOr } from "@evolu/common";
10269
+ * import {
10270
+ * assertOk,
10271
+ * String,
10272
+ * object,
10273
+ * optional,
10274
+ * undefinedOr,
10275
+ * } from "@evolu/common";
9380
10276
  *
9381
10277
  * const User = object({
9382
10278
  * name: String,
@@ -9384,8 +10280,8 @@ export interface OptionalProperty<T extends TypeNode> {
9384
10280
  * preferredName: optional(undefinedOr(String)),
9385
10281
  * });
9386
10282
  *
9387
- * expectOk(User.fromUnknown({ name: "Ada" }), { name: "Ada" });
9388
- * expectOk(User.fromUnknown({ name: "Ada", preferredName: undefined }), {
10283
+ * assertOk(User.fromUnknown({ name: "Ada" }), { name: "Ada" });
10284
+ * assertOk(User.fromUnknown({ name: "Ada", preferredName: undefined }), {
9389
10285
  * name: "Ada",
9390
10286
  * preferredName: undefined,
9391
10287
  * });
@@ -9442,10 +10338,11 @@ type ObjectProperty = ObjectProps[string];
9442
10338
  * required properties nor make optional properties present. This keeps Outputs
9443
10339
  * valid after ordinary object spread restores `Object.prototype`.
9444
10340
  *
9445
- * A `null` prototype or an immediate root prototype whose own prototype is
9446
- * `null` is accepted. Ordinary class instances, deeper prototype chains,
9447
- * accessors, and non-enumerable properties are rejected. Use {@link instanceOf}
9448
- * for class Outputs or {@link transform} to decode instances into plain data.
10341
+ * The prototype must satisfy the realm-neutral structural heuristic described
10342
+ * by {@link isPlainObject}. A matching custom root prototype can be classified
10343
+ * as plain; other custom prototypes, class instances, accessors, and
10344
+ * non-enumerable properties are rejected. Use {@link instanceOf} for class
10345
+ * Outputs or {@link transform} to decode instances into plain data.
9449
10346
  *
9450
10347
  * When decoding changes no property, it preserves the input. When decoding or
9451
10348
  * encoding changes a property, it constructs an object with a `null`
@@ -9458,6 +10355,8 @@ type ObjectProperty = ObjectProps[string];
9458
10355
  *
9459
10356
  * ```ts
9460
10357
  * import {
10358
+ * assertOk,
10359
+ * assertType,
9461
10360
  * Int64FromInt64String,
9462
10361
  * String,
9463
10362
  * object,
@@ -9476,20 +10375,20 @@ type ObjectProperty = ObjectProps[string];
9476
10375
  * loginCount: "42",
9477
10376
  * });
9478
10377
  *
9479
- * expectOk(userFromUnknown, { name: "Ada", loginCount: 42n });
10378
+ * assertOk(userFromUnknown, { name: "Ada", loginCount: 42n });
9480
10379
  *
9481
10380
  * // Validate the object and root property Types.
9482
10381
  * const userInput = User.parent.fromUnknown({
9483
10382
  * name: "Ada",
9484
10383
  * loginCount: "42",
9485
10384
  * });
9486
- * expectOk(userInput, { name: "Ada", loginCount: "42" });
10385
+ * assertOk(userInput, { name: "Ada", loginCount: "42" });
9487
10386
  *
9488
10387
  * // Run the remaining property stages.
9489
10388
  * const userFromInput = User.from.parent(userInput.value);
9490
10389
  *
9491
- * expectOk(userFromInput, { name: "Ada", loginCount: 42n });
9492
- * expectTypeOf(userFromInput.value).toExtend<User>();
10390
+ * assertOk(userFromInput, { name: "Ada", loginCount: 42n });
10391
+ * assertType<typeof User.Output, typeof userFromInput.value>();
9493
10392
  * ```
9494
10393
  *
9495
10394
  * Note that TypeScript does not model an object's runtime prototype. This can
@@ -9497,38 +10396,68 @@ type ObjectProperty = ObjectProps[string];
9497
10396
  * as an object property:
9498
10397
  *
9499
10398
  * ```ts
10399
+ * import {
10400
+ * assertEqual,
10401
+ * assertErr,
10402
+ * assertTrue,
10403
+ * trySync,
10404
+ * } from "@evolu/common";
10405
+ *
9500
10406
  * interface Values {
9501
10407
  * readonly toString?: number;
9502
10408
  * }
9503
10409
  *
9504
- * const nullPrototypeValues = globalThis.Object.create(null) as Values;
10410
+ * const nullPrototypeValues = Object.create(null) as Values;
9505
10411
  * const values = { ...nullPrototypeValues };
9506
10412
  *
9507
10413
  * // TypeScript treats the inherited function as `number | undefined`.
9508
10414
  * const value: number | undefined = values.toString;
10415
+ * const valueType = typeof value;
9509
10416
  *
9510
- * expect(typeof value).toBe("function");
9511
- * expect(() => {
9512
- * if (value !== undefined) value.toFixed();
9513
- * }).toThrow(TypeError);
10417
+ * assertEqual(valueType, "function");
10418
+ * const called = trySync(
10419
+ * () => {
10420
+ * if (value !== undefined) value.toFixed(0);
10421
+ * },
10422
+ * (error) => error,
10423
+ * );
10424
+ * assertErr(called);
10425
+ * assertTrue(called.error instanceof TypeError);
9514
10426
  * ```
9515
10427
  *
9516
10428
  * Evolu Object Outputs use the same TypeScript object representation.
9517
10429
  *
9518
10430
  * ```ts
9519
- * import { Number, object, optional } from "@evolu/common";
10431
+ * import {
10432
+ * assertEqual,
10433
+ * assertErr,
10434
+ * assertOk,
10435
+ * assertTrue,
10436
+ * trySync,
10437
+ * Number,
10438
+ * object,
10439
+ * optional,
10440
+ * type Data,
10441
+ * } from "@evolu/common";
9520
10442
  *
9521
10443
  * const Values = object({ toString: optional(Number) });
9522
10444
  * const result = Values.fromUnknown({});
10445
+ * const emptyValues: Data = {};
9523
10446
  *
9524
- * expectOk(result, {});
10447
+ * assertOk(result, emptyValues);
9525
10448
  *
9526
10449
  * const value: number | undefined = result.value.toString;
10450
+ * const valueType = typeof value;
9527
10451
  *
9528
- * expect(typeof value).toBe("function");
9529
- * expect(() => {
9530
- * if (value !== undefined) value.toFixed();
9531
- * }).toThrow(TypeError);
10452
+ * assertEqual(valueType, "function");
10453
+ * const called = trySync(
10454
+ * () => {
10455
+ * if (value !== undefined) value.toFixed(0);
10456
+ * },
10457
+ * (error) => error,
10458
+ * );
10459
+ * assertErr(called);
10460
+ * assertTrue(called.error instanceof TypeError);
9532
10461
  * ```
9533
10462
  *
9534
10463
  * In other words, treat Object Outputs as data rather than calling inherited
@@ -9543,7 +10472,36 @@ export function object<const Props extends ObjectProps>(
9543
10472
  : [ValidationFailure<ObjectValidationError<Props>>]
9544
10473
  ): StrictObjectType<Props>;
9545
10474
 
9546
- /** Creates an Object Type with additional record properties. */
10475
+ /**
10476
+ * Creates an Object Type with additional record properties.
10477
+ *
10478
+ * ### Example
10479
+ *
10480
+ * ```ts
10481
+ * import {
10482
+ * assertOk,
10483
+ * assertType,
10484
+ * String,
10485
+ * object,
10486
+ * record,
10487
+ * } from "@evolu/common";
10488
+ *
10489
+ * const RequestHeaders = object(
10490
+ * { authorization: String },
10491
+ * record(String, String),
10492
+ * );
10493
+ * const result = RequestHeaders.fromUnknown({
10494
+ * authorization: "Bearer token",
10495
+ * "x-request-id": "request-1",
10496
+ * });
10497
+ *
10498
+ * assertOk(result, {
10499
+ * authorization: "Bearer token",
10500
+ * "x-request-id": "request-1",
10501
+ * });
10502
+ * assertType<string | undefined, (typeof result.value)["x-request-id"]>();
10503
+ * ```
10504
+ */
9547
10505
  export function object<
9548
10506
  const Props extends ObjectProps,
9549
10507
  const Rest extends RecordTypeNode & ConcreteTypeNode,
@@ -10382,12 +11340,10 @@ export interface ObjectNotObjectError extends TypeError<"Object"> {
10382
11340
  * An error returned when an {@link object} input falls outside its supported
10383
11341
  * plain-object prototype boundary.
10384
11342
  *
10385
- * Object Types accept a `null` prototype or a prototype whose own prototype is
10386
- * `null`. This includes ordinary and cross-realm plain objects as well as
10387
- * objects created from an immediate root prototype. Arrays, JavaScript built-in
10388
- * objects, class instances, and objects with deeper custom prototype chains
10389
- * return this error instead of having their prototype or inherited state
10390
- * discarded. `reason.value` is the rejected object.
11343
+ * Object Types use the realm-neutral structural heuristic described by
11344
+ * {@link isPlainObject}. Values that do not satisfy it return this error instead
11345
+ * of having their prototype or inherited state discarded. `reason.value` is the
11346
+ * rejected object.
10391
11347
  *
10392
11348
  * @group Objects
10393
11349
  */
@@ -10547,13 +11503,20 @@ const createRecordPropertyError = <Error extends TypeError>(
10547
11503
  * ### Example
10548
11504
  *
10549
11505
  * ```ts
10550
- * import { PositiveInt, String, partial } from "@evolu/common";
11506
+ * import {
11507
+ * assertFalse,
11508
+ * assertOk,
11509
+ * PositiveInt,
11510
+ * String,
11511
+ * partial,
11512
+ * } from "@evolu/common";
10551
11513
  *
10552
11514
  * const PartialUser = partial({ name: String, age: PositiveInt });
11515
+ * const emptyPartialUser: typeof PartialUser.Output = {};
10553
11516
  *
10554
- * expectOk(PartialUser.fromUnknown({}), {});
10555
- * expectOk(PartialUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
10556
- * expect(PartialUser.fromUnknown({ age: -1 }).ok).toBe(false);
11517
+ * assertOk(PartialUser.fromUnknown({}), emptyPartialUser);
11518
+ * assertOk(PartialUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
11519
+ * assertFalse(PartialUser.fromUnknown({ age: -1 }).ok);
10557
11520
  * ```
10558
11521
  *
10559
11522
  * @group Objects
@@ -10603,15 +11566,20 @@ export type PartialObjectProps<Props extends ObjectProps> = {
10603
11566
  * ### Example
10604
11567
  *
10605
11568
  * ```ts
10606
- * import { String, nullOr, nullableToOptional } from "@evolu/common";
11569
+ * import {
11570
+ * assertOk,
11571
+ * String,
11572
+ * nullOr,
11573
+ * nullableToOptional,
11574
+ * } from "@evolu/common";
10607
11575
  *
10608
11576
  * const User = nullableToOptional({
10609
11577
  * name: String,
10610
11578
  * nickname: nullOr(String),
10611
11579
  * });
10612
11580
  *
10613
- * expectOk(User.fromUnknown({ name: "Ada" }), { name: "Ada" });
10614
- * expectOk(User.fromUnknown({ name: "Ada", nickname: null }), {
11581
+ * assertOk(User.fromUnknown({ name: "Ada" }), { name: "Ada" });
11582
+ * assertOk(User.fromUnknown({ name: "Ada", nickname: null }), {
10615
11583
  * name: "Ada",
10616
11584
  * nickname: null,
10617
11585
  * });
@@ -10675,12 +11643,12 @@ export type NullableToOptionalProps<Props extends ObjectProps> = {
10675
11643
  * ### Example
10676
11644
  *
10677
11645
  * ```ts
10678
- * import { String, object, omit } from "@evolu/common";
11646
+ * import { assertOk, String, object, omit } from "@evolu/common";
10679
11647
  *
10680
11648
  * const User = object({ name: String, password: String });
10681
11649
  * const PublicUser = omit(User, "password");
10682
11650
  *
10683
- * expectOk(PublicUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
11651
+ * assertOk(PublicUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
10684
11652
  * ```
10685
11653
  *
10686
11654
  * @group Objects
@@ -10746,6 +11714,7 @@ type OmitKeyConcreteTypeError = CompileTimeError<
10746
11714
  *
10747
11715
  * ```ts
10748
11716
  * import {
11717
+ * assertOk,
10749
11718
  * NonNegativeInt,
10750
11719
  * String,
10751
11720
  * object,
@@ -10763,8 +11732,8 @@ type OmitKeyConcreteTypeError = CompileTimeError<
10763
11732
  * value: { timestamp: 42 },
10764
11733
  * });
10765
11734
  *
10766
- * expectOk(validated, { ok: true, value: { timestamp: 42 } });
10767
- * expectOk(validated.value, { timestamp: 42 });
11735
+ * assertOk(validated, { ok: true, value: { timestamp: 42 } });
11736
+ * assertOk(validated.value, { timestamp: 42 });
10768
11737
  * ```
10769
11738
  *
10770
11739
  * @group Results
@@ -10831,16 +11800,31 @@ export type UnknownResult = typeof UnknownResult.Output;
10831
11800
  * ### Example
10832
11801
  *
10833
11802
  * ```ts
10834
- * import { String, typed } from "@evolu/common";
11803
+ * import {
11804
+ * assertFalse,
11805
+ * assertOk,
11806
+ * assertType,
11807
+ * String,
11808
+ * discriminatedUnion,
11809
+ * typed,
11810
+ * } from "@evolu/common";
10835
11811
  *
10836
- * const Pending = typed("Pending", {
10837
- * label: String,
10838
- * });
11812
+ * const Loading = typed("Loading");
11813
+ * const Loaded = typed("Loaded", { value: String });
11814
+ * const State = discriminatedUnion(Loading, Loaded);
10839
11815
  *
10840
- * expectOk(Pending.fromUnknown({ type: "Pending", label: "Waiting" }), {
10841
- * type: "Pending",
10842
- * label: "Waiting",
11816
+ * assertOk(State.fromUnknown({ type: "Loading" }), { type: "Loading" });
11817
+ * assertOk(State.fromUnknown({ type: "Loaded", value: "Evolu" }), {
11818
+ * type: "Loaded",
11819
+ * value: "Evolu",
10843
11820
  * });
11821
+ * assertFalse(Loading.is({ type: "Loading", progress: 1 }));
11822
+ * assertType<
11823
+ * true,
11824
+ * typeof Loading.Output extends { readonly type: "Loading" }
11825
+ * ? true
11826
+ * : false
11827
+ * >();
10844
11828
  * ```
10845
11829
  *
10846
11830
  * @group Discriminated unions
@@ -10849,7 +11833,24 @@ export function typed<const Tag extends TypeName>(
10849
11833
  tag: ValidateTypedTag<Tag>,
10850
11834
  ): TypedType<Tag>;
10851
11835
 
10852
- /** Creates a Tagged Object Type with declared properties. */
11836
+ /**
11837
+ * Creates a Tagged Object Type with declared properties.
11838
+ *
11839
+ * ### Example
11840
+ *
11841
+ * ```ts
11842
+ * import { assertOk, String, typed } from "@evolu/common";
11843
+ *
11844
+ * const Pending = typed("Pending", {
11845
+ * label: String,
11846
+ * });
11847
+ *
11848
+ * assertOk(Pending.fromUnknown({ type: "Pending", label: "Waiting" }), {
11849
+ * type: "Pending",
11850
+ * label: "Waiting",
11851
+ * });
11852
+ * ```
11853
+ */
10853
11854
  export function typed<
10854
11855
  const Tag extends TypeName,
10855
11856
  const Props extends ObjectProps,
@@ -10861,7 +11862,35 @@ export function typed<
10861
11862
  : [ValidationFailure<TypedValidationError<Props>>]
10862
11863
  ): TypedType<Tag, Props>;
10863
11864
 
10864
- /** Creates a Tagged Object Type with additional record properties. */
11865
+ /**
11866
+ * Creates a Tagged Object Type with additional record properties.
11867
+ *
11868
+ * ### Example
11869
+ *
11870
+ * ```ts
11871
+ * import {
11872
+ * assertOk,
11873
+ * assertType,
11874
+ * String,
11875
+ * record,
11876
+ * typed,
11877
+ * } from "@evolu/common";
11878
+ *
11879
+ * const Open = typed("Open", { label: String }, record(String, String));
11880
+ * const result = Open.fromUnknown({
11881
+ * type: "Open",
11882
+ * label: "Ready",
11883
+ * note: "Connected",
11884
+ * });
11885
+ *
11886
+ * assertOk(result, {
11887
+ * type: "Open",
11888
+ * label: "Ready",
11889
+ * note: "Connected",
11890
+ * });
11891
+ * assertType<string | undefined, typeof result.value.note>();
11892
+ * ```
11893
+ */
10865
11894
  export function typed<
10866
11895
  const Tag extends TypeName,
10867
11896
  const Props extends ObjectProps,
@@ -10919,7 +11948,14 @@ export function typed(
10919
11948
  * ### Example
10920
11949
  *
10921
11950
  * ```ts
10922
- * import { err, ok, type Result, type Typed } from "@evolu/common";
11951
+ * import {
11952
+ * assertErr,
11953
+ * assertOk,
11954
+ * err,
11955
+ * ok,
11956
+ * type Result,
11957
+ * type Typed,
11958
+ * } from "@evolu/common";
10923
11959
  *
10924
11960
  * interface User extends Typed<"User"> {
10925
11961
  * readonly id: string;
@@ -10934,8 +11970,8 @@ export function typed(
10934
11970
  * readonly id: string;
10935
11971
  * }
10936
11972
  *
10937
- * expectOk(getUser("user-1"), { type: "User", id: "user-1" });
10938
- * expectErr(getUser("missing"), { type: "UserNotFound", id: "missing" });
11973
+ * assertOk(getUser("user-1"), { type: "User", id: "user-1" });
11974
+ * assertErr(getUser("missing"), { type: "UserNotFound", id: "missing" });
10939
11975
  * ```
10940
11976
  *
10941
11977
  * @group Discriminated unions
@@ -10955,6 +11991,7 @@ export interface Typed<Tag extends TypeName> {
10955
11991
  *
10956
11992
  * ```ts
10957
11993
  * import {
11994
+ * assertType,
10958
11995
  * String,
10959
11996
  * discriminatedUnion,
10960
11997
  * typed,
@@ -10968,10 +12005,10 @@ export interface Typed<Tag extends TypeName> {
10968
12005
  *
10969
12006
  * type CreateMessage = ExtractTyped<Message, "Create">;
10970
12007
  *
10971
- * expectTypeOf<CreateMessage>().toEqualTypeOf<typeof Create.Output>();
12008
+ * assertType<typeof Create.Output, CreateMessage>();
10972
12009
  *
10973
12010
  * // @ts-expect-error "Cretae" is not a Message type.
10974
- * type Typo = ExtractTyped<Message, "Cretae">;
12011
+ * type _Typo = ExtractTyped<Message, "Cretae">;
10975
12012
  * ```
10976
12013
  *
10977
12014
  * @group Discriminated unions
@@ -11029,7 +12066,7 @@ type TypedTypePropertyError = CompileTimeError<
11029
12066
  * ### Example
11030
12067
  *
11031
12068
  * ```ts
11032
- * import { String, nextResult, typed } from "@evolu/common";
12069
+ * import { assertEqual, String, nextResult, typed } from "@evolu/common";
11033
12070
  *
11034
12071
  * const StringNextResult = nextResult(
11035
12072
  * String,
@@ -11047,19 +12084,21 @@ type TypedTypePropertyError = CompileTimeError<
11047
12084
  * return `Error: ${result.error.message}`;
11048
12085
  * };
11049
12086
  *
11050
- * expect(describeNext({ ok: true, value: "item" })).toBe("Value: item");
11051
- * expect(
12087
+ * assertEqual(describeNext({ ok: true, value: "item" }), "Value: item");
12088
+ * assertEqual(
11052
12089
  * describeNext({
11053
12090
  * ok: false,
11054
12091
  * error: { type: "Done", done: "complete" },
11055
12092
  * }),
11056
- * ).toBe("Done: complete");
11057
- * expect(
12093
+ * "Done: complete",
12094
+ * );
12095
+ * assertEqual(
11058
12096
  * describeNext({
11059
12097
  * ok: false,
11060
12098
  * error: { type: "ReadFailed", message: "Offline" },
11061
12099
  * }),
11062
- * ).toBe("Error: Offline");
12100
+ * "Error: Offline",
12101
+ * );
11063
12102
  * ```
11064
12103
  *
11065
12104
  * @group Results
@@ -11135,13 +12174,18 @@ export type UnknownNextResult = typeof UnknownNextResult.Output;
11135
12174
  * ### Example
11136
12175
  *
11137
12176
  * ```ts
11138
- * import { String, discriminatedUnion, typed } from "@evolu/common";
12177
+ * import {
12178
+ * assertOk,
12179
+ * String,
12180
+ * discriminatedUnion,
12181
+ * typed,
12182
+ * } from "@evolu/common";
11139
12183
  *
11140
12184
  * const Created = typed("Created", { id: String });
11141
12185
  * const Deleted = typed("Deleted", { id: String });
11142
12186
  * const Event = discriminatedUnion(Created, Deleted);
11143
12187
  *
11144
- * expectOk(Event.fromUnknown({ type: "Created", id: "id" }), {
12188
+ * assertOk(Event.fromUnknown({ type: "Created", id: "id" }), {
11145
12189
  * type: "Created",
11146
12190
  * id: "id",
11147
12191
  * });
@@ -11162,6 +12206,7 @@ export function discriminatedUnion<
11162
12206
  *
11163
12207
  * ```ts
11164
12208
  * import {
12209
+ * assertOk,
11165
12210
  * Number,
11166
12211
  * String,
11167
12212
  * discriminatedUnion,
@@ -11173,11 +12218,11 @@ export function discriminatedUnion<
11173
12218
  * const Removed = object({ kind: literal("removed"), id: Number });
11174
12219
  * const Event = discriminatedUnion("kind", Added, Removed);
11175
12220
  *
11176
- * expectOk(Event.fromUnknown({ kind: "added", value: "Evolu" }), {
12221
+ * assertOk(Event.fromUnknown({ kind: "added", value: "Evolu" }), {
11177
12222
  * kind: "added",
11178
12223
  * value: "Evolu",
11179
12224
  * });
11180
- * expectOk(Event.fromUnknown({ kind: "removed", id: 1 }), {
12225
+ * assertOk(Event.fromUnknown({ kind: "removed", id: 1 }), {
11181
12226
  * kind: "removed",
11182
12227
  * id: 1,
11183
12228
  * });
@@ -11224,7 +12269,7 @@ export function discriminatedUnion(
11224
12269
  type: "Object",
11225
12270
  reason: { kind: "UnexpectedPrototype", value },
11226
12271
  })
11227
- : ok(value as Readonly<Record<string, unknown>>);
12272
+ : ok(value);
11228
12273
 
11229
12274
  if (!objectResult.ok) {
11230
12275
  return err({
@@ -11242,19 +12287,19 @@ export function discriminatedUnion(
11242
12287
  return err({
11243
12288
  type: "DiscriminatedUnion",
11244
12289
  reason: { kind: "PropertyAccess", key, reason: "Inherited" },
11245
- } satisfies DiscriminatedUnionPropertyAccessError<string>);
12290
+ } satisfies DiscriminatedUnionPropertyAccessError);
11246
12291
  }
11247
12292
  discriminator = undefined;
11248
12293
  } else if (!("value" in descriptor)) {
11249
12294
  return err({
11250
12295
  type: "DiscriminatedUnion",
11251
12296
  reason: { kind: "PropertyAccess", key, reason: "Accessor" },
11252
- } satisfies DiscriminatedUnionPropertyAccessError<string>);
12297
+ } satisfies DiscriminatedUnionPropertyAccessError);
11253
12298
  } else if (!descriptor.enumerable) {
11254
12299
  return err({
11255
12300
  type: "DiscriminatedUnion",
11256
12301
  reason: { kind: "PropertyAccess", key, reason: "NonEnumerable" },
11257
- } satisfies DiscriminatedUnionPropertyAccessError<string>);
12302
+ } satisfies DiscriminatedUnionPropertyAccessError);
11258
12303
  } else {
11259
12304
  discriminator = descriptor.value;
11260
12305
  }
@@ -11778,15 +12823,18 @@ type RuntimeDiscriminatedUnionMember = RuntimeObjectTypeNode & {
11778
12823
  *
11779
12824
  * The definition must return one concrete non-Lazy Type. Use {@link union} for
11780
12825
  * alternatives. Every recursive Lazy reference must be nested behind an
11781
- * {@link object}, {@link array}, {@link tuple}, {@link record}, or {@link set}
11782
- * structural boundary. Union does not guard recursion because it passes the
11783
- * same value to every member. Lazy defers schema construction; it does not make
11784
- * cyclic runtime object graphs or arbitrarily deep values stack-safe.
12826
+ * {@link object}, {@link array}, {@link tuple}, {@link record}, {@link set}, or
12827
+ * {@link map} structural boundary. Union does not guard recursion because it
12828
+ * passes the same value to every member. Lazy defers schema construction; it
12829
+ * does not make cyclic runtime object graphs or arbitrarily deep values
12830
+ * stack-safe.
11785
12831
  *
11786
12832
  * ### Example
11787
12833
  *
11788
12834
  * ```ts
11789
12835
  * import {
12836
+ * assertOk,
12837
+ * assertType,
11790
12838
  * String,
11791
12839
  * array,
11792
12840
  * lazy,
@@ -11816,11 +12864,11 @@ type RuntimeDiscriminatedUnionMember = RuntimeObjectTypeNode & {
11816
12864
  * children: [{ value: "leaf", children: [] }],
11817
12865
  * });
11818
12866
  *
11819
- * expectOk(result, {
12867
+ * assertOk(result, {
11820
12868
  * value: "root",
11821
12869
  * children: [{ value: "leaf", children: [] }],
11822
12870
  * });
11823
- * expectTypeOf(result.value).toEqualTypeOf<Tree>();
12871
+ * assertType<Tree, typeof result.value>();
11824
12872
  * ```
11825
12873
  *
11826
12874
  * @group Recursive
@@ -11963,58 +13011,610 @@ export interface LazyType<
11963
13011
  Type<"Lazy", Input, Input, InputError, null, InputError, never, Input>,
11964
13012
  Errors,
11965
13013
  never,
11966
- CanonicalInput,
11967
- UsesIdentityEncoding
11968
- > {
11969
- readonly [lazyTypeSymbol]: true;
11970
- /** Formats an error returned by any Lazy Type decoding operation. */
11971
- readonly formatError: TypeErrorFormatter<Errors | FromError>;
13014
+ CanonicalInput,
13015
+ UsesIdentityEncoding
13016
+ > {
13017
+ readonly [lazyTypeSymbol]: true;
13018
+ /** Formats an error returned by any Lazy Type decoding operation. */
13019
+ readonly formatError: TypeErrorFormatter<Errors | FromError>;
13020
+ }
13021
+
13022
+ type ValidateLazyTarget<Target extends ConcreteTypeNode> =
13023
+ IsUnion<Target> extends false ? Target : LazyTargetConcreteTypeError;
13024
+
13025
+ type LazyTargetConcreteTypeError = CompileTimeError<
13026
+ "Type",
13027
+ "Lazy Type definition must return one concrete Type node. Pass a Union Type node instead of a union of Type nodes."
13028
+ >;
13029
+
13030
+ type LazyResolution =
13031
+ | { readonly state: "unresolved" }
13032
+ | { readonly state: "resolving" }
13033
+ | ResolvedLazyType
13034
+ | { readonly state: "failed"; readonly error: unknown };
13035
+
13036
+ interface ResolvedLazyType {
13037
+ readonly state: "resolved";
13038
+ readonly target: RuntimeTypeNode;
13039
+ readonly targetFromInput: RuntimeOperation<Result<unknown, TypeError>>;
13040
+ readonly root: RuntimeTypeNode;
13041
+ }
13042
+
13043
+ const lazyTypeNodes = /*#__PURE__*/ new WeakSet<TypeNode>();
13044
+
13045
+ const assertLazyReferencesAreGuarded = (type: RuntimeTypeNode): void => {
13046
+ for (;;) {
13047
+ assert(
13048
+ !lazyTypeNodes.has(type),
13049
+ "A Lazy Type definition must place every Lazy Type behind a structural boundary.",
13050
+ );
13051
+
13052
+ if ("members" in type && Array.isArray(type.members)) {
13053
+ for (const member of type.members as ReadonlyArray<RuntimeTypeNode>)
13054
+ assertLazyReferencesAreGuarded(member);
13055
+ }
13056
+
13057
+ if ("output" in type) {
13058
+ assertLazyReferencesAreGuarded(type.output as RuntimeTypeNode);
13059
+ }
13060
+
13061
+ if (!type.parent) return;
13062
+ type = type.parent as RuntimeTypeNode;
13063
+ }
13064
+ };
13065
+
13066
+ /**
13067
+ * Evolu's recursive platform-independent structured-cloneable data domain.
13068
+ *
13069
+ * Data is intentionally limited to values supported by the structured clone
13070
+ * algorithm, so it can be sent through APIs such as worker `postMessage`. It
13071
+ * includes supported JavaScript primitives and the runtime representations
13072
+ * defined by {@link array}, {@link Object}, {@link set}, {@link map}, {@link Date},
13073
+ * and {@link Uint8Array}. Array elements, Object properties, Set elements, and
13074
+ * Map keys and values must also be Data. Cyclic and shared data graphs are
13075
+ * supported. Raw {@link ArrayBuffer} values are excluded; represent bytes with a
13076
+ * Uint8Array.
13077
+ *
13078
+ * Functions, arbitrary class instances, and other behavioral objects are not
13079
+ * Data. The structural representations follow the corresponding Evolu Type
13080
+ * rules. As with every Type operation, classification assumes trusted
13081
+ * application code or audited dependencies; it is not a security boundary for
13082
+ * adversarial Proxies or forged built-in object tags.
13083
+ *
13084
+ * TypeScript cannot express dense Arrays, exact property descriptors, object
13085
+ * prototypes, or the absence of custom properties on built-ins. Use the runtime
13086
+ * {@link Data} Type when a value crosses an unknown boundary.
13087
+ *
13088
+ * ### Example
13089
+ *
13090
+ * ```ts
13091
+ * import { assertOk, assertSame, assertType, Data } from "@evolu/common";
13092
+ *
13093
+ * const value: unknown = {
13094
+ * user: { name: "Ada" },
13095
+ * scores: new Map([["logic", 100]]),
13096
+ * roles: new Set(["admin"]),
13097
+ * };
13098
+ * const result = Data.fromUnknown(value);
13099
+ *
13100
+ * assertOk(result);
13101
+ * assertSame(result.value, value);
13102
+ * assertType<Data, typeof result.value>();
13103
+ * ```
13104
+ *
13105
+ * @group Base
13106
+ */
13107
+ export type Data =
13108
+ | undefined
13109
+ | null
13110
+ | string
13111
+ | number
13112
+ | bigint
13113
+ | boolean
13114
+ | ReadonlyArray<Data>
13115
+ | { readonly [key: string]: Data }
13116
+ | ReadonlySet<Data>
13117
+ | ReadonlyMap<Data, Data>
13118
+ | globalThis.Date
13119
+ | globalThis.Uint8Array;
13120
+
13121
+ /**
13122
+ * Returns whether a TypeScript type consists only of {@link Data}.
13123
+ *
13124
+ * Unlike `Value extends Data`, this recursively checks the declared properties
13125
+ * of object types, so ordinary interfaces do not need a string index signature.
13126
+ * This is a compile-time approximation of the Data domain; representation
13127
+ * details such as prototypes and property descriptors still require the runtime
13128
+ * {@link Data} Type.
13129
+ *
13130
+ * Recursive object types are supported.
13131
+ *
13132
+ * ### Example
13133
+ *
13134
+ * ```ts
13135
+ * import { assertType, type IsData } from "@evolu/common";
13136
+ *
13137
+ * interface User {
13138
+ * readonly name: string;
13139
+ * readonly roles: ReadonlySet<string>;
13140
+ * }
13141
+ *
13142
+ * interface Service {
13143
+ * readonly run: () => void;
13144
+ * }
13145
+ *
13146
+ * assertType<true, IsData<User>>();
13147
+ * assertType<false, IsData<Service>>();
13148
+ * ```
13149
+ *
13150
+ * @group Base
13151
+ */
13152
+ export type IsData<Value> = IsDataValue<Value, readonly []>;
13153
+
13154
+ type IsDataValue<Value, Seen extends ReadonlyArray<unknown>> =
13155
+ IsAny<Value> extends true
13156
+ ? false
13157
+ : unknown extends Value
13158
+ ? false
13159
+ : [Value] extends [never]
13160
+ ? true
13161
+ : false extends (
13162
+ Value extends unknown ? IsDataMember<Value, Seen> : never
13163
+ )
13164
+ ? false
13165
+ : true;
13166
+
13167
+ type IsAny<Value> = 0 extends 1 & Value ? true : false;
13168
+
13169
+ type IsDataMember<Value, Seen extends ReadonlyArray<unknown>> =
13170
+ IsSameType<Value, object> extends true
13171
+ ? false
13172
+ : IncludesSameType<Seen, Value> extends true
13173
+ ? true
13174
+ : Value extends void | null | string | number | bigint | boolean
13175
+ ? true
13176
+ : Value extends globalThis.Function
13177
+ ? false
13178
+ : Value extends globalThis.Date | globalThis.Uint8Array
13179
+ ? true
13180
+ : Value extends ReadonlyArray<infer Element>
13181
+ ? IsDataValue<Element, readonly [...Seen, Value]>
13182
+ : Value extends ReadonlySet<infer Element>
13183
+ ? IsDataValue<Element, readonly [...Seen, Value]>
13184
+ : Value extends ReadonlyMap<infer Key, infer MapValue>
13185
+ ? IsDataValue<Key | MapValue, readonly [...Seen, Value]>
13186
+ : Value extends object
13187
+ ? Extract<keyof Value, symbol> extends never
13188
+ ? IsDataObject<Value, readonly [...Seen, Value]>
13189
+ : false
13190
+ : false;
13191
+
13192
+ type IsDataObject<Value extends object, Seen extends ReadonlyArray<unknown>> = [
13193
+ keyof Value,
13194
+ ] extends [never]
13195
+ ? false
13196
+ : false extends {
13197
+ [Key in keyof Value]-?: IsDataValue<Value[Key], Seen>;
13198
+ }[keyof Value]
13199
+ ? false
13200
+ : true;
13201
+
13202
+ type IncludesSameType<
13203
+ Values extends ReadonlyArray<unknown>,
13204
+ Value,
13205
+ > = Values extends readonly [infer First, ...infer Rest]
13206
+ ? IsSameType<First, Value> extends true
13207
+ ? true
13208
+ : IncludesSameType<Rest, Value>
13209
+ : false;
13210
+
13211
+ /**
13212
+ * One issue found while validating a candidate as {@link Data}.
13213
+ *
13214
+ * @group Base
13215
+ */
13216
+ export type DataIssue =
13217
+ | {
13218
+ readonly kind: "InvalidType";
13219
+ readonly path: ReadonlyArray<PropertyKey>;
13220
+ readonly value: unknown;
13221
+ }
13222
+ | {
13223
+ readonly kind: "UnexpectedPrototype";
13224
+ readonly path: ReadonlyArray<PropertyKey>;
13225
+ readonly container: "Object";
13226
+ readonly value: object;
13227
+ }
13228
+ | {
13229
+ readonly kind: "Accessor";
13230
+ readonly path: ReadonlyArray<PropertyKey>;
13231
+ }
13232
+ | {
13233
+ readonly kind: "NonEnumerable";
13234
+ readonly path: ReadonlyArray<PropertyKey>;
13235
+ }
13236
+ | {
13237
+ readonly kind: "SymbolProperty";
13238
+ readonly path: ReadonlyArray<PropertyKey>;
13239
+ }
13240
+ | {
13241
+ readonly kind: "Hole";
13242
+ readonly path: ReadonlyArray<PropertyKey>;
13243
+ }
13244
+ | {
13245
+ readonly kind: "InvalidUint8Array";
13246
+ readonly path: ReadonlyArray<PropertyKey>;
13247
+ readonly value: globalThis.Uint8Array;
13248
+ }
13249
+ | {
13250
+ readonly kind: "ExcessProperty";
13251
+ readonly path: ReadonlyArray<PropertyKey>;
13252
+ readonly container: "Array" | "Set" | "Map";
13253
+ };
13254
+
13255
+ /**
13256
+ * An error containing one or more issues found while validating a candidate as
13257
+ * {@link Data}.
13258
+ *
13259
+ * @group Base
13260
+ */
13261
+ export interface DataError extends TypeError<"Data"> {
13262
+ readonly reason: {
13263
+ readonly kind: "Issues";
13264
+ readonly issues: NonEmptyReadonlyArray<DataIssue>;
13265
+ };
13266
+ }
13267
+
13268
+ /**
13269
+ * The root {@link Type} for Evolu {@link Data}.
13270
+ *
13271
+ * @group Base
13272
+ */
13273
+ export interface DataType extends Type<
13274
+ "Data",
13275
+ Data,
13276
+ Data,
13277
+ DataError,
13278
+ null,
13279
+ DataError,
13280
+ never,
13281
+ Data
13282
+ > {}
13283
+
13284
+ interface DataPathNode {
13285
+ readonly parent: DataPathNode | null;
13286
+ readonly key: PropertyKey;
11972
13287
  }
11973
13288
 
11974
- type ValidateLazyTarget<Target extends ConcreteTypeNode> =
11975
- IsUnion<Target> extends false ? Target : LazyTargetConcreteTypeError;
13289
+ interface DataWork {
13290
+ readonly value: unknown;
13291
+ readonly path: DataPathNode | null;
13292
+ }
11976
13293
 
11977
- type LazyTargetConcreteTypeError = CompileTimeError<
11978
- "Type",
11979
- "Lazy Type definition must return one concrete Type node. Pass a Union Type node instead of a union of Type nodes."
11980
- >;
13294
+ const emptyDataPath: ReadonlyArray<PropertyKey> =
13295
+ /*#__PURE__*/ globalThis.Object.freeze([]);
11981
13296
 
11982
- type LazyResolution =
11983
- | { readonly state: "unresolved" }
11984
- | { readonly state: "resolving" }
11985
- | ResolvedLazyType
11986
- | { readonly state: "failed"; readonly error: unknown };
13297
+ const dataPathToArray = (
13298
+ path: DataPathNode | null,
13299
+ ): ReadonlyArray<PropertyKey> => {
13300
+ if (path === null) return emptyDataPath;
11987
13301
 
11988
- interface ResolvedLazyType {
11989
- readonly state: "resolved";
11990
- readonly target: RuntimeTypeNode;
11991
- readonly targetFromInput: RuntimeOperation<Result<unknown, TypeError>>;
11992
- readonly root: RuntimeTypeNode;
11993
- }
13302
+ const keys: Array<PropertyKey> = [];
13303
+ let node: DataPathNode | null = path;
11994
13304
 
11995
- const lazyTypeNodes = /*#__PURE__*/ new WeakSet<TypeNode>();
13305
+ while (node !== null) {
13306
+ keys.push(node.key);
13307
+ node = node.parent;
13308
+ }
11996
13309
 
11997
- const assertLazyReferencesAreGuarded = (type: RuntimeTypeNode): void => {
11998
- for (;;) {
11999
- assert(
12000
- !lazyTypeNodes.has(type),
12001
- "A Lazy Type definition must place every Lazy Type behind a structural boundary.",
12002
- );
13310
+ keys.reverse();
13311
+ return globalThis.Object.freeze(keys);
13312
+ };
12003
13313
 
12004
- if ("members" in type && globalThis.Array.isArray(type.members)) {
12005
- for (const member of type.members as ReadonlyArray<RuntimeTypeNode>)
12006
- assertLazyReferencesAreGuarded(member);
13314
+ const dataChildPath = (
13315
+ parent: DataPathNode | null,
13316
+ key: PropertyKey,
13317
+ ): DataPathNode => ({ parent, key });
13318
+
13319
+ const validateData = (
13320
+ value: unknown,
13321
+ options: ValidationOptions = firstValidationOptions,
13322
+ ): Result<Data, DataError> => {
13323
+ const work: Array<DataWork> = [{ value, path: null }];
13324
+ const visited = new WeakSet<object>();
13325
+ let issues: Array<DataIssue> | undefined;
13326
+
13327
+ const addIssue = (issue: DataIssue): boolean => {
13328
+ (issues ??= []).push(issue);
13329
+ return options.errors === "first";
13330
+ };
13331
+
13332
+ while (work.length > 0) {
13333
+ const current = work.pop()!;
13334
+ const { path, value } = current;
13335
+
13336
+ if (
13337
+ value === null ||
13338
+ value === undefined ||
13339
+ typeof value === "string" ||
13340
+ typeof value === "number" ||
13341
+ typeof value === "bigint" ||
13342
+ typeof value === "boolean"
13343
+ ) {
13344
+ continue;
12007
13345
  }
13346
+ if (typeof value !== "object") {
13347
+ if (
13348
+ addIssue({
13349
+ kind: "InvalidType",
13350
+ path: dataPathToArray(path),
13351
+ value,
13352
+ })
13353
+ ) {
13354
+ break;
13355
+ }
13356
+ continue;
13357
+ }
13358
+ if (visited.has(value)) continue;
12008
13359
 
12009
- if ("output" in type) {
12010
- assertLazyReferencesAreGuarded(type.output as RuntimeTypeNode);
13360
+ const children: Array<DataWork> = [];
13361
+ const kind = getObjectKind(value);
13362
+
13363
+ if (kind === "Array") {
13364
+ const array = value as ReadonlyArray<unknown>;
13365
+ for (const key of Reflect.ownKeys(array)) {
13366
+ if (key === "length") continue;
13367
+ if (typeof key === "string") {
13368
+ const index = globalThis.Number(key) >>> 0;
13369
+ if (index < array.length && globalThis.String(index) === key) {
13370
+ continue;
13371
+ }
13372
+ }
13373
+
13374
+ if (
13375
+ addIssue({
13376
+ kind: "ExcessProperty",
13377
+ path: dataPathToArray(dataChildPath(path, key)),
13378
+ container: "Array",
13379
+ })
13380
+ ) {
13381
+ break;
13382
+ }
13383
+ }
13384
+ if (issues !== undefined && options.errors === "first") break;
13385
+
13386
+ for (let index = 0; index < array.length; index++) {
13387
+ const childPath = dataChildPath(path, index);
13388
+ const descriptor = globalThis.Object.getOwnPropertyDescriptor(
13389
+ array,
13390
+ index,
13391
+ );
13392
+
13393
+ if (descriptor === undefined) {
13394
+ if (addIssue({ kind: "Hole", path: dataPathToArray(childPath) })) {
13395
+ break;
13396
+ }
13397
+ continue;
13398
+ }
13399
+ if (!("value" in descriptor)) {
13400
+ if (
13401
+ addIssue({ kind: "Accessor", path: dataPathToArray(childPath) })
13402
+ ) {
13403
+ break;
13404
+ }
13405
+ continue;
13406
+ }
13407
+
13408
+ children.push({ value: descriptor.value, path: childPath });
13409
+ }
13410
+ } else if (kind === "Object") {
13411
+ for (const key of Reflect.ownKeys(value)) {
13412
+ const childPath = dataChildPath(path, key);
13413
+
13414
+ if (typeof key === "symbol") {
13415
+ if (
13416
+ addIssue({
13417
+ kind: "SymbolProperty",
13418
+ path: dataPathToArray(childPath),
13419
+ })
13420
+ ) {
13421
+ break;
13422
+ }
13423
+ continue;
13424
+ }
13425
+
13426
+ const descriptor = globalThis.Object.getOwnPropertyDescriptor(
13427
+ value,
13428
+ key,
13429
+ )!;
13430
+ if (!("value" in descriptor)) {
13431
+ if (
13432
+ addIssue({ kind: "Accessor", path: dataPathToArray(childPath) })
13433
+ ) {
13434
+ break;
13435
+ }
13436
+ continue;
13437
+ }
13438
+ if (!descriptor.enumerable) {
13439
+ if (
13440
+ addIssue({
13441
+ kind: "NonEnumerable",
13442
+ path: dataPathToArray(childPath),
13443
+ })
13444
+ ) {
13445
+ break;
13446
+ }
13447
+ continue;
13448
+ }
13449
+
13450
+ children.push({ value: descriptor.value, path: childPath });
13451
+ }
13452
+ } else if (kind === "Date") {
13453
+ visited.add(value);
13454
+ continue;
13455
+ } else if (kind === "Uint8Array") {
13456
+ try {
13457
+ void globalThis.Uint8Array.prototype.values.call(
13458
+ value as globalThis.Uint8Array,
13459
+ );
13460
+ } catch {
13461
+ if (
13462
+ addIssue({
13463
+ kind: "InvalidUint8Array",
13464
+ path: dataPathToArray(path),
13465
+ value: value as globalThis.Uint8Array,
13466
+ })
13467
+ ) {
13468
+ break;
13469
+ }
13470
+ }
13471
+ visited.add(value);
13472
+ continue;
13473
+ } else if (kind === "Set") {
13474
+ for (const key of Reflect.ownKeys(value)) {
13475
+ if (
13476
+ addIssue({
13477
+ kind: "ExcessProperty",
13478
+ path: dataPathToArray(dataChildPath(path, key)),
13479
+ container: "Set",
13480
+ })
13481
+ ) {
13482
+ break;
13483
+ }
13484
+ }
13485
+ if (issues !== undefined && options.errors === "first") break;
13486
+
13487
+ let index = 0;
13488
+ for (const item of value as ReadonlySet<unknown>) {
13489
+ children.push({ value: item, path: dataChildPath(path, index++) });
13490
+ }
13491
+ } else if (kind === "Map") {
13492
+ for (const key of Reflect.ownKeys(value)) {
13493
+ if (
13494
+ addIssue({
13495
+ kind: "ExcessProperty",
13496
+ path: dataPathToArray(dataChildPath(path, key)),
13497
+ container: "Map",
13498
+ })
13499
+ ) {
13500
+ break;
13501
+ }
13502
+ }
13503
+ if (issues !== undefined && options.errors === "first") break;
13504
+
13505
+ let index = 0;
13506
+ for (const [key, item] of value as ReadonlyMap<unknown, unknown>) {
13507
+ const entryPath = dataChildPath(path, index++);
13508
+ children.push({ value: key, path: dataChildPath(entryPath, "key") });
13509
+ children.push({
13510
+ value: item,
13511
+ path: dataChildPath(entryPath, "value"),
13512
+ });
13513
+ }
13514
+ } else if (
13515
+ addIssue({
13516
+ kind: "UnexpectedPrototype",
13517
+ path: dataPathToArray(path),
13518
+ container: "Object",
13519
+ value,
13520
+ })
13521
+ ) {
13522
+ break;
12011
13523
  }
12012
13524
 
12013
- if (!type.parent) return;
12014
- type = type.parent as RuntimeTypeNode;
13525
+ if (issues !== undefined && options.errors === "first") break;
13526
+
13527
+ visited.add(value);
13528
+ for (let index = children.length - 1; index >= 0; index--) {
13529
+ work.push(children[index]);
13530
+ }
12015
13531
  }
13532
+
13533
+ return issues === undefined
13534
+ ? ok(value as Data)
13535
+ : err({
13536
+ type: "Data",
13537
+ reason: {
13538
+ kind: "Issues",
13539
+ issues: globalThis.Object.freeze(
13540
+ issues,
13541
+ ) as unknown as NonEmptyReadonlyArray<DataIssue>,
13542
+ },
13543
+ });
13544
+ };
13545
+
13546
+ const getDataRuntimeTypeIssues: RuntimeGetTypeIssues = (error, mode) => {
13547
+ const dataError = error as DataError;
13548
+ const issues =
13549
+ mode === "first"
13550
+ ? ([dataError.reason.issues[0]] as const)
13551
+ : dataError.reason.issues;
13552
+
13553
+ return issues.map((issue) => ({
13554
+ name: "Data",
13555
+ error:
13556
+ mode === "first"
13557
+ ? error
13558
+ : {
13559
+ type: "Data",
13560
+ reason: { kind: "Issues", issues: [issue] },
13561
+ },
13562
+ path: issue.path,
13563
+ formatError: ((error: DataError) => {
13564
+ const issue = error.reason.issues[0];
13565
+ switch (issue.kind) {
13566
+ case "InvalidType":
13567
+ return `A value ${safelyStringifyUnknownValue(issue.value)} is not Data.`;
13568
+ case "UnexpectedPrototype":
13569
+ return `A Data ${issue.container} has an unexpected prototype.`;
13570
+ case "Accessor":
13571
+ return "A Data property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
13572
+ case "NonEnumerable":
13573
+ return "A Data Object property must be enumerable. Remove it or use a different Type.";
13574
+ case "SymbolProperty":
13575
+ return "A Data Object property key must be a string. Remove the symbol property or use a different Type.";
13576
+ case "Hole":
13577
+ return "A Data Array element is missing.";
13578
+ case "InvalidUint8Array":
13579
+ return "A Data Uint8Array must have an attached, in-bounds ArrayBuffer.";
13580
+ case "ExcessProperty":
13581
+ return `A Data ${issue.container} must not have excess own properties. Remove the property or use a different Type.`;
13582
+ }
13583
+ }) as TypeErrorFormatter<TypeError>,
13584
+ })) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>;
12016
13585
  };
12017
13586
 
13587
+ /**
13588
+ * Root Type for {@link Data} values.
13589
+ *
13590
+ * Validation is iterative and preserves the input identity. Cyclic and shared
13591
+ * data graphs are valid.
13592
+ *
13593
+ * ### Example
13594
+ *
13595
+ * ```ts
13596
+ * import { assertOk, assertTrue, Data } from "@evolu/common";
13597
+ *
13598
+ * const value = new Map([["count", 1]]);
13599
+ * const result = Data.fromUnknown(value);
13600
+ *
13601
+ * assertOk(result, value);
13602
+ * assertTrue(Data.is(result.value));
13603
+ * ```
13604
+ *
13605
+ * @group Base
13606
+ */
13607
+ export const Data: DataType = /*#__PURE__*/ createTypeNode<DataType>(
13608
+ "Data",
13609
+ null,
13610
+ validateData,
13611
+ (value) => validateData(value).ok,
13612
+ validateData,
13613
+ ok,
13614
+ identity,
13615
+ getDataRuntimeTypeIssues,
13616
+ );
13617
+
12018
13618
  /**
12019
13619
  * A candidate JSON value before exact runtime validation.
12020
13620
  *
@@ -12260,6 +13860,7 @@ const validateJsonValue = (
12260
13860
  }
12261
13861
  if (typeof value === "number") {
12262
13862
  if (!globalThis.Number.isFinite(value)) {
13863
+ // oxlint-disable-next-line unicorn/no-lonely-if -- Keep issue construction separate from the finite-number guard.
12263
13864
  if (
12264
13865
  addIssue({
12265
13866
  kind: "NonFiniteNumber",
@@ -12285,7 +13886,7 @@ const validateJsonValue = (
12285
13886
  continue;
12286
13887
  }
12287
13888
 
12288
- const isArray = globalThis.Array.isArray(value);
13889
+ const isArray = Array.isArray(value);
12289
13890
 
12290
13891
  if (!isArray && !isPlainObject(value)) {
12291
13892
  if (
@@ -12491,8 +14092,7 @@ const getJsonValueRuntimeTypeIssues: RuntimeGetTypeIssues = (error, mode) => {
12491
14092
  })) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>;
12492
14093
  };
12493
14094
 
12494
- const parseJson = (value: string): JsonValue =>
12495
- globalThis.JSON.parse(value) as JsonValue;
14095
+ const parseJson = (value: string): JsonValue => JSON.parse(value) as JsonValue;
12496
14096
 
12497
14097
  const jsonToJsonValueResult = (value: string): Result<JsonValue, JsonError> => {
12498
14098
  const result = trySync((): unknown => parseJson(value));
@@ -12521,9 +14121,10 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12521
14121
  continue;
12522
14122
  }
12523
14123
 
14124
+ // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- JsonValue excludes the additional runtime types reported by tsgolint.
12524
14125
  switch (typeof value) {
12525
14126
  case "string":
12526
- chunks.push(globalThis.JSON.stringify(value));
14127
+ chunks.push(JSON.stringify(value));
12527
14128
  break;
12528
14129
  case "number":
12529
14130
  chunks.push(
@@ -12534,7 +14135,7 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12534
14135
  chunks.push(value ? "true" : "false");
12535
14136
  break;
12536
14137
  case "object": {
12537
- if (globalThis.Array.isArray(value)) {
14138
+ if (Array.isArray(value)) {
12538
14139
  const array = value as JsonArray;
12539
14140
  chunks.push("[");
12540
14141
  work.push({ kind: "Text", value: "]" });
@@ -12557,7 +14158,7 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12557
14158
  work.push({ kind: "Text", value: ":" });
12558
14159
  work.push({
12559
14160
  kind: "Text",
12560
- value: globalThis.JSON.stringify(key),
14161
+ value: JSON.stringify(key),
12561
14162
  });
12562
14163
  if (index > 0) work.push({ kind: "Text", value: "," });
12563
14164
  }
@@ -12575,7 +14176,12 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12575
14176
  * ### Example
12576
14177
  *
12577
14178
  * ```ts
12578
- * import { JsonValue, type JsonValueInput } from "@evolu/common";
14179
+ * import {
14180
+ * assertOk,
14181
+ * assertType,
14182
+ * JsonValue,
14183
+ * type JsonValueInput,
14184
+ * } from "@evolu/common";
12579
14185
  *
12580
14186
  * const input: JsonValueInput = {
12581
14187
  * name: "Ada",
@@ -12583,8 +14189,8 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12583
14189
  * };
12584
14190
  * const result = JsonValue.fromUnknown(input);
12585
14191
  *
12586
- * expectOk(result, input);
12587
- * expectTypeOf(result.value).toEqualTypeOf<JsonValue>();
14192
+ * assertOk(result, input);
14193
+ * assertType<JsonValue, typeof result.value>();
12588
14194
  * ```
12589
14195
  *
12590
14196
  * @group JSON
@@ -12646,11 +14252,11 @@ export type Json = typeof Json.Output;
12646
14252
  * ### Example
12647
14253
  *
12648
14254
  * ```ts
12649
- * import { Json, jsonToJsonValue } from "@evolu/common";
14255
+ * import { assertEqual, Json, jsonToJsonValue } from "@evolu/common";
12650
14256
  *
12651
14257
  * const value = jsonToJsonValue(Json.orThrow('{"name":"Ada"}'));
12652
14258
  *
12653
- * expect(value).toEqual({ name: "Ada" });
14259
+ * assertEqual(value, { name: "Ada" });
12654
14260
  * ```
12655
14261
  *
12656
14262
  * @group JSON
@@ -12663,11 +14269,15 @@ export const jsonToJsonValue = (value: Json): JsonValue => parseJson(value);
12663
14269
  * ### Example
12664
14270
  *
12665
14271
  * ```ts
12666
- * import { jsonValueToJson, type JsonValue } from "@evolu/common";
14272
+ * import {
14273
+ * assertEqual,
14274
+ * jsonValueToJson,
14275
+ * type JsonValue,
14276
+ * } from "@evolu/common";
12667
14277
  *
12668
14278
  * const value: JsonValue = { name: "Ada" };
12669
14279
  *
12670
- * expect(jsonValueToJson(value)).toBe('{"name":"Ada"}');
14280
+ * assertEqual(jsonValueToJson(value), '{"name":"Ada"}');
12671
14281
  * ```
12672
14282
  *
12673
14283
  * @group JSON
@@ -12684,12 +14294,12 @@ export const jsonValueToJson = (value: JsonValue): Json =>
12684
14294
  * ### Example
12685
14295
  *
12686
14296
  * ```ts
12687
- * import { JsonValueFromJson } from "@evolu/common";
14297
+ * import { assertEqual, assertOk, JsonValueFromJson } from "@evolu/common";
12688
14298
  *
12689
14299
  * const result = JsonValueFromJson.fromUnknown('{ "name": "Ada" }');
12690
14300
  *
12691
- * expectOk(result, { name: "Ada" });
12692
- * expect(JsonValueFromJson.to(result.value)).toBe('{"name":"Ada"}');
14301
+ * assertOk(result, { name: "Ada" });
14302
+ * assertEqual(JsonValueFromJson.to(result.value), '{"name":"Ada"}');
12693
14303
  * ```
12694
14304
  *
12695
14305
  * @group JSON
@@ -12716,6 +14326,8 @@ export const JsonValueFromJson = /*#__PURE__*/ transform(
12716
14326
  *
12717
14327
  * ```ts
12718
14328
  * import {
14329
+ * assertEqual,
14330
+ * assertType,
12719
14331
  * Age,
12720
14332
  * NonEmptyTrimmedString100,
12721
14333
  * json,
@@ -12736,11 +14348,13 @@ export const JsonValueFromJson = /*#__PURE__*/ transform(
12736
14348
  * const user = User.orThrow({ name: "Ada", age: 37 });
12737
14349
  * const userJson = userToUserJson(user);
12738
14350
  *
12739
- * expectTypeOf(userJson).toEqualTypeOf<
12740
- * string & Brand<"Json"> & Brand<"UserJson">
14351
+ * assertType<
14352
+ * string & Brand<"Json"> & Brand<"UserJson">,
14353
+ * typeof userJson
12741
14354
  * >();
12742
- * expect(userJson).toBe('{"name":"Ada","age":37}');
12743
- * expect(userJsonToUser(userJson)).toEqual(user);
14355
+ * assertEqual(userJson, '{"name":"Ada","age":37}');
14356
+ * assertEqual(UserJson.orThrow(userJson), userJson);
14357
+ * assertEqual(userJsonToUser(userJson), user);
12744
14358
  * ```
12745
14359
  *
12746
14360
  * The supplied Type must have a JSON-compatible `CanonicalInput`. The branded
@@ -12932,14 +14546,12 @@ type JsonCompatibleMember<
12932
14546
 
12933
14547
  type AllowedJsonArrayKeys<Value extends ReadonlyArray<unknown>> =
12934
14548
  | keyof ReadonlyArray<unknown>
12935
- | (Value extends globalThis.Array<unknown>
12936
- ? keyof globalThis.Array<unknown>
12937
- : never)
14549
+ | (Value extends Array<unknown> ? keyof Array<unknown> : never)
12938
14550
  | JsonTupleIndexKeys<Value>;
12939
14551
 
12940
14552
  type JsonTupleIndexKeys<Value extends ReadonlyArray<unknown>> =
12941
14553
  Value extends readonly [...infer Elements]
12942
- ? Exclude<keyof Elements, keyof globalThis.Array<unknown>>
14554
+ ? Exclude<keyof Elements, keyof Array<unknown>>
12943
14555
  : never;
12944
14556
 
12945
14557
  type IsJsonCompatibilityCycle<
@@ -12951,17 +14563,6 @@ type IsJsonCompatibilityCycle<
12951
14563
  : IsJsonCompatibilityCycle<Value, Rest>
12952
14564
  : false;
12953
14565
 
12954
- /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters -- The
12955
- generic-function comparison distinguishes recursive types that mutual
12956
- assignability cannot. */
12957
- type IsSameType<A, B> =
12958
- (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
12959
- ? (<T>() => T extends B ? 1 : 2) extends <T>() => T extends A ? 1 : 2
12960
- ? true
12961
- : false
12962
- : false;
12963
- /* eslint-enable @typescript-eslint/no-unnecessary-type-parameters */
12964
-
12965
14566
  type JsonNumberCompatible<Value extends number> = [Value] extends [FiniteNumber]
12966
14567
  ? true
12967
14568
  : [Value] extends [Brand<string>]