@evolu/common 8.5.0 → 8.6.1

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 (328) hide show
  1. package/dist/src/Array.d.ts +170 -97
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +67 -42
  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 +150 -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/LockManager.d.ts +12 -10
  35. package/dist/src/LockManager.d.ts.map +1 -1
  36. package/dist/src/Lookup.d.ts +4 -2
  37. package/dist/src/Lookup.d.ts.map +1 -1
  38. package/dist/src/Lookup.js +4 -2
  39. package/dist/src/Number.d.ts +28 -24
  40. package/dist/src/Number.d.ts.map +1 -1
  41. package/dist/src/Number.js +16 -17
  42. package/dist/src/Object.d.ts +89 -47
  43. package/dist/src/Object.d.ts.map +1 -1
  44. package/dist/src/Object.js +88 -41
  45. package/dist/src/Option.d.ts +14 -5
  46. package/dist/src/Option.d.ts.map +1 -1
  47. package/dist/src/Option.js +14 -5
  48. package/dist/src/Order.d.ts +12 -12
  49. package/dist/src/Order.js +12 -12
  50. package/dist/src/Platform.d.ts +2 -2
  51. package/dist/src/Random.d.ts +7 -4
  52. package/dist/src/Random.d.ts.map +1 -1
  53. package/dist/src/Redacted.d.ts +13 -6
  54. package/dist/src/Redacted.d.ts.map +1 -1
  55. package/dist/src/Redacted.js +4 -2
  56. package/dist/src/Ref.d.ts +4 -4
  57. package/dist/src/Relation.d.ts +5 -7
  58. package/dist/src/Relation.d.ts.map +1 -1
  59. package/dist/src/Resource.d.ts +27 -12
  60. package/dist/src/Resource.d.ts.map +1 -1
  61. package/dist/src/Result.d.ts +258 -114
  62. package/dist/src/Result.d.ts.map +1 -1
  63. package/dist/src/Result.js +71 -34
  64. package/dist/src/Schedule.d.ts +233 -116
  65. package/dist/src/Schedule.d.ts.map +1 -1
  66. package/dist/src/Schedule.js +204 -110
  67. package/dist/src/Set.d.ts +43 -24
  68. package/dist/src/Set.d.ts.map +1 -1
  69. package/dist/src/Set.js +25 -13
  70. package/dist/src/Sqlite.d.ts +4 -4
  71. package/dist/src/Sqlite.d.ts.map +1 -1
  72. package/dist/src/Sqlite.js +9 -8
  73. package/dist/src/Task.d.ts +441 -212
  74. package/dist/src/Task.d.ts.map +1 -1
  75. package/dist/src/Task.js +238 -115
  76. package/dist/src/Test.d.ts +10 -4
  77. package/dist/src/Test.d.ts.map +1 -1
  78. package/dist/src/Test.js +10 -4
  79. package/dist/src/Time.d.ts +27 -17
  80. package/dist/src/Time.d.ts.map +1 -1
  81. package/dist/src/Time.js +16 -6
  82. package/dist/src/Type.d.ts +1096 -387
  83. package/dist/src/Type.d.ts.map +1 -1
  84. package/dist/src/Type.js +1046 -372
  85. package/dist/src/Types.d.ts +95 -40
  86. package/dist/src/Types.d.ts.map +1 -1
  87. package/dist/src/Types.js +13 -4
  88. package/dist/src/WebSocket.d.ts +14 -6
  89. package/dist/src/WebSocket.d.ts.map +1 -1
  90. package/dist/src/Worker.d.ts +10 -6
  91. package/dist/src/Worker.d.ts.map +1 -1
  92. package/dist/src/intl/_en.d.ts +3 -1
  93. package/dist/src/intl/_en.d.ts.map +1 -1
  94. package/dist/src/intl/_en.js +17 -3
  95. package/dist/src/intl/ar.d.ts +3 -1
  96. package/dist/src/intl/ar.d.ts.map +1 -1
  97. package/dist/src/intl/ar.js +15 -2
  98. package/dist/src/intl/bn.d.ts +3 -1
  99. package/dist/src/intl/bn.d.ts.map +1 -1
  100. package/dist/src/intl/bn.js +16 -3
  101. package/dist/src/intl/ca.d.ts +3 -1
  102. package/dist/src/intl/ca.d.ts.map +1 -1
  103. package/dist/src/intl/ca.js +16 -3
  104. package/dist/src/intl/cs.d.ts +3 -1
  105. package/dist/src/intl/cs.d.ts.map +1 -1
  106. package/dist/src/intl/cs.js +16 -3
  107. package/dist/src/intl/da.d.ts +3 -1
  108. package/dist/src/intl/da.d.ts.map +1 -1
  109. package/dist/src/intl/da.js +16 -3
  110. package/dist/src/intl/de.d.ts +3 -1
  111. package/dist/src/intl/de.d.ts.map +1 -1
  112. package/dist/src/intl/de.js +16 -3
  113. package/dist/src/intl/el.d.ts +3 -1
  114. package/dist/src/intl/el.d.ts.map +1 -1
  115. package/dist/src/intl/el.js +16 -3
  116. package/dist/src/intl/es.d.ts +3 -1
  117. package/dist/src/intl/es.d.ts.map +1 -1
  118. package/dist/src/intl/es.js +16 -3
  119. package/dist/src/intl/fa.d.ts +3 -1
  120. package/dist/src/intl/fa.d.ts.map +1 -1
  121. package/dist/src/intl/fa.js +16 -3
  122. package/dist/src/intl/fi.d.ts +3 -1
  123. package/dist/src/intl/fi.d.ts.map +1 -1
  124. package/dist/src/intl/fi.js +16 -3
  125. package/dist/src/intl/fil.d.ts +3 -1
  126. package/dist/src/intl/fil.d.ts.map +1 -1
  127. package/dist/src/intl/fil.js +16 -3
  128. package/dist/src/intl/fr.d.ts +3 -1
  129. package/dist/src/intl/fr.d.ts.map +1 -1
  130. package/dist/src/intl/fr.js +16 -3
  131. package/dist/src/intl/he.d.ts +3 -1
  132. package/dist/src/intl/he.d.ts.map +1 -1
  133. package/dist/src/intl/he.js +16 -3
  134. package/dist/src/intl/hi.d.ts +3 -1
  135. package/dist/src/intl/hi.d.ts.map +1 -1
  136. package/dist/src/intl/hi.js +16 -3
  137. package/dist/src/intl/hr.d.ts +3 -1
  138. package/dist/src/intl/hr.d.ts.map +1 -1
  139. package/dist/src/intl/hr.js +16 -3
  140. package/dist/src/intl/hu.d.ts +2 -1
  141. package/dist/src/intl/hu.d.ts.map +1 -1
  142. package/dist/src/intl/hu.js +15 -3
  143. package/dist/src/intl/id.d.ts +3 -1
  144. package/dist/src/intl/id.d.ts.map +1 -1
  145. package/dist/src/intl/id.js +16 -3
  146. package/dist/src/intl/it.d.ts +3 -1
  147. package/dist/src/intl/it.d.ts.map +1 -1
  148. package/dist/src/intl/it.js +16 -3
  149. package/dist/src/intl/ja.d.ts +3 -1
  150. package/dist/src/intl/ja.d.ts.map +1 -1
  151. package/dist/src/intl/ja.js +16 -3
  152. package/dist/src/intl/ko.d.ts +3 -1
  153. package/dist/src/intl/ko.d.ts.map +1 -1
  154. package/dist/src/intl/ko.js +16 -3
  155. package/dist/src/intl/ml.d.ts +3 -1
  156. package/dist/src/intl/ml.d.ts.map +1 -1
  157. package/dist/src/intl/ml.js +16 -3
  158. package/dist/src/intl/mr.d.ts +3 -1
  159. package/dist/src/intl/mr.d.ts.map +1 -1
  160. package/dist/src/intl/mr.js +16 -3
  161. package/dist/src/intl/ms.d.ts +3 -1
  162. package/dist/src/intl/ms.d.ts.map +1 -1
  163. package/dist/src/intl/ms.js +15 -2
  164. package/dist/src/intl/nb.d.ts +2 -1
  165. package/dist/src/intl/nb.d.ts.map +1 -1
  166. package/dist/src/intl/nb.js +14 -2
  167. package/dist/src/intl/nl.d.ts +3 -1
  168. package/dist/src/intl/nl.d.ts.map +1 -1
  169. package/dist/src/intl/nl.js +16 -3
  170. package/dist/src/intl/pa.d.ts +3 -1
  171. package/dist/src/intl/pa.d.ts.map +1 -1
  172. package/dist/src/intl/pa.js +16 -3
  173. package/dist/src/intl/pl.d.ts +2 -0
  174. package/dist/src/intl/pl.d.ts.map +1 -1
  175. package/dist/src/intl/pl.js +15 -2
  176. package/dist/src/intl/pt-BR.d.ts +3 -1
  177. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  178. package/dist/src/intl/pt-BR.js +16 -3
  179. package/dist/src/intl/pt.d.ts +3 -1
  180. package/dist/src/intl/pt.d.ts.map +1 -1
  181. package/dist/src/intl/pt.js +15 -2
  182. package/dist/src/intl/ro.d.ts +3 -1
  183. package/dist/src/intl/ro.d.ts.map +1 -1
  184. package/dist/src/intl/ro.js +16 -3
  185. package/dist/src/intl/sk.d.ts +3 -1
  186. package/dist/src/intl/sk.d.ts.map +1 -1
  187. package/dist/src/intl/sk.js +15 -2
  188. package/dist/src/intl/sl.d.ts +3 -1
  189. package/dist/src/intl/sl.d.ts.map +1 -1
  190. package/dist/src/intl/sl.js +16 -3
  191. package/dist/src/intl/sv.d.ts +3 -1
  192. package/dist/src/intl/sv.d.ts.map +1 -1
  193. package/dist/src/intl/sv.js +16 -3
  194. package/dist/src/intl/sw.d.ts +1 -0
  195. package/dist/src/intl/sw.d.ts.map +1 -1
  196. package/dist/src/intl/sw.js +14 -2
  197. package/dist/src/intl/ta.d.ts +3 -1
  198. package/dist/src/intl/ta.d.ts.map +1 -1
  199. package/dist/src/intl/ta.js +16 -3
  200. package/dist/src/intl/te.d.ts +3 -1
  201. package/dist/src/intl/te.d.ts.map +1 -1
  202. package/dist/src/intl/te.js +16 -3
  203. package/dist/src/intl/th.d.ts +3 -1
  204. package/dist/src/intl/th.d.ts.map +1 -1
  205. package/dist/src/intl/th.js +16 -3
  206. package/dist/src/intl/tr.d.ts +3 -1
  207. package/dist/src/intl/tr.d.ts.map +1 -1
  208. package/dist/src/intl/tr.js +16 -3
  209. package/dist/src/intl/uk.d.ts +3 -1
  210. package/dist/src/intl/uk.d.ts.map +1 -1
  211. package/dist/src/intl/uk.js +16 -3
  212. package/dist/src/intl/ur.d.ts +3 -1
  213. package/dist/src/intl/ur.d.ts.map +1 -1
  214. package/dist/src/intl/ur.js +15 -2
  215. package/dist/src/intl/vi.d.ts +3 -1
  216. package/dist/src/intl/vi.d.ts.map +1 -1
  217. package/dist/src/intl/vi.js +16 -3
  218. package/dist/src/intl/zh-CN.d.ts +3 -1
  219. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  220. package/dist/src/intl/zh-CN.js +16 -3
  221. package/dist/src/intl/zh-TW.d.ts +3 -1
  222. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  223. package/dist/src/intl/zh-TW.js +16 -3
  224. package/dist/src/local-first/Evolu.d.ts +47 -24
  225. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  226. package/dist/src/local-first/Owner.d.ts +10 -7
  227. package/dist/src/local-first/Owner.d.ts.map +1 -1
  228. package/dist/src/local-first/Owner.js +10 -7
  229. package/dist/src/local-first/Protocol.d.ts +5 -5
  230. package/dist/src/local-first/Protocol.js +7 -7
  231. package/dist/src/local-first/Query.d.ts +34 -21
  232. package/dist/src/local-first/Query.d.ts.map +1 -1
  233. package/dist/src/local-first/Query.js +25 -13
  234. package/dist/src/local-first/Relay.d.ts +12 -5
  235. package/dist/src/local-first/Relay.d.ts.map +1 -1
  236. package/dist/src/local-first/Schema.d.ts +19 -11
  237. package/dist/src/local-first/Schema.d.ts.map +1 -1
  238. package/dist/src/local-first/Schema.js +11 -6
  239. package/dist/src/local-first/Storage.d.ts +4 -2
  240. package/dist/src/local-first/Storage.d.ts.map +1 -1
  241. package/package.json +3 -4
  242. package/src/Array.ts +170 -97
  243. package/src/Assert.ts +336 -31
  244. package/src/BigInt.ts +3 -3
  245. package/src/Brand.ts +7 -7
  246. package/src/Buffer.ts +12 -4
  247. package/src/Cache.ts +8 -3
  248. package/src/Callbacks.ts +8 -4
  249. package/src/Console.ts +45 -30
  250. package/src/Crypto.ts +13 -8
  251. package/src/Eq.ts +450 -122
  252. package/src/Function.ts +54 -24
  253. package/src/Http.ts +52 -19
  254. package/src/Identicon.ts +9 -4
  255. package/src/LockManager.ts +12 -10
  256. package/src/Lookup.ts +4 -2
  257. package/src/Number.ts +28 -24
  258. package/src/Object.ts +115 -49
  259. package/src/Option.ts +14 -5
  260. package/src/Order.ts +12 -12
  261. package/src/Platform.ts +2 -2
  262. package/src/Random.ts +7 -4
  263. package/src/Redacted.ts +13 -6
  264. package/src/Ref.ts +4 -4
  265. package/src/Relation.ts +5 -7
  266. package/src/Resource.ts +27 -12
  267. package/src/Result.ts +258 -114
  268. package/src/Schedule.ts +233 -116
  269. package/src/Set.ts +43 -24
  270. package/src/Sqlite.ts +9 -8
  271. package/src/Task.ts +441 -212
  272. package/src/Test.ts +10 -4
  273. package/src/Time.ts +27 -17
  274. package/src/Type.ts +2077 -620
  275. package/src/Types.ts +108 -40
  276. package/src/WebSocket.ts +14 -6
  277. package/src/Worker.ts +10 -6
  278. package/src/intl/_en.ts +20 -4
  279. package/src/intl/ar.ts +17 -2
  280. package/src/intl/bn.ts +19 -4
  281. package/src/intl/ca.ts +19 -4
  282. package/src/intl/cs.ts +19 -4
  283. package/src/intl/da.ts +19 -4
  284. package/src/intl/de.ts +19 -4
  285. package/src/intl/el.ts +19 -4
  286. package/src/intl/es.ts +19 -4
  287. package/src/intl/fa.ts +19 -4
  288. package/src/intl/fi.ts +19 -4
  289. package/src/intl/fil.ts +19 -4
  290. package/src/intl/fr.ts +19 -4
  291. package/src/intl/he.ts +19 -4
  292. package/src/intl/hi.ts +19 -4
  293. package/src/intl/hr.ts +19 -4
  294. package/src/intl/hu.ts +18 -4
  295. package/src/intl/id.ts +19 -4
  296. package/src/intl/it.ts +19 -4
  297. package/src/intl/ja.ts +19 -4
  298. package/src/intl/ko.ts +19 -4
  299. package/src/intl/ml.ts +19 -4
  300. package/src/intl/mr.ts +19 -4
  301. package/src/intl/ms.ts +17 -2
  302. package/src/intl/nb.ts +16 -2
  303. package/src/intl/nl.ts +19 -4
  304. package/src/intl/pa.ts +19 -4
  305. package/src/intl/pl.ts +18 -2
  306. package/src/intl/pt-BR.ts +19 -4
  307. package/src/intl/pt.ts +17 -2
  308. package/src/intl/ro.ts +19 -4
  309. package/src/intl/sk.ts +17 -2
  310. package/src/intl/sl.ts +19 -4
  311. package/src/intl/sv.ts +19 -4
  312. package/src/intl/sw.ts +15 -2
  313. package/src/intl/ta.ts +19 -4
  314. package/src/intl/te.ts +19 -4
  315. package/src/intl/th.ts +19 -4
  316. package/src/intl/tr.ts +19 -4
  317. package/src/intl/uk.ts +19 -4
  318. package/src/intl/ur.ts +17 -2
  319. package/src/intl/vi.ts +19 -3
  320. package/src/intl/zh-CN.ts +19 -4
  321. package/src/intl/zh-TW.ts +19 -4
  322. package/src/local-first/Evolu.ts +47 -24
  323. package/src/local-first/Owner.ts +10 -7
  324. package/src/local-first/Protocol.ts +7 -7
  325. package/src/local-first/Query.ts +34 -21
  326. package/src/local-first/Relay.ts +12 -5
  327. package/src/local-first/Schema.ts +19 -11
  328. package/src/local-first/Storage.ts +4 -2
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
  *
@@ -441,7 +463,7 @@ import { assert, assertNonNullable } from "./Assert.ts";
441
463
  import type { Brand } from "./Brand.ts";
442
464
  import type { RandomBytesDep } from "./Crypto.ts";
443
465
  import { identity, type Thunk } from "./Function.ts";
444
- import { createMutableRecord } from "./Object.ts";
466
+ import { createMutableRecord, getObjectKind, isPlainObject } from "./Object.ts";
445
467
  import { hasNodeBuffer } from "./Platform.ts";
446
468
  import {
447
469
  err,
@@ -461,6 +483,7 @@ import {
461
483
  isInstance,
462
484
  type CompileTimeError,
463
485
  type Instance,
486
+ type IsSameType,
464
487
  type IsUnion,
465
488
  type Literal,
466
489
  type Simplify,
@@ -572,12 +595,12 @@ export interface Type<
572
595
  * ### Example
573
596
  *
574
597
  * ```ts
575
- * import { PositiveInt } from "@evolu/common";
598
+ * import { assertOk, PositiveInt } from "@evolu/common";
576
599
  *
577
600
  * const value: unknown = 42;
578
601
  * const result = PositiveInt.fromUnknown(value);
579
602
  *
580
- * expectOk(result, 42);
603
+ * assertOk(result, 42);
581
604
  * ```
582
605
  */
583
606
  readonly fromUnknown: (
@@ -597,12 +620,25 @@ export interface Type<
597
620
  * ### Example
598
621
  *
599
622
  * ```ts
600
- * import { String } from "@evolu/common";
623
+ * import {
624
+ * assertEqual,
625
+ * assertType,
626
+ * Data,
627
+ * assertErr,
628
+ * String,
629
+ * } from "@evolu/common";
601
630
  *
602
631
  * const result = String.fromUnknown(42);
603
632
  *
604
- * expectErr(result, { type: "TypeOf", expected: "String", value: 42 });
605
- * 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),
606
642
  * "A value 42 is not a string.",
607
643
  * );
608
644
  * ```
@@ -619,14 +655,20 @@ export interface Type<
619
655
  * ### Example
620
656
  *
621
657
  * ```ts
622
- * import { Int64FromInt64String, type Int64 } from "@evolu/common";
658
+ * import {
659
+ * assertFalse,
660
+ * assertType,
661
+ * assertTrue,
662
+ * Int64FromInt64String,
663
+ * type Int64,
664
+ * } from "@evolu/common";
623
665
  *
624
666
  * const values: ReadonlyArray<unknown> = [42n, "42", null];
625
667
  * const integers = values.filter(Int64FromInt64String.is);
626
668
  *
627
- * expectTypeOf(integers).toEqualTypeOf<globalThis.Array<Int64>>();
628
- * expect(Int64FromInt64String.is(42n)).toBe(true);
629
- * expect(Int64FromInt64String.is("42")).toBe(false);
669
+ * assertType<Array<Int64>, typeof integers>();
670
+ * assertTrue(Int64FromInt64String.is(42n));
671
+ * assertFalse(Int64FromInt64String.is("42"));
630
672
  * ```
631
673
  */
632
674
  readonly is: (value: unknown) => value is Output;
@@ -652,6 +694,8 @@ export interface Type<
652
694
  *
653
695
  * ```ts
654
696
  * import {
697
+ * assertOk,
698
+ * assertType,
655
699
  * flatMapResult,
656
700
  * NonEmptyTrimmedString100,
657
701
  * object,
@@ -684,10 +728,11 @@ export interface Type<
684
728
  * const note = TrimmedString.orThrow("Remember oat milk");
685
729
  * const result = saveTodo(title, note);
686
730
  *
687
- * expectTypeOf(result).toEqualTypeOf<
688
- * Result<typeof Todo.Output, MaxLengthError<100> | MinLengthError<1>>
731
+ * assertType<
732
+ * Result<typeof Todo.Output, MaxLengthError<100> | MinLengthError<1>>,
733
+ * typeof result
689
734
  * >();
690
- * expectOk(result, { title, note });
735
+ * assertOk(result, { title, note });
691
736
  * ```
692
737
  */
693
738
  readonly from: [CustomFrom] extends [never]
@@ -707,11 +752,15 @@ export interface Type<
707
752
  * ### Example
708
753
  *
709
754
  * ```ts
710
- * import { Int64, Int64FromInt64String } from "@evolu/common";
755
+ * import {
756
+ * assertEqual,
757
+ * Int64,
758
+ * Int64FromInt64String,
759
+ * } from "@evolu/common";
711
760
  *
712
761
  * const value = Int64.orThrow(42n);
713
762
  *
714
- * expect(Int64FromInt64String.to(value)).toBe("42");
763
+ * assertEqual(Int64FromInt64String.to(value), "42");
715
764
  * ```
716
765
  */
717
766
  readonly to: [Parent] extends [infer P extends TypeNode]
@@ -737,7 +786,12 @@ export interface Type<
737
786
  * ### Example
738
787
  *
739
788
  * ```ts
740
- * import { getOrThrow, minLength, String } from "@evolu/common";
789
+ * import {
790
+ * assertEqual,
791
+ * getOrThrow,
792
+ * minLength,
793
+ * String,
794
+ * } from "@evolu/common";
741
795
  *
742
796
  * const NonEmptyString = minLength(1)(String);
743
797
  *
@@ -746,8 +800,8 @@ export interface Type<
746
800
  * // Equivalent because `from.parent` is this Type's deepest `from` operation:
747
801
  * const sameValue = getOrThrow(NonEmptyString.from.parent("Evolu"));
748
802
  *
749
- * expect(value).toBe("Evolu");
750
- * expect(sameValue).toBe(value);
803
+ * assertEqual(value, "Evolu");
804
+ * assertEqual(sameValue, value);
751
805
  * ```
752
806
  */
753
807
  readonly orThrow: TypeOperationFn<"orThrow", Input, Output, never>;
@@ -771,7 +825,12 @@ export interface Type<
771
825
  * ### Example
772
826
  *
773
827
  * ```ts
774
- * import { getOrNull, minLength, String } from "@evolu/common";
828
+ * import {
829
+ * assertEqual,
830
+ * getOrNull,
831
+ * minLength,
832
+ * String,
833
+ * } from "@evolu/common";
775
834
  *
776
835
  * const NonEmptyString = minLength(1)(String);
777
836
  *
@@ -780,9 +839,9 @@ export interface Type<
780
839
  * // Equivalent because `from.parent` is this Type's deepest `from` operation:
781
840
  * const sameValue = getOrNull(NonEmptyString.from.parent("Evolu"));
782
841
  *
783
- * expect(value).toBe("Evolu");
784
- * expect(sameValue).toBe(value);
785
- * expect(NonEmptyString.orNull("")).toBeNull();
842
+ * assertEqual(value, "Evolu");
843
+ * assertEqual(sameValue, value);
844
+ * assertEqual(NonEmptyString.orNull(""), null);
786
845
  * ```
787
846
  */
788
847
  readonly orNull: TypeOperationFn<"orNull", Input, Output, never>;
@@ -976,12 +1035,18 @@ const formatDefaultRuntimeTypeIssue: RuntimeFormatTypeIssue = (issue) =>
976
1035
  issue.formatError(issue.error);
977
1036
 
978
1037
  /**
979
- * 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.
1040
+ *
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.
980
1045
  *
981
- * Use this for internal invariants, not external input. Validate external input
982
- * with `Type.fromUnknown` so validation failures remain typed values. A failed
983
- * assertion uses the Type name for its message and preserves the exact Output
984
- * validation error as the thrown Error's cause.
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.
985
1050
  *
986
1051
  * ### Example
987
1052
  *
@@ -994,17 +1059,37 @@ const formatDefaultRuntimeTypeIssue: RuntimeFormatTypeIssue = (issue) =>
994
1059
  *
995
1060
  * const value: unknown = "Evolu";
996
1061
  * assertType(NonEmptyTrimmedString100, value);
997
- * expectTypeOf(value).toEqualTypeOf<
998
- * string & Brand<"Trimmed"> & Brand<"MinLength1"> & Brand<"MaxLength100">
1062
+ * assertType<
1063
+ * string &
1064
+ * Brand<"Trimmed"> &
1065
+ * Brand<"MinLength1"> &
1066
+ * Brand<"MaxLength100">,
1067
+ * typeof value
999
1068
  * >();
1000
1069
  * ```
1001
1070
  *
1002
1071
  * @group Core
1003
1072
  */
1004
- 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>(
1005
1084
  type: T,
1006
1085
  value: unknown,
1007
- ) => 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
+
1008
1093
  // TODO: Make assert prepend "Expected " and accept an optional third cause
1009
1094
  // argument. Then use it here and migrate every other assertion to pass an
1010
1095
  // expectation fragment.
@@ -1014,7 +1099,7 @@ export const assertType: <T extends TypeNode>(
1014
1099
  if (!result.ok) {
1015
1100
  throw new Error(`Expected ${runtimeType.name}.`, { cause: result.error });
1016
1101
  }
1017
- };
1102
+ }
1018
1103
 
1019
1104
  const assertTypeOutput = <Error extends TypeError>(
1020
1105
  name: TypeName,
@@ -1059,13 +1144,22 @@ const assertTypeOutput = <Error extends TypeError>(
1059
1144
  * every locale an app supports also allows language changes without a network
1060
1145
  * connection.
1061
1146
  *
1062
- * The selected Type map, locale map, and formatter maps must be plain objects
1063
- * 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.
1064
1150
  *
1065
1151
  * ### Example
1066
1152
  *
1067
1153
  * ```ts
1068
- * 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";
1069
1163
  * import { cs } from "@evolu/common/intl";
1070
1164
  *
1071
1165
  * const Label = minLength(1)(String);
@@ -1080,13 +1174,14 @@ const assertTypeOutput = <Error extends TypeError>(
1080
1174
  * },
1081
1175
  * );
1082
1176
  *
1083
- * expectTypeOf<typeof typesByLocale.cs.Label>().toEqualTypeOf<
1084
- * typeof Label
1085
- * >();
1177
+ * assertType<typeof Label, typeof typesByLocale.cs.Label>();
1086
1178
  *
1087
1179
  * const result = typesByLocale.cs.Label.fromUnknown("");
1088
- * expectErr(result, { type: "MinLength1", min: 1, value: "" });
1089
- * 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),
1090
1185
  * "Text nesmí být prázdný.",
1091
1186
  * );
1092
1187
  * ```
@@ -1200,12 +1295,7 @@ export const localizeTypes = ((
1200
1295
  const getLocalizationMapKeys = (value: unknown): ReadonlyArray<string> => {
1201
1296
  const errorMessage =
1202
1297
  "localizeTypes maps must be plain objects with own enumerable string-keyed data properties.";
1203
- assert(value !== null && typeof value === "object", errorMessage);
1204
- const prototype: unknown = globalThis.Object.getPrototypeOf(value);
1205
- assert(
1206
- prototype === null || globalThis.Object.getPrototypeOf(prototype) === null,
1207
- errorMessage,
1208
- );
1298
+ assert(isPlainObject(value), errorMessage);
1209
1299
 
1210
1300
  const keys = Reflect.ownKeys(value);
1211
1301
  for (const key of keys) {
@@ -1298,14 +1388,12 @@ const localizeTypeReflection = (
1298
1388
  localizedTypeBySource,
1299
1389
  );
1300
1390
  }
1301
- if (globalThis.Array.isArray(value)) {
1391
+ if (Array.isArray(value)) {
1302
1392
  return (value as ReadonlyArray<unknown>).map((value) =>
1303
1393
  localizeTypeReflection(value, formatIssue, localizedTypeBySource),
1304
1394
  );
1305
1395
  }
1306
- if (value === null || typeof value !== "object" || !isPlainObject(value)) {
1307
- return value;
1308
- }
1396
+ if (!isPlainObject(value)) return value;
1309
1397
 
1310
1398
  const localized = globalThis.Object.create(
1311
1399
  globalThis.Object.getPrototypeOf(value) as object | null,
@@ -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
@@ -2149,7 +2247,15 @@ const createChildType = <
2149
2247
  * ### Example
2150
2248
  *
2151
2249
  * ```ts
2152
- * 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";
2153
2259
  *
2154
2260
  * const BooleanString = union(literal("false"), literal("true"));
2155
2261
  * const BooleanFromString = transform(
@@ -2162,8 +2268,8 @@ const createChildType = <
2162
2268
  * },
2163
2269
  * );
2164
2270
  *
2165
- * expectOk(BooleanFromString.from.parent("true"), true);
2166
- * expect(BooleanFromString.to(false)).toBe("false");
2271
+ * assertOk(BooleanFromString.from.parent("true"), true);
2272
+ * assertEqual(BooleanFromString.to(false), "false");
2167
2273
  * ```
2168
2274
  *
2169
2275
  * @group Construction
@@ -2193,6 +2299,11 @@ export function transform<
2193
2299
  *
2194
2300
  * ```ts
2195
2301
  * import {
2302
+ * assertEqual,
2303
+ * assertType,
2304
+ * Data,
2305
+ * assertErr,
2306
+ * assertOk,
2196
2307
  * Boolean,
2197
2308
  * String,
2198
2309
  * err,
@@ -2222,12 +2333,15 @@ export function transform<
2222
2333
  * () => 'Expected "true" or "false".',
2223
2334
  * );
2224
2335
  *
2225
- * expectOk(BooleanFromString.fromUnknown("true"), true);
2226
- * expect(BooleanFromString.to(false)).toBe("false");
2336
+ * assertOk(BooleanFromString.fromUnknown("true"), true);
2337
+ * assertEqual(BooleanFromString.to(false), "false");
2227
2338
  *
2228
2339
  * const invalid = BooleanFromString.fromUnknown("yes");
2229
- * expectErr(invalid, { type: "BooleanFromString", value: "yes" });
2230
- * expect(BooleanFromString.formatError(invalid.error)).toBe(
2340
+ * assertErr(invalid);
2341
+ * assertType(Data, invalid.error);
2342
+ * assertEqual(invalid.error, { type: "BooleanFromString", value: "yes" });
2343
+ * assertEqual(
2344
+ * BooleanFromString.formatError(invalid.error),
2231
2345
  * 'Expected "true" or "false".',
2232
2346
  * );
2233
2347
  * ```
@@ -2745,16 +2859,20 @@ export interface TypeOfError<
2745
2859
  * and significant whitespace:
2746
2860
  *
2747
2861
  * ```ts
2748
- * 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";
2749
2869
  *
2750
2870
  * const WireValue100 = maxLength(100)(String);
2751
2871
  * type WireValue100 = typeof WireValue100.Output;
2752
2872
  *
2753
- * expectTypeOf<WireValue100>().toEqualTypeOf<
2754
- * string & Brand<"MaxLength100">
2755
- * >();
2756
- * expectOk(WireValue100.fromUnknown(""), "");
2757
- * expectOk(WireValue100.fromUnknown(" value "), " value ");
2873
+ * assertType<string & Brand<"MaxLength100">, WireValue100>();
2874
+ * assertOk(WireValue100.fromUnknown(""), "");
2875
+ * assertOk(WireValue100.fromUnknown(" value "), " value ");
2758
2876
  * ```
2759
2877
  *
2760
2878
  * @group String
@@ -2781,7 +2899,12 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
2781
2899
  *
2782
2900
  * ```ts
2783
2901
  * import {
2902
+ * assertEqual,
2903
+ * assertErr,
2904
+ * assertOk,
2905
+ * assertType,
2784
2906
  * Age,
2907
+ * Data,
2785
2908
  * FiniteNumber,
2786
2909
  * Int,
2787
2910
  * NonNaNNumber,
@@ -2791,36 +2914,41 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
2791
2914
  * } from "@evolu/common";
2792
2915
  *
2793
2916
  * // Note how every additional constraint accumulates its Brand.
2794
- * expectTypeOf<typeof Number.Output>().toEqualTypeOf<number>();
2795
- * expectTypeOf<typeof NonNaNNumber.Output>().toEqualTypeOf<
2796
- * 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
2797
2922
  * >();
2798
- * expectTypeOf<typeof FiniteNumber.Output>().toEqualTypeOf<
2799
- * number & Brand<"NonNaN"> & Brand<"Finite">
2923
+ * assertType<
2924
+ * number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int">,
2925
+ * typeof Int.Output
2800
2926
  * >();
2801
- * expectTypeOf<typeof Int.Output>().toEqualTypeOf<
2802
- * number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int">
2803
- * >();
2804
- * expectTypeOf<typeof NonNegativeInt.Output>().toEqualTypeOf<
2927
+ * assertType<
2805
2928
  * number &
2806
2929
  * Brand<"NonNaN"> &
2807
2930
  * Brand<"Finite"> &
2808
2931
  * Brand<"Int"> &
2809
- * Brand<"NonNegative">
2932
+ * Brand<"NonNegative">,
2933
+ * typeof NonNegativeInt.Output
2810
2934
  * >();
2811
2935
  *
2812
- * expectTypeOf<typeof Age.Output>().toEqualTypeOf<
2936
+ * assertType<
2813
2937
  * number &
2814
2938
  * Brand<"NonNaN"> &
2815
2939
  * Brand<"Finite"> &
2816
2940
  * Brand<"Int"> &
2817
2941
  * Brand<"NonNegative"> &
2818
2942
  * Brand<"LessThan200"> &
2819
- * Brand<"Age">
2943
+ * Brand<"Age">,
2944
+ * typeof Age.Output
2820
2945
  * >();
2821
2946
  *
2822
- * expectOk(Age.fromUnknown(122), 122);
2823
- * 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, {
2824
2952
  * type: "LessThan200",
2825
2953
  * value: 200,
2826
2954
  * max: 200,
@@ -2973,12 +3101,12 @@ interface ObjectTagOutputByName {
2973
3101
  * ### Example
2974
3102
  *
2975
3103
  * ```ts
2976
- * import { objectTag } from "@evolu/common";
3104
+ * import { assertOk, objectTag } from "@evolu/common";
2977
3105
  *
2978
3106
  * const DateType = objectTag("Date");
2979
- * const date = new globalThis.Date("2025-01-01T00:00:00.000Z");
3107
+ * const date = new Date("2025-01-01T00:00:00.000Z");
2980
3108
  *
2981
- * expectOk(DateType.fromUnknown(date), date);
3109
+ * assertOk(DateType.fromUnknown(date), date);
2982
3110
  * ```
2983
3111
  *
2984
3112
  * @group Base
@@ -3002,10 +3130,17 @@ export function objectTag<Name extends keyof ObjectTagOutputByName>(
3002
3130
  * ### Example
3003
3131
  *
3004
3132
  * ```ts
3005
- * import { instanceOf, objectTag } from "@evolu/common";
3133
+ * import {
3134
+ * assertEqual,
3135
+ * assertOk,
3136
+ * assertSame,
3137
+ * assertType,
3138
+ * instanceOf,
3139
+ * objectTag,
3140
+ * } from "@evolu/common";
3006
3141
  *
3007
3142
  * class TaggedValue {
3008
- * readonly [globalThis.Symbol.toStringTag] = "TaggedValue";
3143
+ * readonly [Symbol.toStringTag] = "TaggedValue";
3009
3144
  * }
3010
3145
  *
3011
3146
  * const TaggedValueType = objectTag(
@@ -3015,9 +3150,13 @@ export function objectTag<Name extends keyof ObjectTagOutputByName>(
3015
3150
  * const value = new TaggedValue();
3016
3151
  * const result = TaggedValueType.fromUnknown(value);
3017
3152
  *
3018
- * expectOk(result, value);
3019
- * expectTypeOf(result.value).toExtend<TaggedValue>();
3020
- * expect(TaggedValueType.expected).toBe("TaggedValue");
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");
3021
3160
  * ```
3022
3161
  */
3023
3162
  export function objectTag<
@@ -3110,7 +3249,7 @@ export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
3110
3249
  * ### Example
3111
3250
  *
3112
3251
  * ```ts
3113
- * import { instanceOf } from "@evolu/common";
3252
+ * import { assertFalse, assertTrue, instanceOf } from "@evolu/common";
3114
3253
  *
3115
3254
  * class User {
3116
3255
  * readonly name: string;
@@ -3122,8 +3261,8 @@ export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
3122
3261
  *
3123
3262
  * const UserInstance = instanceOf(User);
3124
3263
  *
3125
- * assert(UserInstance.is(new User("Ada")));
3126
- * assert(!UserInstance.is({ name: "Ada" }));
3264
+ * assertTrue(UserInstance.is(new User("Ada")));
3265
+ * assertFalse(UserInstance.is({ name: "Ada" }));
3127
3266
  * ```
3128
3267
  *
3129
3268
  * @group Base
@@ -3225,13 +3364,23 @@ type InstanceConstructorCompileTimeError = CompileTimeError<
3225
3364
  * ### Example
3226
3365
  *
3227
3366
  * ```ts
3228
- * import { literal } from "@evolu/common";
3367
+ * import {
3368
+ * assertEqual,
3369
+ * assertErr,
3370
+ * assertOk,
3371
+ * assertType,
3372
+ * Data,
3373
+ * literal,
3374
+ * } from "@evolu/common";
3229
3375
  *
3230
3376
  * const Ready = literal("ready");
3231
3377
  *
3232
- * expectTypeOf<typeof Ready.Output>().toEqualTypeOf<"ready">();
3233
- * expectOk(Ready.fromUnknown("ready"), "ready");
3234
- * 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, {
3235
3384
  * type: "Literal",
3236
3385
  * expected: "ready",
3237
3386
  * value: "pending",
@@ -3388,17 +3537,29 @@ export const Null = /*#__PURE__*/ literal(null);
3388
3537
  * ### Example
3389
3538
  *
3390
3539
  * ```ts
3391
- * 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";
3392
3550
  *
3393
3551
  * const Status = union("draft", "published");
3394
3552
  * const StatusOrCode = union("draft", "published", Number);
3395
3553
  *
3396
- * expectOk(Status.fromUnknown("draft"), "draft");
3397
- * expectOk(StatusOrCode.fromUnknown(42), 42);
3554
+ * assertOk(Status.fromUnknown("draft"), "draft");
3555
+ * assertOk(StatusOrCode.fromUnknown(42), 42);
3398
3556
  *
3399
3557
  * const TextOrNumber = union(String, Number);
3400
3558
  *
3401
- * 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, {
3402
3563
  * type: "Union",
3403
3564
  * errors: [
3404
3565
  * {
@@ -3556,11 +3717,11 @@ const createUnionValidation =
3556
3717
  * ### Example
3557
3718
  *
3558
3719
  * ```ts
3559
- * import { String, undefinedOr } from "@evolu/common";
3720
+ * import { assertOk, String, undefinedOr } from "@evolu/common";
3560
3721
  *
3561
3722
  * const StringOrUndefined = undefinedOr(String);
3562
3723
  *
3563
- * expectOk(StringOrUndefined.fromUnknown(undefined), undefined);
3724
+ * assertOk(StringOrUndefined.fromUnknown(undefined), undefined);
3564
3725
  * ```
3565
3726
  *
3566
3727
  * @group Unions
@@ -3575,11 +3736,11 @@ export const undefinedOr = <ValueType extends TypeNode>(
3575
3736
  * ### Example
3576
3737
  *
3577
3738
  * ```ts
3578
- * import { String, nullOr } from "@evolu/common";
3739
+ * import { assertOk, String, nullOr } from "@evolu/common";
3579
3740
  *
3580
3741
  * const NullableString = nullOr(String);
3581
3742
  *
3582
- * expectOk(NullableString.fromUnknown(null), null);
3743
+ * assertOk(NullableString.fromUnknown(null), null);
3583
3744
  * ```
3584
3745
  *
3585
3746
  * @group Unions
@@ -3594,12 +3755,12 @@ export const nullOr = <ValueType extends TypeNode>(
3594
3755
  * ### Example
3595
3756
  *
3596
3757
  * ```ts
3597
- * import { String, nullishOr } from "@evolu/common";
3758
+ * import { assertOk, String, nullishOr } from "@evolu/common";
3598
3759
  *
3599
3760
  * const NullishString = nullishOr(String);
3600
3761
  *
3601
- * expectOk(NullishString.fromUnknown(undefined), undefined);
3602
- * expectOk(NullishString.fromUnknown(null), null);
3762
+ * assertOk(NullishString.fromUnknown(undefined), undefined);
3763
+ * assertOk(NullishString.fromUnknown(null), null);
3603
3764
  * ```
3604
3765
  *
3605
3766
  * @group Unions
@@ -3674,9 +3835,7 @@ interface RuntimeUnionTypeNode extends RuntimeTypeNode {
3674
3835
  const isRuntimeUnionTypeNode = (
3675
3836
  type: RuntimeTypeNode,
3676
3837
  ): type is RuntimeUnionTypeNode =>
3677
- type.name === "Union" &&
3678
- "members" in type &&
3679
- globalThis.Array.isArray(type.members);
3838
+ type.name === "Union" && "members" in type && Array.isArray(type.members);
3680
3839
 
3681
3840
  /**
3682
3841
  * A root {@link Type} validating the encoded Inputs accepted by {@link union}.
@@ -3837,7 +3996,16 @@ interface UnionErrorValue<
3837
3996
  * the structured data decoded from it:
3838
3997
  *
3839
3998
  * ```ts
3840
- * 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";
3841
4009
  *
3842
4010
  * const Language = union("en", "cs");
3843
4011
  * const Region = union("US", "CZ");
@@ -3847,36 +4015,39 @@ interface UnionErrorValue<
3847
4015
  *
3848
4016
  * // Output is the decoded language and region.
3849
4017
  * type SupportedLocale = typeof SupportedLocale.Output;
3850
- * expectTypeOf<SupportedLocale>().toEqualTypeOf<
3851
- * readonly ["en" | "cs", "US" | "CZ"]
3852
- * >();
4018
+ * assertType<readonly ["en" | "cs", "US" | "CZ"], SupportedLocale>();
3853
4019
  *
3854
4020
  * // The parent Output is the canonical locale string.
3855
4021
  * type SupportedLocaleLiteral = typeof SupportedLocale.parent.Output;
3856
- * expectTypeOf<SupportedLocaleLiteral>().toEqualTypeOf<
3857
- * "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
4022
+ * assertType<
4023
+ * "en-US" | "en-CZ" | "cs-US" | "cs-CZ",
4024
+ * SupportedLocaleLiteral
3858
4025
  * >();
3859
4026
  *
3860
4027
  * // Parse an unknown string into structured data.
3861
4028
  * const result = SupportedLocale.fromUnknown("cs-CZ");
3862
- * expectOk(result, ["cs", "CZ"]);
4029
+ * assertOk(result, ["cs", "CZ"]);
3863
4030
  * const locale = result.value;
3864
- * expectTypeOf(locale).toEqualTypeOf<SupportedLocale>();
3865
- * 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, {
3866
4037
  * type: "TemplateLiteral",
3867
4038
  * value: "cs/CZ",
3868
4039
  * });
3869
4040
  *
3870
4041
  * // Encode structured data into its canonical string.
3871
4042
  * const localeLiteral = SupportedLocale.to(locale);
3872
- * expectTypeOf(localeLiteral).toEqualTypeOf<SupportedLocaleLiteral>();
3873
- * expect(localeLiteral).toBe("cs-CZ");
4043
+ * assertType<SupportedLocaleLiteral, typeof localeLiteral>();
4044
+ * assertEqual(localeLiteral, "cs-CZ");
3874
4045
  *
3875
4046
  * // Validate a string configuration value.
3876
4047
  * const configValue: unknown = "cs-CZ";
3877
- * assert(SupportedLocale.parent.is(configValue));
3878
- * expectTypeOf(configValue).toEqualTypeOf<SupportedLocaleLiteral>();
3879
- * 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"));
3880
4051
  * ```
3881
4052
  *
3882
4053
  * `SupportedLocale` is structured data for application code.
@@ -3889,6 +4060,8 @@ interface UnionErrorValue<
3889
4060
  *
3890
4061
  * ```ts
3891
4062
  * import {
4063
+ * assertEqual,
4064
+ * assertOk,
3892
4065
  * NonNegativeDecimalString,
3893
4066
  * templateLiteralParser,
3894
4067
  * } from "@evolu/common";
@@ -3901,8 +4074,8 @@ interface UnionErrorValue<
3901
4074
  * // DecimalText.to requires a validated NonNegativeDecimalString.
3902
4075
  * const zero = NonNegativeDecimalString.orThrow("0");
3903
4076
  *
3904
- * expectOk(DecimalText.fromUnknown("decimal:0"), [zero]);
3905
- * expect(DecimalText.to([zero])).toBe("decimal:0");
4077
+ * assertOk(DecimalText.fromUnknown("decimal:0"), [zero]);
4078
+ * assertEqual(DecimalText.to([zero]), "decimal:0");
3906
4079
  * ```
3907
4080
  *
3908
4081
  * Capture Types (the Type arguments passed to `templateLiteralParser`) can use
@@ -3910,6 +4083,9 @@ interface UnionErrorValue<
3910
4083
  *
3911
4084
  * ```ts
3912
4085
  * import {
4086
+ * assertEqual,
4087
+ * assertOk,
4088
+ * assertType,
3913
4089
  * Int64FromInt64String,
3914
4090
  * templateLiteralParser,
3915
4091
  * } from "@evolu/common";
@@ -3920,24 +4096,29 @@ interface UnionErrorValue<
3920
4096
  *
3921
4097
  * // Decode the string into structured data.
3922
4098
  * const result = ItemId.fromUnknown("item-42");
3923
- * expectOk(result, [42n]);
4099
+ * assertOk(result, [42n]);
3924
4100
  * const itemId = result.value;
3925
- * expectTypeOf(itemId).toEqualTypeOf<ItemId>();
4101
+ * assertType<ItemId, typeof itemId>();
3926
4102
  *
3927
4103
  * // Encode the structured data into its canonical string.
3928
4104
  * const itemIdLiteral = ItemId.to(itemId);
3929
- * expectTypeOf(itemIdLiteral).toEqualTypeOf<ItemIdLiteral>();
3930
- * expect(itemIdLiteral).toBe("item-42");
4105
+ * assertType<ItemIdLiteral, typeof itemIdLiteral>();
4106
+ * assertEqual(itemIdLiteral, "item-42");
3931
4107
  *
3932
4108
  * // TypeScript cannot prove from the literal alone that "42" is a valid Int64 encoding.
3933
4109
  * // @ts-expect-error Validate it with ItemId.parent or create it with ItemId.to.
3934
- * const invalidItemIdLiteral: ItemIdLiteral = "item-42";
4110
+ * const _invalidItemIdLiteral: ItemIdLiteral = "item-42";
3935
4111
  * ```
3936
4112
  *
3937
4113
  * Fixed-width captures can be adjacent:
3938
4114
  *
3939
4115
  * ```ts
3940
- * import { templateLiteralParser, union } from "@evolu/common";
4116
+ * import {
4117
+ * assertEqual,
4118
+ * assertOk,
4119
+ * templateLiteralParser,
4120
+ * union,
4121
+ * } from "@evolu/common";
3941
4122
  *
3942
4123
  * const Digit = union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
3943
4124
  * const TwoDigits = templateLiteralParser(Digit, Digit);
@@ -3947,10 +4128,10 @@ interface UnionErrorValue<
3947
4128
  * const twoDigits: TwoDigits = ["4", "2"];
3948
4129
  * const twoDigitsLiteral: TwoDigitsLiteral = "42";
3949
4130
  * // @ts-expect-error TwoDigitsLiteral requires exactly two digits.
3950
- * const threeDigitsLiteral: TwoDigitsLiteral = "123";
4131
+ * const _threeDigitsLiteral: TwoDigitsLiteral = "123";
3951
4132
  *
3952
- * expectOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
3953
- * expect(TwoDigits.to(twoDigits)).toBe(twoDigitsLiteral);
4133
+ * assertOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
4134
+ * assertEqual(TwoDigits.to(twoDigits), twoDigitsLiteral);
3954
4135
  * ```
3955
4136
  *
3956
4137
  * TypeScript rejects multiple variable-width captures because their encoded
@@ -4236,17 +4417,24 @@ export interface TemplateLiteralType<
4236
4417
  * ### Example
4237
4418
  *
4238
4419
  * ```ts
4239
- * import { templateLiteral, union } from "@evolu/common";
4420
+ * import {
4421
+ * assertFalse,
4422
+ * assertOk,
4423
+ * assertType,
4424
+ * templateLiteral,
4425
+ * union,
4426
+ * } from "@evolu/common";
4240
4427
  *
4241
4428
  * const Language = union("en", "cs");
4242
4429
  * const Region = union("US", "CZ");
4243
4430
  * const Locale = templateLiteral(Language, "-", Region);
4244
4431
  *
4245
- * expectTypeOf<typeof Locale.Output>().toEqualTypeOf<
4246
- * "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
4432
+ * assertType<
4433
+ * "en-US" | "en-CZ" | "cs-US" | "cs-CZ",
4434
+ * typeof Locale.Output
4247
4435
  * >();
4248
- * expectOk(Locale.fromUnknown("cs-CZ"), "cs-CZ");
4249
- * expect(Locale.is("fr-CZ")).toBe(false);
4436
+ * assertOk(Locale.fromUnknown("cs-CZ"), "cs-CZ");
4437
+ * assertFalse(Locale.is("fr-CZ"));
4250
4438
  * ```
4251
4439
  *
4252
4440
  * @group Template literals
@@ -4516,7 +4704,7 @@ const compileTemplateLiteralParser = <Parts extends TemplateLiteralParts>(
4516
4704
  });
4517
4705
 
4518
4706
  return (input) => {
4519
- const inputCodePoints = globalThis.Array.from(input);
4707
+ const inputCodePoints = Array.from(input);
4520
4708
  const variableWidth = inputCodePoints.length - fixedPartsWidth;
4521
4709
  if (variableWidth < 0) {
4522
4710
  return err({ type: "TemplateLiteral", value: input });
@@ -4592,7 +4780,7 @@ const getStringTemplateLiteralFraming = (
4592
4780
  const lastCodeUnit = value.charCodeAt(value.length - 1);
4593
4781
 
4594
4782
  return {
4595
- width: globalThis.Array.from(value).length,
4783
+ width: Array.from(value).length,
4596
4784
  canBeEmpty: value.length === 0,
4597
4785
  canStartWithLowSurrogate:
4598
4786
  firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff,
@@ -4677,8 +4865,13 @@ const getTemplateLiteralPartFraming = (
4677
4865
  *
4678
4866
  * ```ts
4679
4867
  * import {
4868
+ * assertEqual,
4869
+ * assertErr,
4870
+ * assertOk,
4871
+ * assertType,
4680
4872
  * BigInt,
4681
4873
  * brand,
4874
+ * Data,
4682
4875
  * err,
4683
4876
  * ok,
4684
4877
  * type Brand,
@@ -4697,14 +4890,17 @@ const getTemplateLiteralPartFraming = (
4697
4890
  * type Int64 = typeof Int64.Output;
4698
4891
  *
4699
4892
  * // Note the Brand.
4700
- * expectTypeOf<Int64>().toEqualTypeOf<bigint & Brand<"Int64">>();
4893
+ * assertType<bigint & Brand<"Int64">, Int64>();
4701
4894
  *
4702
4895
  * interface Int64Error extends TypeError<"Int64"> {
4703
4896
  * readonly value: bigint;
4704
4897
  * }
4705
4898
  *
4706
- * expectOk(Int64.fromUnknown(42n), 42n);
4707
- * 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, {
4708
4904
  * type: "Int64",
4709
4905
  * value: 2n ** 63n,
4710
4906
  * });
@@ -4788,13 +4984,21 @@ export interface BrandType<
4788
4984
  * ### Example
4789
4985
  *
4790
4986
  * ```ts
4791
- * import { DateIso } from "@evolu/common";
4987
+ * import {
4988
+ * assertEqual,
4989
+ * assertErr,
4990
+ * assertOk,
4991
+ * assertType,
4992
+ * Data,
4993
+ * DateIso,
4994
+ * } from "@evolu/common";
4792
4995
  *
4793
- * expectOk(
4794
- * DateIso.fromUnknown("2023-01-01T12:00:00.000Z"),
4795
- * "2023-01-01T12:00:00.000Z",
4796
- * );
4797
- * 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, {
4798
5002
  * type: "DateIso",
4799
5003
  * value: "2023-01-01",
4800
5004
  * });
@@ -4829,13 +5033,13 @@ export interface DateIsoError extends TypeError<"DateIso"> {
4829
5033
  * ### Example
4830
5034
  *
4831
5035
  * ```ts
4832
- * import { DateIsoFromDate } from "@evolu/common";
5036
+ * import { assertEqual, assertOk, DateIsoFromDate } from "@evolu/common";
4833
5037
  *
4834
- * const date = new globalThis.Date("2025-01-01T12:00:00.000Z");
5038
+ * const date = new Date("2025-01-01T12:00:00.000Z");
4835
5039
  * const result = DateIsoFromDate.fromUnknown(date);
4836
5040
  *
4837
- * expectOk(result, "2025-01-01T12:00:00.000Z");
4838
- * expect(DateIsoFromDate.to(result.value)).toEqual(date);
5041
+ * assertOk(result, "2025-01-01T12:00:00.000Z");
5042
+ * assertEqual(DateIsoFromDate.to(result.value), date);
4839
5043
  * ```
4840
5044
  *
4841
5045
  * @group String
@@ -4928,6 +5132,11 @@ export interface UInt64Error extends TypeError<"UInt64"> {
4928
5132
  *
4929
5133
  * ```ts
4930
5134
  * import {
5135
+ * assertEqual,
5136
+ * assertErr,
5137
+ * assertOk,
5138
+ * assertType,
5139
+ * Data,
4931
5140
  * String,
4932
5141
  * brand,
4933
5142
  * err,
@@ -4953,14 +5162,17 @@ export interface UInt64Error extends TypeError<"UInt64"> {
4953
5162
  * const TrimmedString = trimmed(String);
4954
5163
  * type TrimmedString = typeof TrimmedString.Output;
4955
5164
  *
4956
- * expectTypeOf<TrimmedString>().toEqualTypeOf<string & Brand<"Trimmed">>();
5165
+ * assertType<string & Brand<"Trimmed">, TrimmedString>();
4957
5166
  *
4958
5167
  * interface TrimmedError extends TypeError<"Trimmed"> {
4959
5168
  * readonly value: string;
4960
5169
  * }
4961
5170
  *
4962
- * expectOk(TrimmedString.fromUnknown("Evolu"), "Evolu");
4963
- * 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, {
4964
5176
  * type: "Trimmed",
4965
5177
  * value: " Evolu",
4966
5178
  * });
@@ -4992,6 +5204,7 @@ export type BrandFactory<
4992
5204
  *
4993
5205
  * ```ts
4994
5206
  * import {
5207
+ * assertType,
4995
5208
  * Number,
4996
5209
  * brand,
4997
5210
  * err,
@@ -5023,10 +5236,8 @@ export type BrandFactory<
5023
5236
  * const LessThan100 = lessThan(100)(Number);
5024
5237
  * type LessThan100 = typeof LessThan100.Output;
5025
5238
  *
5026
- * expectTypeOf(LessThan100.name).toEqualTypeOf<"LessThan100">();
5027
- * expectTypeOf<LessThan100>().toEqualTypeOf<
5028
- * number & Brand<"LessThan100">
5029
- * >();
5239
+ * assertType<"LessThan100", typeof LessThan100.name>();
5240
+ * assertType<number & Brand<"LessThan100">, LessThan100>();
5030
5241
  *
5031
5242
  * interface LessThanError<
5032
5243
  * Max extends number,
@@ -5061,17 +5272,27 @@ type BrandFactoryNumberError = CompileTimeError<
5061
5272
  * ### Example
5062
5273
  *
5063
5274
  * ```ts
5064
- * 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";
5065
5285
  *
5066
5286
  * const CapitalizedString = capitalized(String);
5067
5287
  * type CapitalizedString = typeof CapitalizedString.Output;
5068
5288
  *
5069
- * expectTypeOf<CapitalizedString>().toEqualTypeOf<
5070
- * string & Brand<"Capitalized">
5071
- * >();
5289
+ * assertType<string & Brand<"Capitalized">, CapitalizedString>();
5072
5290
  *
5073
- * expectOk(CapitalizedString.fromUnknown("Evolu"), "Evolu");
5074
- * 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, {
5075
5296
  * type: "Capitalized",
5076
5297
  * value: "evolu",
5077
5298
  * });
@@ -5121,11 +5342,11 @@ export type CapitalizedString = typeof CapitalizedString.Output;
5121
5342
  * ### Example
5122
5343
  *
5123
5344
  * ```ts
5124
- * import { String, trimmed } from "@evolu/common";
5345
+ * import { assertOk, String, trimmed } from "@evolu/common";
5125
5346
  *
5126
5347
  * const Trimmed = trimmed(String);
5127
5348
  *
5128
- * expectOk(Trimmed.fromUnknown("Evolu"), "Evolu");
5349
+ * assertOk(Trimmed.fromUnknown("Evolu"), "Evolu");
5129
5350
  * ```
5130
5351
  *
5131
5352
  * @group String
@@ -5173,9 +5394,9 @@ export type TrimmedString = typeof TrimmedString.Output;
5173
5394
  * ### Example
5174
5395
  *
5175
5396
  * ```ts
5176
- * import { trim } from "@evolu/common";
5397
+ * import { assertEqual, trim } from "@evolu/common";
5177
5398
  *
5178
- * expect(trim(" Evolu ")).toBe("Evolu");
5399
+ * assertEqual(trim(" Evolu "), "Evolu");
5179
5400
  * ```
5180
5401
  *
5181
5402
  * @group String
@@ -5189,13 +5410,13 @@ export const trim = (value: string): TrimmedString =>
5189
5410
  * ### Example
5190
5411
  *
5191
5412
  * ```ts
5192
- * import { String, array, minLength } from "@evolu/common";
5413
+ * import { assertOk, String, array, minLength } from "@evolu/common";
5193
5414
  *
5194
5415
  * const AtLeastThreeCharacters = minLength(3)(String);
5195
5416
  * const AtLeastTwoItems = minLength(2)(array(String));
5196
5417
  *
5197
- * expectOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
5198
- * expectOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5418
+ * assertOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
5419
+ * assertOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5199
5420
  * ```
5200
5421
  *
5201
5422
  * @group String
@@ -5255,13 +5476,13 @@ export type NonEmptyTrimmedString = typeof NonEmptyTrimmedString.Output;
5255
5476
  * ### Example
5256
5477
  *
5257
5478
  * ```ts
5258
- * import { String, array, maxLength } from "@evolu/common";
5479
+ * import { assertOk, String, array, maxLength } from "@evolu/common";
5259
5480
  *
5260
5481
  * const AtMostThreeCharacters = maxLength(3)(String);
5261
5482
  * const AtMostTwoItems = maxLength(2)(array(String));
5262
5483
  *
5263
- * expectOk(AtMostThreeCharacters.fromUnknown("abc"), "abc");
5264
- * expectOk(AtMostTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5484
+ * assertOk(AtMostThreeCharacters.fromUnknown("abc"), "abc");
5485
+ * assertOk(AtMostTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5265
5486
  * ```
5266
5487
  *
5267
5488
  * @group String
@@ -5324,13 +5545,13 @@ export type NonEmptyTrimmedString1000 = typeof NonEmptyTrimmedString1000.Output;
5324
5545
  * ### Example
5325
5546
  *
5326
5547
  * ```ts
5327
- * import { String, array, length } from "@evolu/common";
5548
+ * import { assertOk, String, array, length } from "@evolu/common";
5328
5549
  *
5329
5550
  * const ThreeCharacters = length(3)(String);
5330
5551
  * const TwoItems = length(2)(array(String));
5331
5552
  *
5332
- * expectOk(ThreeCharacters.fromUnknown("abc"), "abc");
5333
- * expectOk(TwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5553
+ * assertOk(ThreeCharacters.fromUnknown("abc"), "abc");
5554
+ * assertOk(TwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
5334
5555
  * ```
5335
5556
  *
5336
5557
  * @group String
@@ -5375,21 +5596,34 @@ export interface LengthError<
5375
5596
  * A non-empty string using the URL-safe alphabet:
5376
5597
  *
5377
5598
  * ```ts
5378
- * 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";
5379
5609
  *
5380
- * const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
5610
+ * const UrlSafeString = regex(
5611
+ * "UrlSafeString",
5612
+ * /^[A-Za-z0-9_-]+$/u,
5613
+ * )(String);
5381
5614
  * type UrlSafeString = typeof UrlSafeString.Output;
5382
5615
  *
5383
- * expectTypeOf<UrlSafeString>().toEqualTypeOf<
5384
- * string & Brand<"UrlSafeString">
5385
- * >();
5616
+ * assertType<string & Brand<"UrlSafeString">, UrlSafeString>();
5386
5617
  *
5387
- * expectOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
5388
- * 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, {
5389
5623
  * type: "UrlSafeString",
5390
5624
  * value: "not safe",
5391
5625
  * source: "^[A-Za-z0-9_-]+$",
5392
- * flags: "",
5626
+ * flags: "u",
5393
5627
  * });
5394
5628
  * ```
5395
5629
  *
@@ -5468,10 +5702,10 @@ const uint8ArrayToBase64UrlString = (bytes: Uint8Array): string => {
5468
5702
  return bytes.toBase64(base64UrlOptions);
5469
5703
  }
5470
5704
 
5471
- const binaryString = globalThis.Array.from(bytes, (byte) =>
5705
+ const binaryString = Array.from(bytes, (byte) =>
5472
5706
  globalThis.String.fromCodePoint(byte),
5473
5707
  ).join("");
5474
- const base64 = globalThis.btoa(binaryString);
5708
+ const base64 = btoa(binaryString);
5475
5709
 
5476
5710
  return base64.replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
5477
5711
  };
@@ -5489,7 +5723,7 @@ const base64UrlStringToUint8Array = (value: string): Uint8Array => {
5489
5723
  let base64 = value.replaceAll("-", "+").replaceAll("_", "/");
5490
5724
  while (base64.length % 4 !== 0) base64 += "=";
5491
5725
 
5492
- const binaryString = globalThis.atob(base64);
5726
+ const binaryString = atob(base64);
5493
5727
  return globalThis.Uint8Array.from(binaryString, (character) =>
5494
5728
  character.charCodeAt(0),
5495
5729
  );
@@ -5533,9 +5767,10 @@ export interface Base64UrlError extends TypeError<"Base64Url"> {
5533
5767
  * ### Example
5534
5768
  *
5535
5769
  * ```ts
5536
- * import { uint8ArrayToBase64Url } from "@evolu/common";
5770
+ * import { assertEqual, uint8ArrayToBase64Url } from "@evolu/common";
5537
5771
  *
5538
- * expect(uint8ArrayToBase64Url(new Uint8Array([0, 1, 2, 255]))).toBe(
5772
+ * assertEqual(
5773
+ * uint8ArrayToBase64Url(new Uint8Array([0, 1, 2, 255])),
5539
5774
  * "AAEC_w",
5540
5775
  * );
5541
5776
  * ```
@@ -5551,11 +5786,16 @@ export const uint8ArrayToBase64Url = (bytes: Uint8Array): Base64Url =>
5551
5786
  * ### Example
5552
5787
  *
5553
5788
  * ```ts
5554
- * import { Base64Url, base64UrlToUint8Array } from "@evolu/common";
5789
+ * import {
5790
+ * assertEqual,
5791
+ * Base64Url,
5792
+ * base64UrlToUint8Array,
5793
+ * } from "@evolu/common";
5555
5794
  *
5556
5795
  * const value = Base64Url.orThrow("AAEC_w");
5557
5796
  *
5558
- * expect(base64UrlToUint8Array(value)).toEqual(
5797
+ * assertEqual(
5798
+ * base64UrlToUint8Array(value),
5559
5799
  * new Uint8Array([0, 1, 2, 255]),
5560
5800
  * );
5561
5801
  * ```
@@ -5674,6 +5914,8 @@ export interface IdError extends TypeError<"Id"> {
5674
5914
  *
5675
5915
  * ```ts
5676
5916
  * import {
5917
+ * assertTrue,
5918
+ * assertType,
5677
5919
  * Id,
5678
5920
  * createId,
5679
5921
  * createRandomBytes,
@@ -5682,8 +5924,8 @@ export interface IdError extends TypeError<"Id"> {
5682
5924
  *
5683
5925
  * const userId = createId<"User">({ randomBytes: createRandomBytes() });
5684
5926
  *
5685
- * expect(Id.is(userId)).toBe(true);
5686
- * expectTypeOf(userId).toEqualTypeOf<Id & Brand<"User">>();
5927
+ * assertTrue(Id.is(userId));
5928
+ * assertType<Id & Brand<"User">, typeof userId>();
5687
5929
  * ```
5688
5930
  *
5689
5931
  * @group String
@@ -5704,14 +5946,20 @@ export const createId = <B extends string = never>(
5704
5946
  * ### Example
5705
5947
  *
5706
5948
  * ```ts
5707
- * 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";
5708
5956
  *
5709
5957
  * const first = createIdFromString("external-user-123");
5710
5958
  * const second = createIdFromString("external-user-123");
5711
5959
  * const todoId = createIdFromString<"Todo">("external-todo-456");
5712
5960
  *
5713
- * expect(first).toBe(second);
5714
- * expectTypeOf(todoId).toEqualTypeOf<Id & Brand<"Todo">>();
5961
+ * assertEqual(first, second);
5962
+ * assertType<Id & Brand<"Todo">, typeof todoId>();
5715
5963
  * ```
5716
5964
  *
5717
5965
  * @group String
@@ -5736,6 +5984,7 @@ export const createIdFromString = <B extends string = never>(
5736
5984
  *
5737
5985
  * ```ts
5738
5986
  * import {
5987
+ * assertEqual,
5739
5988
  * createIdAsUuidv7,
5740
5989
  * createRandomBytes,
5741
5990
  * createTime,
@@ -5748,8 +5997,8 @@ export const createIdFromString = <B extends string = never>(
5748
5997
  * });
5749
5998
  * const bytes = idToIdBytes(value);
5750
5999
  *
5751
- * expect(bytes[6] >> 4).toBe(0x7);
5752
- * expect(bytes[8] & 0xc0).toBe(0x80);
6000
+ * assertEqual(bytes[6] >> 4, 0x7);
6001
+ * assertEqual(bytes[8] & 0xc0, 0x80);
5753
6002
  * ```
5754
6003
  *
5755
6004
  * @group String
@@ -5780,6 +6029,7 @@ export const createIdAsUuidv7 = <B extends string = never>(
5780
6029
  *
5781
6030
  * ```ts
5782
6031
  * import {
6032
+ * assertType,
5783
6033
  * createIdFromString,
5784
6034
  * id,
5785
6035
  * type Brand,
@@ -5789,7 +6039,7 @@ export const createIdAsUuidv7 = <B extends string = never>(
5789
6039
  * const TodoId = id("Todo");
5790
6040
  * const todoId = TodoId.orThrow(createIdFromString("todo"));
5791
6041
  *
5792
- * expectTypeOf(todoId).toEqualTypeOf<Id & Brand<"Todo">>();
6042
+ * assertType<Id & Brand<"Todo">, typeof todoId>();
5793
6043
  * ```
5794
6044
  *
5795
6045
  * @group String
@@ -5903,11 +6153,15 @@ export const idBytesTypeValueLength = 16 as NonNegativeInt;
5903
6153
  * ### Example
5904
6154
  *
5905
6155
  * ```ts
5906
- * import { createIdFromString, idToIdBytes } from "@evolu/common";
6156
+ * import {
6157
+ * assertEqual,
6158
+ * createIdFromString,
6159
+ * idToIdBytes,
6160
+ * } from "@evolu/common";
5907
6161
  *
5908
6162
  * const bytes = idToIdBytes(createIdFromString("todo"));
5909
6163
  *
5910
- * expect(bytes).toHaveLength(16);
6164
+ * assertEqual(bytes.length, 16);
5911
6165
  * ```
5912
6166
  *
5913
6167
  * @group String
@@ -5922,6 +6176,7 @@ export const idToIdBytes = (value: Id): IdBytes =>
5922
6176
  *
5923
6177
  * ```ts
5924
6178
  * import {
6179
+ * assertEqual,
5925
6180
  * createIdFromString,
5926
6181
  * idBytesToId,
5927
6182
  * idToIdBytes,
@@ -5929,7 +6184,7 @@ export const idToIdBytes = (value: Id): IdBytes =>
5929
6184
  *
5930
6185
  * const value = createIdFromString("todo");
5931
6186
  *
5932
- * expect(idBytesToId(idToIdBytes(value))).toBe(value);
6187
+ * assertEqual(idBytesToId(idToIdBytes(value)), value);
5933
6188
  * ```
5934
6189
  *
5935
6190
  * @group String
@@ -5986,12 +6241,17 @@ export interface Int64StringError extends TypeError<"Int64String"> {
5986
6241
  * ### Example
5987
6242
  *
5988
6243
  * ```ts
5989
- * import { Int64FromInt64String } from "@evolu/common";
6244
+ * import {
6245
+ * assertEqual,
6246
+ * assertOk,
6247
+ * Int64FromInt64String,
6248
+ * } from "@evolu/common";
5990
6249
  *
5991
6250
  * const result = Int64FromInt64String.fromUnknown("9223372036854775807");
5992
6251
  *
5993
- * expectOk(result, 9223372036854775807n);
5994
- * expect(Int64FromInt64String.to(result.value)).toBe(
6252
+ * assertOk(result, 9223372036854775807n);
6253
+ * assertEqual(
6254
+ * Int64FromInt64String.to(result.value),
5995
6255
  * "9223372036854775807",
5996
6256
  * );
5997
6257
  * ```
@@ -6014,11 +6274,11 @@ export const Int64FromInt64String = /*#__PURE__*/ transform(
6014
6274
  * ### Example
6015
6275
  *
6016
6276
  * ```ts
6017
- * import { Number, nonNegative } from "@evolu/common";
6277
+ * import { assertOk, Number, nonNegative } from "@evolu/common";
6018
6278
  *
6019
6279
  * const NonNegative = nonNegative(Number);
6020
6280
  *
6021
- * expectOk(NonNegative.fromUnknown(0), 0);
6281
+ * assertOk(NonNegative.fromUnknown(0), 0);
6022
6282
  * ```
6023
6283
  *
6024
6284
  * @group Number
@@ -6060,11 +6320,11 @@ export type NonNegativeNumber = typeof NonNegativeNumber.Output;
6060
6320
  * ### Example
6061
6321
  *
6062
6322
  * ```ts
6063
- * import { Number, positive } from "@evolu/common";
6323
+ * import { assertOk, Number, positive } from "@evolu/common";
6064
6324
  *
6065
6325
  * const Positive = positive(Number);
6066
6326
  *
6067
- * expectOk(Positive.fromUnknown(1), 1);
6327
+ * assertOk(Positive.fromUnknown(1), 1);
6068
6328
  * ```
6069
6329
  *
6070
6330
  * @group Number
@@ -6107,11 +6367,11 @@ export type PositiveNumber = typeof PositiveNumber.Output;
6107
6367
  * ### Example
6108
6368
  *
6109
6369
  * ```ts
6110
- * import { Number, nonPositive } from "@evolu/common";
6370
+ * import { assertOk, Number, nonPositive } from "@evolu/common";
6111
6371
  *
6112
6372
  * const NonPositive = nonPositive(Number);
6113
6373
  *
6114
- * expectOk(NonPositive.fromUnknown(0), 0);
6374
+ * assertOk(NonPositive.fromUnknown(0), 0);
6115
6375
  * ```
6116
6376
  *
6117
6377
  * @group Number
@@ -6153,11 +6413,11 @@ export type NonPositiveNumber = typeof NonPositiveNumber.Output;
6153
6413
  * ### Example
6154
6414
  *
6155
6415
  * ```ts
6156
- * import { Number, negative } from "@evolu/common";
6416
+ * import { assertOk, Number, negative } from "@evolu/common";
6157
6417
  *
6158
6418
  * const Negative = negative(Number);
6159
6419
  *
6160
- * expectOk(Negative.fromUnknown(-1), -1);
6420
+ * assertOk(Negative.fromUnknown(-1), -1);
6161
6421
  * ```
6162
6422
  *
6163
6423
  * @group Number
@@ -6200,11 +6460,11 @@ export type NegativeNumber = typeof NegativeNumber.Output;
6200
6460
  * ### Example
6201
6461
  *
6202
6462
  * ```ts
6203
- * import { Number, nonNaN } from "@evolu/common";
6463
+ * import { assertOk, Number, nonNaN } from "@evolu/common";
6204
6464
  *
6205
6465
  * const NonNaN = nonNaN(Number);
6206
6466
  *
6207
- * expectOk(NonNaN.fromUnknown(Infinity), Infinity);
6467
+ * assertOk(NonNaN.fromUnknown(Infinity), Infinity);
6208
6468
  * ```
6209
6469
  *
6210
6470
  * @group Number
@@ -6248,11 +6508,11 @@ export type NonNaNNumber = typeof NonNaNNumber.Output;
6248
6508
  * ### Example
6249
6509
  *
6250
6510
  * ```ts
6251
- * import { Number, finite } from "@evolu/common";
6511
+ * import { assertOk, Number, finite } from "@evolu/common";
6252
6512
  *
6253
6513
  * const Finite = finite(Number);
6254
6514
  *
6255
- * expectOk(Finite.fromUnknown(42), 42);
6515
+ * assertOk(Finite.fromUnknown(42), 42);
6256
6516
  * ```
6257
6517
  *
6258
6518
  * @group Number
@@ -6313,15 +6573,28 @@ export type PositiveFiniteNumber = typeof PositiveFiniteNumber.Output;
6313
6573
  * ### Example
6314
6574
  *
6315
6575
  * ```ts
6316
- * 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";
6317
6586
  *
6318
6587
  * const Int = int(Number);
6319
6588
  * type Int = typeof Int.Output;
6320
6589
  *
6321
- * expectTypeOf<Int>().toEqualTypeOf<number & Brand<"Int">>();
6590
+ * assertType<number & Brand<"Int">, Int>();
6322
6591
  *
6323
- * expectOk(Int.fromUnknown(42), 42);
6324
- * 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 });
6325
6598
  * ```
6326
6599
  *
6327
6600
  * @group Number
@@ -6421,11 +6694,11 @@ export type NegativeInt = typeof NegativeInt.Output;
6421
6694
  * ### Example
6422
6695
  *
6423
6696
  * ```ts
6424
- * import { Number, greaterThan } from "@evolu/common";
6697
+ * import { assertOk, Number, greaterThan } from "@evolu/common";
6425
6698
  *
6426
6699
  * const GreaterThanTen = greaterThan(10)(Number);
6427
6700
  *
6428
- * expectOk(GreaterThanTen.fromUnknown(11), 11);
6701
+ * assertOk(GreaterThanTen.fromUnknown(11), 11);
6429
6702
  * ```
6430
6703
  *
6431
6704
  * @group Number
@@ -6467,11 +6740,11 @@ export interface GreaterThanError<
6467
6740
  * ### Example
6468
6741
  *
6469
6742
  * ```ts
6470
- * import { Number, greaterThanOrEqualTo } from "@evolu/common";
6743
+ * import { assertOk, Number, greaterThanOrEqualTo } from "@evolu/common";
6471
6744
  *
6472
6745
  * const AtLeastTen = greaterThanOrEqualTo(10)(Number);
6473
6746
  *
6474
- * expectOk(AtLeastTen.fromUnknown(10), 10);
6747
+ * assertOk(AtLeastTen.fromUnknown(10), 10);
6475
6748
  * ```
6476
6749
  *
6477
6750
  * @group Number
@@ -6521,11 +6794,11 @@ export interface GreaterThanOrEqualToError<
6521
6794
  * ### Example
6522
6795
  *
6523
6796
  * ```ts
6524
- * import { Number, lessThan } from "@evolu/common";
6797
+ * import { assertOk, Number, lessThan } from "@evolu/common";
6525
6798
  *
6526
6799
  * const LessThanTen = lessThan(10)(Number);
6527
6800
  *
6528
- * expectOk(LessThanTen.fromUnknown(9), 9);
6801
+ * assertOk(LessThanTen.fromUnknown(9), 9);
6529
6802
  * ```
6530
6803
  *
6531
6804
  * @group Number
@@ -6578,11 +6851,11 @@ export type Age = typeof Age.Output;
6578
6851
  * ### Example
6579
6852
  *
6580
6853
  * ```ts
6581
- * import { Number, lessThanOrEqualTo } from "@evolu/common";
6854
+ * import { assertOk, Number, lessThanOrEqualTo } from "@evolu/common";
6582
6855
  *
6583
6856
  * const AtMostTen = lessThanOrEqualTo(10)(Number);
6584
6857
  *
6585
- * expectOk(AtMostTen.fromUnknown(10), 10);
6858
+ * assertOk(AtMostTen.fromUnknown(10), 10);
6586
6859
  * ```
6587
6860
  *
6588
6861
  * @group Number
@@ -6662,13 +6935,23 @@ export type Ratio = typeof Ratio.Output;
6662
6935
  * ### Example
6663
6936
  *
6664
6937
  * ```ts
6665
- * import { DecimalString } from "@evolu/common";
6938
+ * import {
6939
+ * assertEqual,
6940
+ * assertErr,
6941
+ * assertOk,
6942
+ * assertType,
6943
+ * Data,
6944
+ * DecimalString,
6945
+ * } from "@evolu/common";
6666
6946
  *
6667
- * expectOk(DecimalString.fromUnknown("-10.25"), "-10.25");
6668
- * expectOk(DecimalString.fromUnknown("0"), "0");
6669
- * 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");
6670
6950
  *
6671
- * expectErr(DecimalString.fromUnknown("10.250"), {
6951
+ * const invalid = DecimalString.fromUnknown("10.250");
6952
+ * assertErr(invalid);
6953
+ * assertType(Data, invalid.error);
6954
+ * assertEqual(invalid.error, {
6672
6955
  * type: "DecimalString",
6673
6956
  * value: "10.250",
6674
6957
  * });
@@ -6703,11 +6986,15 @@ export interface DecimalStringError extends TypeError<"DecimalString"> {
6703
6986
  * ### Example
6704
6987
  *
6705
6988
  * ```ts
6706
- * import { DecimalString, nonNegativeDecimalString } from "@evolu/common";
6989
+ * import {
6990
+ * assertOk,
6991
+ * DecimalString,
6992
+ * nonNegativeDecimalString,
6993
+ * } from "@evolu/common";
6707
6994
  *
6708
6995
  * const NonNegative = nonNegativeDecimalString(DecimalString);
6709
6996
  *
6710
- * expectOk(NonNegative.fromUnknown("0.5"), "0.5");
6997
+ * assertOk(NonNegative.fromUnknown("0.5"), "0.5");
6711
6998
  * ```
6712
6999
  *
6713
7000
  * @group Number
@@ -6755,11 +7042,15 @@ export type NonNegativeDecimalString = typeof NonNegativeDecimalString.Output;
6755
7042
  * ### Example
6756
7043
  *
6757
7044
  * ```ts
6758
- * import { DecimalString, positiveDecimalString } from "@evolu/common";
7045
+ * import {
7046
+ * assertOk,
7047
+ * DecimalString,
7048
+ * positiveDecimalString,
7049
+ * } from "@evolu/common";
6759
7050
  *
6760
7051
  * const Positive = positiveDecimalString(DecimalString);
6761
7052
  *
6762
- * expectOk(Positive.fromUnknown("0.5"), "0.5");
7053
+ * assertOk(Positive.fromUnknown("0.5"), "0.5");
6763
7054
  * ```
6764
7055
  *
6765
7056
  * @group Number
@@ -6811,11 +7102,15 @@ export type PositiveDecimalString = typeof PositiveDecimalString.Output;
6811
7102
  * ### Example
6812
7103
  *
6813
7104
  * ```ts
6814
- * import { DecimalString, nonPositiveDecimalString } from "@evolu/common";
7105
+ * import {
7106
+ * assertOk,
7107
+ * DecimalString,
7108
+ * nonPositiveDecimalString,
7109
+ * } from "@evolu/common";
6815
7110
  *
6816
7111
  * const NonPositive = nonPositiveDecimalString(DecimalString);
6817
7112
  *
6818
- * expectOk(NonPositive.fromUnknown("-0.5"), "-0.5");
7113
+ * assertOk(NonPositive.fromUnknown("-0.5"), "-0.5");
6819
7114
  * ```
6820
7115
  *
6821
7116
  * @group Number
@@ -6863,11 +7158,15 @@ export type NonPositiveDecimalString = typeof NonPositiveDecimalString.Output;
6863
7158
  * ### Example
6864
7159
  *
6865
7160
  * ```ts
6866
- * import { DecimalString, negativeDecimalString } from "@evolu/common";
7161
+ * import {
7162
+ * assertOk,
7163
+ * DecimalString,
7164
+ * negativeDecimalString,
7165
+ * } from "@evolu/common";
6867
7166
  *
6868
7167
  * const Negative = negativeDecimalString(DecimalString);
6869
7168
  *
6870
- * expectOk(Negative.fromUnknown("-0.5"), "-0.5");
7169
+ * assertOk(Negative.fromUnknown("-0.5"), "-0.5");
6871
7170
  * ```
6872
7171
  *
6873
7172
  * @group Number
@@ -6927,16 +7226,29 @@ export type NegativeDecimalString = typeof NegativeDecimalString.Output;
6927
7226
  * ### Example
6928
7227
  *
6929
7228
  * ```ts
6930
- * 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";
6931
7239
  *
6932
7240
  * const Tenths = multipleOf("0.1")(FiniteNumber);
6933
7241
  *
6934
- * expectTypeOf<typeof Tenths.Output>().toEqualTypeOf<
6935
- * FiniteNumber & Brand<"MultipleOf0.1">
7242
+ * assertType<
7243
+ * FiniteNumber & Brand<"MultipleOf0.1">,
7244
+ * typeof Tenths.Output
6936
7245
  * >();
6937
7246
  *
6938
- * expectOk(Tenths.fromUnknown(0.3), 0.3);
6939
- * 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, {
6940
7252
  * type: "MultipleOf0.1",
6941
7253
  * value: 0.31,
6942
7254
  * divisor: "0.1",
@@ -7073,11 +7385,11 @@ const decimalStringToParts = (value: string): DecimalParts => {
7073
7385
  * ### Example
7074
7386
  *
7075
7387
  * ```ts
7076
- * import { Number, between } from "@evolu/common";
7388
+ * import { assertOk, Number, between } from "@evolu/common";
7077
7389
  *
7078
7390
  * const Percentage = between(0, 100)(Number);
7079
7391
  *
7080
- * expectOk(Percentage.fromUnknown(75), 75);
7392
+ * assertOk(Percentage.fromUnknown(75), 75);
7081
7393
  * ```
7082
7394
  *
7083
7395
  * @group Number
@@ -7147,6 +7459,11 @@ export interface BetweenError<
7147
7459
  *
7148
7460
  * ```ts
7149
7461
  * import {
7462
+ * assertEqual,
7463
+ * assertErr,
7464
+ * assertOk,
7465
+ * assertType,
7466
+ * Data,
7150
7467
  * String,
7151
7468
  * array,
7152
7469
  * brand,
@@ -7158,12 +7475,16 @@ export interface BetweenError<
7158
7475
  * const UserIds = array(UserId);
7159
7476
  * const result = UserIds.from.parent(["ada", "grace"]);
7160
7477
  *
7161
- * expectTypeOf(result).toEqualTypeOf<
7162
- * Result<ReadonlyArray<string & Brand<"UserId">>, never>
7478
+ * assertType<
7479
+ * Result<ReadonlyArray<string & Brand<"UserId">>>,
7480
+ * typeof result
7163
7481
  * >();
7164
- * expectOk(result, ["ada", "grace"]);
7165
- * expectOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
7166
- * 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, {
7167
7488
  * type: "Array",
7168
7489
  * reason: { kind: "NotArray", value: "ada" },
7169
7490
  * });
@@ -7579,20 +7900,20 @@ const copyArrayPrefix = (
7579
7900
  /**
7580
7901
  * Set {@link Type} whose every element must match one Type.
7581
7902
  *
7582
- * Direct Sets from this or another realm are accepted. Set subclasses are
7583
- * rejected. A Set must have no own properties; its elements are validated in
7584
- * iteration order. Classification uses the realm-neutral object tag and
7585
- * 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.
7586
7907
  *
7587
7908
  * ### Example
7588
7909
  *
7589
7910
  * ```ts
7590
- * import { String, set } from "@evolu/common";
7911
+ * import { assertOk, String, set } from "@evolu/common";
7591
7912
  *
7592
7913
  * const Tags = set(String);
7593
7914
  * const tags = new Set(["local-first", "offline"]);
7594
7915
  *
7595
- * expectOk(Tags.fromUnknown(tags), tags);
7916
+ * assertOk(Tags.fromUnknown(tags), tags);
7596
7917
  * ```
7597
7918
  *
7598
7919
  * @group Collection
@@ -7681,15 +8002,12 @@ type SetNodeError<ElementType extends TypeNode> = [
7681
8002
  : SetError<ElementType["Error"]>;
7682
8003
 
7683
8004
  /**
7684
- * Error returned by {@link set} for a non-Set value, an invalid Set prototype,
7685
- * or invalid Set items.
8005
+ * Error returned by {@link set} for a non-Set value or invalid Set items.
7686
8006
  *
7687
8007
  * @group Collection
7688
8008
  */
7689
8009
  export type SetError<Error extends TypeError = TypeError> =
7690
- | SetNotSetError
7691
- | SetUnexpectedPrototypeError
7692
- | SetItemsErrorValue<Error, true>;
8010
+ SetNotSetError | SetItemsErrorValue<Error, true>;
7693
8011
 
7694
8012
  /**
7695
8013
  * Error returned when a {@link set} input is not a Set.
@@ -7703,18 +8021,6 @@ export interface SetNotSetError extends TypeError<"Set"> {
7703
8021
  };
7704
8022
  }
7705
8023
 
7706
- /**
7707
- * An error returned when a {@link set} input is a Set subclass.
7708
- *
7709
- * @group Collection
7710
- */
7711
- export interface SetUnexpectedPrototypeError extends TypeError<"Set"> {
7712
- readonly reason: {
7713
- readonly kind: "UnexpectedPrototype";
7714
- readonly value: ReadonlySet<unknown>;
7715
- };
7716
- }
7717
-
7718
8024
  /**
7719
8025
  * An own property found on a Set value.
7720
8026
  *
@@ -7868,91 +8174,54 @@ const createHomogeneousCollectionType = <Collection>(
7868
8174
 
7869
8175
  const setTypeByElement = /*#__PURE__*/ new WeakMap<TypeNode, TypeNode>();
7870
8176
 
7871
- const validateSetCollection = (
7872
- value: unknown,
7873
- validateElement: RuntimeOutputValidation,
7874
- options: ValidationOptions,
7875
- ): Result<ReadonlySet<unknown>, SetError> => {
7876
- if (!hasObjectTag(value, "Set")) {
7877
- return err({
7878
- type: "Set",
7879
- reason: { kind: "NotSet", value },
7880
- });
7881
- }
7882
- if (!hasDirectSetPrototype(value as object)) {
7883
- return err({
7884
- type: "Set",
7885
- reason: {
7886
- kind: "UnexpectedPrototype",
7887
- value: value as ReadonlySet<unknown>,
7888
- },
7889
- });
7890
- }
7891
-
7892
- return validateSetItems(
7893
- value as ReadonlySet<unknown>,
7894
- validateElement,
7895
- options,
7896
- true,
7897
- );
7898
- };
7899
-
7900
- const encodeSetCollection = (
7901
- value: ReadonlySet<unknown>,
7902
- encodeElement: RuntimeEncoder,
7903
- ): ReadonlySet<unknown> => {
7904
- let changed = false;
7905
- const output = new globalThis.Set<unknown>();
7906
-
7907
- for (const item of value) {
7908
- const encoded = encodeElement(item as never);
7909
- if (!globalThis.Object.is(encoded, item)) changed = true;
7910
- output.add(encoded);
7911
- }
7912
-
7913
- return changed ? output : value;
7914
- };
7915
-
7916
- const isSetCollection = (
7917
- value: unknown,
7918
- isElement: (value: unknown) => boolean,
7919
- ): boolean => {
7920
- if (!hasObjectTag(value, "Set")) return false;
7921
- if (!hasDirectSetPrototype(value as object)) return false;
7922
- if (Reflect.ownKeys(value as object).length !== 0) return false;
7923
- for (const item of value as ReadonlySet<unknown>) {
7924
- if (!isElement(item)) return false;
7925
- }
7926
- return true;
7927
- };
7928
-
7929
- const hasDirectSetPrototype = (value: object): boolean => {
7930
- const prototype: unknown = globalThis.Object.getPrototypeOf(value);
7931
- if (prototype === null) return false;
7932
-
7933
- const objectPrototype: unknown = globalThis.Object.getPrototypeOf(prototype);
7934
-
7935
- return (
7936
- objectPrototype !== null &&
7937
- globalThis.Object.getPrototypeOf(objectPrototype) === null
7938
- );
7939
- };
7940
-
7941
8177
  const setRuntimeConfig: HomogeneousCollectionRuntimeConfig<
7942
8178
  ReadonlySet<unknown>
7943
8179
  > = {
7944
8180
  name: "Set",
7945
8181
  typeByElement: setTypeByElement,
7946
- 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
+ },
7947
8196
  validateItems: (value, validateElement, options) =>
7948
8197
  validateSetItems(value, validateElement, options, false),
7949
- encode: encodeSetCollection,
7950
- 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
+ },
7951
8222
  formatError: ((error: SetError) => {
7952
8223
  if (error.reason.kind === "NotSet")
7953
8224
  return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Set.`;
7954
- if (error.reason.kind === "UnexpectedPrototype")
7955
- return "The value is an instance of a Set subclass, but a Set Output must be a direct Set instance.";
7956
8225
  const issue = error.reason.issues[0];
7957
8226
  switch (issue.kind) {
7958
8227
  case "ExcessProperty":
@@ -7972,7 +8241,7 @@ const validateSetItems = (
7972
8241
  let issues:
7973
8242
  Array<SetStructuralIssue | SetElementIssue<TypeError>> | undefined;
7974
8243
  let changed = false;
7975
- const output = new globalThis.Set<unknown>();
8244
+ const output = new Set<unknown>();
7976
8245
 
7977
8246
  if (checkStructure) {
7978
8247
  for (const key of Reflect.ownKeys(value)) {
@@ -8009,70 +8278,590 @@ const validateSetItems = (
8009
8278
  };
8010
8279
 
8011
8280
  /**
8012
- * Tuple {@link Type}.
8013
- *
8014
- * Use `tuple(First, Second, ...)` for a fixed-length readonly array in which
8015
- * every position has its own Type.
8016
- *
8017
- * `fromUnknown` validates the Tuple representation and runs every element's
8018
- * complete Type pipeline. By default, it returns the first issue. Pass `{
8019
- * errors: "all" }` to collect issues across the whole Tuple.
8281
+ * Map {@link Type} whose keys and values must match their respective Types.
8020
8282
  *
8021
- * `from` accepts the Tuple Output. When any element Type has a parent,
8022
- * `from.parent` accepts a Tuple of root element Outputs and runs all remaining
8023
- * element stages. This collapsed input boundary keeps a Tuple to at most one
8024
- * `.parent` suffix even when its element Types have different pipeline depths.
8025
- *
8026
- * A Tuple must be recognized by `Array.isArray`, have exactly the declared
8027
- * length, be dense, and have no own properties other than `length` and the
8028
- * indexed data properties for its elements. Array subclasses, custom-prototype
8029
- * arrays, and foreign-realm arrays are accepted when their own data
8030
- * representation is valid. Sparse arrays, accessor elements, and excess
8031
- * properties are rejected.
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.
8032
8288
  *
8033
8289
  * ### Example
8034
8290
  *
8035
8291
  * ```ts
8036
- * import { Int64FromInt64String, String, tuple } from "@evolu/common";
8292
+ * import { assertOk, PositiveInt, String, map } from "@evolu/common";
8037
8293
  *
8038
- * const Entry = tuple(String, Int64FromInt64String);
8294
+ * const Scores = map(String, PositiveInt);
8295
+ * const scores = new Map([
8296
+ * ["Ada", 10],
8297
+ * ["Grace", 20],
8298
+ * ]);
8039
8299
  *
8040
- * expectOk(Entry.fromUnknown(["count", "1"]), ["count", 1n]);
8041
- * expectOk(Entry.from.parent(["count", "1"]), ["count", 1n]);
8300
+ * assertOk(Scores.fromUnknown(scores), scores);
8042
8301
  * ```
8043
8302
  *
8044
8303
  * @group Collection
8045
8304
  */
8046
- export function tuple<const Elements extends TupleElements>(
8047
- ...elements: Elements & TupleValidation<Elements>
8048
- ): TupleType<Elements>;
8049
- export function tuple(...elements: TupleElements): TypeNode {
8050
- return createTupleType(elements as NonEmptyReadonlyArray<RuntimeTypeNode>);
8051
- }
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>;
8052
8318
 
8053
- const createTupleType = (
8054
- typeElements: NonEmptyReadonlyArray<RuntimeTypeNode>,
8055
- ): TypeNode => {
8056
- const expectedLength = typeElements.length;
8057
8319
  const validate = (
8058
- value: unknown,
8059
- validateElement: (
8060
- element: RuntimeTypeNode,
8061
- value: unknown,
8062
- options: ValidationOptions,
8063
- ) => Result<unknown, TypeError>,
8320
+ input: unknown,
8321
+ validateKey: RuntimeOutputValidation,
8322
+ validateValue: RuntimeOutputValidation,
8064
8323
  options: ValidationOptions,
8065
- ): Result<ReadonlyArray<unknown>, TupleError> => {
8066
- if (!Array.isArray(value)) {
8324
+ ): Result<ReadonlyMap<unknown, unknown>, MapError> => {
8325
+ if (!hasObjectTag(input, "Map")) {
8067
8326
  return err({
8068
- type: "Tuple",
8069
- reason: { kind: "NotArray", value },
8327
+ type: "Map",
8328
+ reason: { kind: "NotMap", value: input },
8070
8329
  });
8071
8330
  }
8072
- if (value.length !== expectedLength) {
8073
- return err({
8074
- type: "Tuple",
8075
- reason: {
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.
8805
+ *
8806
+ * `from` accepts the Tuple Output. When any element Type has a parent,
8807
+ * `from.parent` accepts a Tuple of root element Outputs and runs all remaining
8808
+ * element stages. This collapsed input boundary keeps a Tuple to at most one
8809
+ * `.parent` suffix even when its element Types have different pipeline depths.
8810
+ *
8811
+ * A Tuple must be recognized by `Array.isArray`, have exactly the declared
8812
+ * length, be dense, and have no own properties other than `length` and the
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.
8816
+ *
8817
+ * ### Example
8818
+ *
8819
+ * ```ts
8820
+ * import {
8821
+ * assertOk,
8822
+ * Int64FromInt64String,
8823
+ * String,
8824
+ * tuple,
8825
+ * } from "@evolu/common";
8826
+ *
8827
+ * const Entry = tuple(String, Int64FromInt64String);
8828
+ *
8829
+ * assertOk(Entry.fromUnknown(["count", "1"]), ["count", 1n]);
8830
+ * assertOk(Entry.from.parent(["count", "1"]), ["count", 1n]);
8831
+ * ```
8832
+ *
8833
+ * @group Collection
8834
+ */
8835
+ export function tuple<const Elements extends TupleElements>(
8836
+ ...elements: Elements & TupleValidation<Elements>
8837
+ ): TupleType<Elements>;
8838
+ export function tuple(...elements: TupleElements): TypeNode {
8839
+ return createTupleType(elements as NonEmptyReadonlyArray<RuntimeTypeNode>);
8840
+ }
8841
+
8842
+ const createTupleType = (
8843
+ typeElements: NonEmptyReadonlyArray<RuntimeTypeNode>,
8844
+ ): TypeNode => {
8845
+ const expectedLength = typeElements.length;
8846
+ const validate = (
8847
+ value: unknown,
8848
+ validateElement: (
8849
+ element: RuntimeTypeNode,
8850
+ value: unknown,
8851
+ options: ValidationOptions,
8852
+ ) => Result<unknown, TypeError>,
8853
+ options: ValidationOptions,
8854
+ ): Result<ReadonlyArray<unknown>, TupleError> => {
8855
+ if (!Array.isArray(value)) {
8856
+ return err({
8857
+ type: "Tuple",
8858
+ reason: { kind: "NotArray", value },
8859
+ });
8860
+ }
8861
+ if (value.length !== expectedLength) {
8862
+ return err({
8863
+ type: "Tuple",
8864
+ reason: {
8076
8865
  kind: "InvalidLength",
8077
8866
  expected: expectedLength,
8078
8867
  actual: value.length,
@@ -8625,11 +9414,12 @@ type PlainObjectError = ObjectError<
8625
9414
  * A {@link Type} for readonly plain objects with unknown property values.
8626
9415
  *
8627
9416
  * `Object` is the runtime counterpart of a `Readonly<Record<string, unknown>>`
8628
- * data boundary. Its realm-neutral prototype rule accepts a `null` prototype or
8629
- * an immediate root prototype whose own prototype is `null`. Every own property
8630
- * must have a string key and be an enumerable data property. It rejects
8631
- * ordinary class instances, deeper prototype chains, accessors, non-enumerable
8632
- * 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.
8633
9423
  *
8634
9424
  * Use {@link object} when property names are fixed, {@link record} when keys and
8635
9425
  * values have their own Types, and {@link instanceOf} when an instance belongs
@@ -8704,7 +9494,7 @@ const _Object: Type<
8704
9494
  }
8705
9495
 
8706
9496
  return errors === undefined
8707
- ? ok(value as Readonly<Record<string, unknown>>)
9497
+ ? ok(value)
8708
9498
  : err({
8709
9499
  type: "Object",
8710
9500
  reason: { kind: "Properties", errors },
@@ -8765,13 +9555,6 @@ const _Object: Type<
8765
9555
  // https://github.com/expo/expo/issues/31167
8766
9556
  export { _Object as Object };
8767
9557
 
8768
- const isPlainObject = (value: object): boolean => {
8769
- const prototype: unknown = globalThis.Object.getPrototypeOf(value);
8770
- return (
8771
- prototype === null || globalThis.Object.getPrototypeOf(prototype) === null
8772
- );
8773
- };
8774
-
8775
9558
  /**
8776
9559
  * Record {@link Type}.
8777
9560
  *
@@ -8788,12 +9571,13 @@ const isPlainObject = (value: object): boolean => {
8788
9571
  * By default, validation returns the first issue. Pass `{ errors: "all" }` to
8789
9572
  * collect issues across the whole Record.
8790
9573
  *
8791
- * A Record must have a `null` prototype or an immediate root prototype whose
8792
- * own prototype is `null`. Ordinary class instances and deeper prototype chains
8793
- * are rejected. Every own property must be an enumerable data property whose
8794
- * key and value satisfy their Types. When decoding or encoding changes an
8795
- * entry, the constructed Record uses a `null` prototype so keys such as
8796
- * `__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.
8797
9581
  *
8798
9582
  * If transformed keys collide, validation fails instead of overwriting an
8799
9583
  * entry.
@@ -8802,6 +9586,8 @@ const isPlainObject = (value: object): boolean => {
8802
9586
  *
8803
9587
  * ```ts
8804
9588
  * import {
9589
+ * assertOk,
9590
+ * assertType,
8805
9591
  * Int64FromInt64String,
8806
9592
  * String,
8807
9593
  * record,
@@ -8817,21 +9603,22 @@ const isPlainObject = (value: object): boolean => {
8817
9603
  * grace: "20",
8818
9604
  * });
8819
9605
  *
8820
- * expectOk(scoresFromUnknown, { ada: 10n, grace: 20n });
9606
+ * assertOk(scoresFromUnknown, { ada: 10n, grace: 20n });
8821
9607
  *
8822
9608
  * // Validate keys and values with their root Types.
8823
9609
  * const scoresInput = ScoresByUser.parent.fromUnknown({
8824
9610
  * ada: "10",
8825
9611
  * grace: "20",
8826
9612
  * });
8827
- * expectOk(scoresInput, { ada: "10", grace: "20" });
9613
+ * assertOk(scoresInput, { ada: "10", grace: "20" });
8828
9614
  *
8829
9615
  * // Run the remaining key and value stages.
8830
9616
  * const scoresFromInput = ScoresByUser.from.parent(scoresInput.value);
8831
9617
  *
8832
- * expectOk(scoresFromInput, { ada: 10n, grace: 20n });
8833
- * expectTypeOf(scoresFromInput.value).toEqualTypeOf<
8834
- * Readonly<Partial<Record<string, Int64>>>
9618
+ * assertOk(scoresFromInput, { ada: 10n, grace: 20n });
9619
+ * assertType<
9620
+ * Readonly<Partial<Record<string, Int64>>>,
9621
+ * typeof scoresFromInput.value
8835
9622
  * >();
8836
9623
  * ```
8837
9624
  *
@@ -8840,34 +9627,64 @@ const isPlainObject = (value: object): boolean => {
8840
9627
  * as a Record entry:
8841
9628
  *
8842
9629
  * ```ts
9630
+ * import {
9631
+ * assertEqual,
9632
+ * assertErr,
9633
+ * assertTrue,
9634
+ * trySync,
9635
+ * } from "@evolu/common";
9636
+ *
8843
9637
  * type Values = Partial<Record<"toString", number>>;
8844
9638
  * const values: Values = {};
8845
9639
  *
8846
9640
  * // TypeScript treats the inherited function as `number | undefined`.
8847
9641
  * const value: number | undefined = values.toString;
9642
+ * const valueType = typeof value;
8848
9643
  *
8849
- * expect(typeof value).toBe("function");
8850
- * expect(() => {
8851
- * if (value !== undefined) value.toFixed();
8852
- * }).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);
8853
9653
  * ```
8854
9654
  *
8855
9655
  * Evolu Record Outputs use the same TypeScript Record representation.
8856
9656
  *
8857
9657
  * ```ts
8858
- * 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";
8859
9669
  *
8860
9670
  * const Values = record(literal("toString"), Number);
8861
9671
  * const result = Values.fromUnknown({});
9672
+ * const emptyValues: Data = {};
8862
9673
  *
8863
- * expectOk(result, {});
9674
+ * assertOk(result, emptyValues);
8864
9675
  *
8865
9676
  * const value: number | undefined = result.value.toString;
9677
+ * const valueType = typeof value;
8866
9678
  *
8867
- * expect(typeof value).toBe("function");
8868
- * expect(() => {
8869
- * if (value !== undefined) value.toFixed();
8870
- * }).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);
8871
9688
  * ```
8872
9689
  *
8873
9690
  * In other words, treat Record Outputs as string-keyed data rather than calling
@@ -8905,7 +9722,7 @@ export const record = <
8905
9722
  }
8906
9723
 
8907
9724
  return validateRecordEntries(
8908
- input as Readonly<Record<string, unknown>>,
9725
+ input,
8909
9726
  validateKey as (
8910
9727
  value: unknown,
8911
9728
  options: ValidationOptions,
@@ -9449,7 +10266,13 @@ export interface OptionalProperty<T extends TypeNode> {
9449
10266
  * ### Example
9450
10267
  *
9451
10268
  * ```ts
9452
- * import { String, object, optional, undefinedOr } from "@evolu/common";
10269
+ * import {
10270
+ * assertOk,
10271
+ * String,
10272
+ * object,
10273
+ * optional,
10274
+ * undefinedOr,
10275
+ * } from "@evolu/common";
9453
10276
  *
9454
10277
  * const User = object({
9455
10278
  * name: String,
@@ -9457,8 +10280,8 @@ export interface OptionalProperty<T extends TypeNode> {
9457
10280
  * preferredName: optional(undefinedOr(String)),
9458
10281
  * });
9459
10282
  *
9460
- * expectOk(User.fromUnknown({ name: "Ada" }), { name: "Ada" });
9461
- * expectOk(User.fromUnknown({ name: "Ada", preferredName: undefined }), {
10283
+ * assertOk(User.fromUnknown({ name: "Ada" }), { name: "Ada" });
10284
+ * assertOk(User.fromUnknown({ name: "Ada", preferredName: undefined }), {
9462
10285
  * name: "Ada",
9463
10286
  * preferredName: undefined,
9464
10287
  * });
@@ -9515,10 +10338,11 @@ type ObjectProperty = ObjectProps[string];
9515
10338
  * required properties nor make optional properties present. This keeps Outputs
9516
10339
  * valid after ordinary object spread restores `Object.prototype`.
9517
10340
  *
9518
- * A `null` prototype or an immediate root prototype whose own prototype is
9519
- * `null` is accepted. Ordinary class instances, deeper prototype chains,
9520
- * accessors, and non-enumerable properties are rejected. Use {@link instanceOf}
9521
- * 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.
9522
10346
  *
9523
10347
  * When decoding changes no property, it preserves the input. When decoding or
9524
10348
  * encoding changes a property, it constructs an object with a `null`
@@ -9531,6 +10355,8 @@ type ObjectProperty = ObjectProps[string];
9531
10355
  *
9532
10356
  * ```ts
9533
10357
  * import {
10358
+ * assertOk,
10359
+ * assertType,
9534
10360
  * Int64FromInt64String,
9535
10361
  * String,
9536
10362
  * object,
@@ -9549,20 +10375,20 @@ type ObjectProperty = ObjectProps[string];
9549
10375
  * loginCount: "42",
9550
10376
  * });
9551
10377
  *
9552
- * expectOk(userFromUnknown, { name: "Ada", loginCount: 42n });
10378
+ * assertOk(userFromUnknown, { name: "Ada", loginCount: 42n });
9553
10379
  *
9554
10380
  * // Validate the object and root property Types.
9555
10381
  * const userInput = User.parent.fromUnknown({
9556
10382
  * name: "Ada",
9557
10383
  * loginCount: "42",
9558
10384
  * });
9559
- * expectOk(userInput, { name: "Ada", loginCount: "42" });
10385
+ * assertOk(userInput, { name: "Ada", loginCount: "42" });
9560
10386
  *
9561
10387
  * // Run the remaining property stages.
9562
10388
  * const userFromInput = User.from.parent(userInput.value);
9563
10389
  *
9564
- * expectOk(userFromInput, { name: "Ada", loginCount: 42n });
9565
- * expectTypeOf(userFromInput.value).toExtend<User>();
10390
+ * assertOk(userFromInput, { name: "Ada", loginCount: 42n });
10391
+ * assertType<typeof User.Output, typeof userFromInput.value>();
9566
10392
  * ```
9567
10393
  *
9568
10394
  * Note that TypeScript does not model an object's runtime prototype. This can
@@ -9570,38 +10396,68 @@ type ObjectProperty = ObjectProps[string];
9570
10396
  * as an object property:
9571
10397
  *
9572
10398
  * ```ts
10399
+ * import {
10400
+ * assertEqual,
10401
+ * assertErr,
10402
+ * assertTrue,
10403
+ * trySync,
10404
+ * } from "@evolu/common";
10405
+ *
9573
10406
  * interface Values {
9574
10407
  * readonly toString?: number;
9575
10408
  * }
9576
10409
  *
9577
- * const nullPrototypeValues = globalThis.Object.create(null) as Values;
10410
+ * const nullPrototypeValues = Object.create(null) as Values;
9578
10411
  * const values = { ...nullPrototypeValues };
9579
10412
  *
9580
10413
  * // TypeScript treats the inherited function as `number | undefined`.
9581
10414
  * const value: number | undefined = values.toString;
10415
+ * const valueType = typeof value;
9582
10416
  *
9583
- * expect(typeof value).toBe("function");
9584
- * expect(() => {
9585
- * if (value !== undefined) value.toFixed();
9586
- * }).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);
9587
10426
  * ```
9588
10427
  *
9589
10428
  * Evolu Object Outputs use the same TypeScript object representation.
9590
10429
  *
9591
10430
  * ```ts
9592
- * 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";
9593
10442
  *
9594
10443
  * const Values = object({ toString: optional(Number) });
9595
10444
  * const result = Values.fromUnknown({});
10445
+ * const emptyValues: Data = {};
9596
10446
  *
9597
- * expectOk(result, {});
10447
+ * assertOk(result, emptyValues);
9598
10448
  *
9599
10449
  * const value: number | undefined = result.value.toString;
10450
+ * const valueType = typeof value;
9600
10451
  *
9601
- * expect(typeof value).toBe("function");
9602
- * expect(() => {
9603
- * if (value !== undefined) value.toFixed();
9604
- * }).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);
9605
10461
  * ```
9606
10462
  *
9607
10463
  * In other words, treat Object Outputs as data rather than calling inherited
@@ -9622,7 +10478,13 @@ export function object<const Props extends ObjectProps>(
9622
10478
  * ### Example
9623
10479
  *
9624
10480
  * ```ts
9625
- * import { String, object, record } from "@evolu/common";
10481
+ * import {
10482
+ * assertOk,
10483
+ * assertType,
10484
+ * String,
10485
+ * object,
10486
+ * record,
10487
+ * } from "@evolu/common";
9626
10488
  *
9627
10489
  * const RequestHeaders = object(
9628
10490
  * { authorization: String },
@@ -9633,13 +10495,11 @@ export function object<const Props extends ObjectProps>(
9633
10495
  * "x-request-id": "request-1",
9634
10496
  * });
9635
10497
  *
9636
- * expectOk(result, {
10498
+ * assertOk(result, {
9637
10499
  * authorization: "Bearer token",
9638
10500
  * "x-request-id": "request-1",
9639
10501
  * });
9640
- * expectTypeOf(result.value["x-request-id"]).toEqualTypeOf<
9641
- * string | undefined
9642
- * >();
10502
+ * assertType<string | undefined, (typeof result.value)["x-request-id"]>();
9643
10503
  * ```
9644
10504
  */
9645
10505
  export function object<
@@ -10480,12 +11340,10 @@ export interface ObjectNotObjectError extends TypeError<"Object"> {
10480
11340
  * An error returned when an {@link object} input falls outside its supported
10481
11341
  * plain-object prototype boundary.
10482
11342
  *
10483
- * Object Types accept a `null` prototype or a prototype whose own prototype is
10484
- * `null`. This includes ordinary and cross-realm plain objects as well as
10485
- * objects created from an immediate root prototype. Arrays, JavaScript built-in
10486
- * objects, class instances, and objects with deeper custom prototype chains
10487
- * return this error instead of having their prototype or inherited state
10488
- * 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.
10489
11347
  *
10490
11348
  * @group Objects
10491
11349
  */
@@ -10645,13 +11503,20 @@ const createRecordPropertyError = <Error extends TypeError>(
10645
11503
  * ### Example
10646
11504
  *
10647
11505
  * ```ts
10648
- * import { PositiveInt, String, partial } from "@evolu/common";
11506
+ * import {
11507
+ * assertFalse,
11508
+ * assertOk,
11509
+ * PositiveInt,
11510
+ * String,
11511
+ * partial,
11512
+ * } from "@evolu/common";
10649
11513
  *
10650
11514
  * const PartialUser = partial({ name: String, age: PositiveInt });
11515
+ * const emptyPartialUser: typeof PartialUser.Output = {};
10651
11516
  *
10652
- * expectOk(PartialUser.fromUnknown({}), {});
10653
- * expectOk(PartialUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
10654
- * 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);
10655
11520
  * ```
10656
11521
  *
10657
11522
  * @group Objects
@@ -10701,15 +11566,20 @@ export type PartialObjectProps<Props extends ObjectProps> = {
10701
11566
  * ### Example
10702
11567
  *
10703
11568
  * ```ts
10704
- * import { String, nullOr, nullableToOptional } from "@evolu/common";
11569
+ * import {
11570
+ * assertOk,
11571
+ * String,
11572
+ * nullOr,
11573
+ * nullableToOptional,
11574
+ * } from "@evolu/common";
10705
11575
  *
10706
11576
  * const User = nullableToOptional({
10707
11577
  * name: String,
10708
11578
  * nickname: nullOr(String),
10709
11579
  * });
10710
11580
  *
10711
- * expectOk(User.fromUnknown({ name: "Ada" }), { name: "Ada" });
10712
- * expectOk(User.fromUnknown({ name: "Ada", nickname: null }), {
11581
+ * assertOk(User.fromUnknown({ name: "Ada" }), { name: "Ada" });
11582
+ * assertOk(User.fromUnknown({ name: "Ada", nickname: null }), {
10713
11583
  * name: "Ada",
10714
11584
  * nickname: null,
10715
11585
  * });
@@ -10773,12 +11643,12 @@ export type NullableToOptionalProps<Props extends ObjectProps> = {
10773
11643
  * ### Example
10774
11644
  *
10775
11645
  * ```ts
10776
- * import { String, object, omit } from "@evolu/common";
11646
+ * import { assertOk, String, object, omit } from "@evolu/common";
10777
11647
  *
10778
11648
  * const User = object({ name: String, password: String });
10779
11649
  * const PublicUser = omit(User, "password");
10780
11650
  *
10781
- * expectOk(PublicUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
11651
+ * assertOk(PublicUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
10782
11652
  * ```
10783
11653
  *
10784
11654
  * @group Objects
@@ -10844,6 +11714,7 @@ type OmitKeyConcreteTypeError = CompileTimeError<
10844
11714
  *
10845
11715
  * ```ts
10846
11716
  * import {
11717
+ * assertOk,
10847
11718
  * NonNegativeInt,
10848
11719
  * String,
10849
11720
  * object,
@@ -10861,8 +11732,8 @@ type OmitKeyConcreteTypeError = CompileTimeError<
10861
11732
  * value: { timestamp: 42 },
10862
11733
  * });
10863
11734
  *
10864
- * expectOk(validated, { ok: true, value: { timestamp: 42 } });
10865
- * expectOk(validated.value, { timestamp: 42 });
11735
+ * assertOk(validated, { ok: true, value: { timestamp: 42 } });
11736
+ * assertOk(validated.value, { timestamp: 42 });
10866
11737
  * ```
10867
11738
  *
10868
11739
  * @group Results
@@ -10929,21 +11800,31 @@ export type UnknownResult = typeof UnknownResult.Output;
10929
11800
  * ### Example
10930
11801
  *
10931
11802
  * ```ts
10932
- * import { String, discriminatedUnion, typed } from "@evolu/common";
11803
+ * import {
11804
+ * assertFalse,
11805
+ * assertOk,
11806
+ * assertType,
11807
+ * String,
11808
+ * discriminatedUnion,
11809
+ * typed,
11810
+ * } from "@evolu/common";
10933
11811
  *
10934
11812
  * const Loading = typed("Loading");
10935
11813
  * const Loaded = typed("Loaded", { value: String });
10936
11814
  * const State = discriminatedUnion(Loading, Loaded);
10937
11815
  *
10938
- * expectOk(State.fromUnknown({ type: "Loading" }), { type: "Loading" });
10939
- * expectOk(State.fromUnknown({ type: "Loaded", value: "Evolu" }), {
11816
+ * assertOk(State.fromUnknown({ type: "Loading" }), { type: "Loading" });
11817
+ * assertOk(State.fromUnknown({ type: "Loaded", value: "Evolu" }), {
10940
11818
  * type: "Loaded",
10941
11819
  * value: "Evolu",
10942
11820
  * });
10943
- * expect(Loading.is({ type: "Loading", progress: 1 })).toBe(false);
10944
- * expectTypeOf<typeof Loading.Output>().toExtend<{
10945
- * readonly type: "Loading";
10946
- * }>();
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
+ * >();
10947
11828
  * ```
10948
11829
  *
10949
11830
  * @group Discriminated unions
@@ -10958,13 +11839,13 @@ export function typed<const Tag extends TypeName>(
10958
11839
  * ### Example
10959
11840
  *
10960
11841
  * ```ts
10961
- * import { String, typed } from "@evolu/common";
11842
+ * import { assertOk, String, typed } from "@evolu/common";
10962
11843
  *
10963
11844
  * const Pending = typed("Pending", {
10964
11845
  * label: String,
10965
11846
  * });
10966
11847
  *
10967
- * expectOk(Pending.fromUnknown({ type: "Pending", label: "Waiting" }), {
11848
+ * assertOk(Pending.fromUnknown({ type: "Pending", label: "Waiting" }), {
10968
11849
  * type: "Pending",
10969
11850
  * label: "Waiting",
10970
11851
  * });
@@ -10987,7 +11868,13 @@ export function typed<
10987
11868
  * ### Example
10988
11869
  *
10989
11870
  * ```ts
10990
- * import { String, record, typed } from "@evolu/common";
11871
+ * import {
11872
+ * assertOk,
11873
+ * assertType,
11874
+ * String,
11875
+ * record,
11876
+ * typed,
11877
+ * } from "@evolu/common";
10991
11878
  *
10992
11879
  * const Open = typed("Open", { label: String }, record(String, String));
10993
11880
  * const result = Open.fromUnknown({
@@ -10996,12 +11883,12 @@ export function typed<
10996
11883
  * note: "Connected",
10997
11884
  * });
10998
11885
  *
10999
- * expectOk(result, {
11886
+ * assertOk(result, {
11000
11887
  * type: "Open",
11001
11888
  * label: "Ready",
11002
11889
  * note: "Connected",
11003
11890
  * });
11004
- * expectTypeOf(result.value.note).toEqualTypeOf<string | undefined>();
11891
+ * assertType<string | undefined, typeof result.value.note>();
11005
11892
  * ```
11006
11893
  */
11007
11894
  export function typed<
@@ -11061,7 +11948,14 @@ export function typed(
11061
11948
  * ### Example
11062
11949
  *
11063
11950
  * ```ts
11064
- * 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";
11065
11959
  *
11066
11960
  * interface User extends Typed<"User"> {
11067
11961
  * readonly id: string;
@@ -11076,8 +11970,8 @@ export function typed(
11076
11970
  * readonly id: string;
11077
11971
  * }
11078
11972
  *
11079
- * expectOk(getUser("user-1"), { type: "User", id: "user-1" });
11080
- * 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" });
11081
11975
  * ```
11082
11976
  *
11083
11977
  * @group Discriminated unions
@@ -11097,6 +11991,7 @@ export interface Typed<Tag extends TypeName> {
11097
11991
  *
11098
11992
  * ```ts
11099
11993
  * import {
11994
+ * assertType,
11100
11995
  * String,
11101
11996
  * discriminatedUnion,
11102
11997
  * typed,
@@ -11110,10 +12005,10 @@ export interface Typed<Tag extends TypeName> {
11110
12005
  *
11111
12006
  * type CreateMessage = ExtractTyped<Message, "Create">;
11112
12007
  *
11113
- * expectTypeOf<CreateMessage>().toEqualTypeOf<typeof Create.Output>();
12008
+ * assertType<typeof Create.Output, CreateMessage>();
11114
12009
  *
11115
12010
  * // @ts-expect-error "Cretae" is not a Message type.
11116
- * type Typo = ExtractTyped<Message, "Cretae">;
12011
+ * type _Typo = ExtractTyped<Message, "Cretae">;
11117
12012
  * ```
11118
12013
  *
11119
12014
  * @group Discriminated unions
@@ -11171,7 +12066,7 @@ type TypedTypePropertyError = CompileTimeError<
11171
12066
  * ### Example
11172
12067
  *
11173
12068
  * ```ts
11174
- * import { String, nextResult, typed } from "@evolu/common";
12069
+ * import { assertEqual, String, nextResult, typed } from "@evolu/common";
11175
12070
  *
11176
12071
  * const StringNextResult = nextResult(
11177
12072
  * String,
@@ -11189,19 +12084,21 @@ type TypedTypePropertyError = CompileTimeError<
11189
12084
  * return `Error: ${result.error.message}`;
11190
12085
  * };
11191
12086
  *
11192
- * expect(describeNext({ ok: true, value: "item" })).toBe("Value: item");
11193
- * expect(
12087
+ * assertEqual(describeNext({ ok: true, value: "item" }), "Value: item");
12088
+ * assertEqual(
11194
12089
  * describeNext({
11195
12090
  * ok: false,
11196
12091
  * error: { type: "Done", done: "complete" },
11197
12092
  * }),
11198
- * ).toBe("Done: complete");
11199
- * expect(
12093
+ * "Done: complete",
12094
+ * );
12095
+ * assertEqual(
11200
12096
  * describeNext({
11201
12097
  * ok: false,
11202
12098
  * error: { type: "ReadFailed", message: "Offline" },
11203
12099
  * }),
11204
- * ).toBe("Error: Offline");
12100
+ * "Error: Offline",
12101
+ * );
11205
12102
  * ```
11206
12103
  *
11207
12104
  * @group Results
@@ -11277,13 +12174,18 @@ export type UnknownNextResult = typeof UnknownNextResult.Output;
11277
12174
  * ### Example
11278
12175
  *
11279
12176
  * ```ts
11280
- * import { String, discriminatedUnion, typed } from "@evolu/common";
12177
+ * import {
12178
+ * assertOk,
12179
+ * String,
12180
+ * discriminatedUnion,
12181
+ * typed,
12182
+ * } from "@evolu/common";
11281
12183
  *
11282
12184
  * const Created = typed("Created", { id: String });
11283
12185
  * const Deleted = typed("Deleted", { id: String });
11284
12186
  * const Event = discriminatedUnion(Created, Deleted);
11285
12187
  *
11286
- * expectOk(Event.fromUnknown({ type: "Created", id: "id" }), {
12188
+ * assertOk(Event.fromUnknown({ type: "Created", id: "id" }), {
11287
12189
  * type: "Created",
11288
12190
  * id: "id",
11289
12191
  * });
@@ -11304,6 +12206,7 @@ export function discriminatedUnion<
11304
12206
  *
11305
12207
  * ```ts
11306
12208
  * import {
12209
+ * assertOk,
11307
12210
  * Number,
11308
12211
  * String,
11309
12212
  * discriminatedUnion,
@@ -11315,11 +12218,11 @@ export function discriminatedUnion<
11315
12218
  * const Removed = object({ kind: literal("removed"), id: Number });
11316
12219
  * const Event = discriminatedUnion("kind", Added, Removed);
11317
12220
  *
11318
- * expectOk(Event.fromUnknown({ kind: "added", value: "Evolu" }), {
12221
+ * assertOk(Event.fromUnknown({ kind: "added", value: "Evolu" }), {
11319
12222
  * kind: "added",
11320
12223
  * value: "Evolu",
11321
12224
  * });
11322
- * expectOk(Event.fromUnknown({ kind: "removed", id: 1 }), {
12225
+ * assertOk(Event.fromUnknown({ kind: "removed", id: 1 }), {
11323
12226
  * kind: "removed",
11324
12227
  * id: 1,
11325
12228
  * });
@@ -11366,7 +12269,7 @@ export function discriminatedUnion(
11366
12269
  type: "Object",
11367
12270
  reason: { kind: "UnexpectedPrototype", value },
11368
12271
  })
11369
- : ok(value as Readonly<Record<string, unknown>>);
12272
+ : ok(value);
11370
12273
 
11371
12274
  if (!objectResult.ok) {
11372
12275
  return err({
@@ -11920,15 +12823,18 @@ type RuntimeDiscriminatedUnionMember = RuntimeObjectTypeNode & {
11920
12823
  *
11921
12824
  * The definition must return one concrete non-Lazy Type. Use {@link union} for
11922
12825
  * alternatives. Every recursive Lazy reference must be nested behind an
11923
- * {@link object}, {@link array}, {@link tuple}, {@link record}, or {@link set}
11924
- * structural boundary. Union does not guard recursion because it passes the
11925
- * same value to every member. Lazy defers schema construction; it does not make
11926
- * 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.
11927
12831
  *
11928
12832
  * ### Example
11929
12833
  *
11930
12834
  * ```ts
11931
12835
  * import {
12836
+ * assertOk,
12837
+ * assertType,
11932
12838
  * String,
11933
12839
  * array,
11934
12840
  * lazy,
@@ -11958,11 +12864,11 @@ type RuntimeDiscriminatedUnionMember = RuntimeObjectTypeNode & {
11958
12864
  * children: [{ value: "leaf", children: [] }],
11959
12865
  * });
11960
12866
  *
11961
- * expectOk(result, {
12867
+ * assertOk(result, {
11962
12868
  * value: "root",
11963
12869
  * children: [{ value: "leaf", children: [] }],
11964
12870
  * });
11965
- * expectTypeOf(result.value).toEqualTypeOf<Tree>();
12871
+ * assertType<Tree, typeof result.value>();
11966
12872
  * ```
11967
12873
  *
11968
12874
  * @group Recursive
@@ -12105,58 +13011,610 @@ export interface LazyType<
12105
13011
  Type<"Lazy", Input, Input, InputError, null, InputError, never, Input>,
12106
13012
  Errors,
12107
13013
  never,
12108
- CanonicalInput,
12109
- UsesIdentityEncoding
12110
- > {
12111
- readonly [lazyTypeSymbol]: true;
12112
- /** Formats an error returned by any Lazy Type decoding operation. */
12113
- 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;
12114
13287
  }
12115
13288
 
12116
- type ValidateLazyTarget<Target extends ConcreteTypeNode> =
12117
- IsUnion<Target> extends false ? Target : LazyTargetConcreteTypeError;
13289
+ interface DataWork {
13290
+ readonly value: unknown;
13291
+ readonly path: DataPathNode | null;
13292
+ }
12118
13293
 
12119
- type LazyTargetConcreteTypeError = CompileTimeError<
12120
- "Type",
12121
- "Lazy Type definition must return one concrete Type node. Pass a Union Type node instead of a union of Type nodes."
12122
- >;
13294
+ const emptyDataPath: ReadonlyArray<PropertyKey> =
13295
+ /*#__PURE__*/ globalThis.Object.freeze([]);
12123
13296
 
12124
- type LazyResolution =
12125
- | { readonly state: "unresolved" }
12126
- | { readonly state: "resolving" }
12127
- | ResolvedLazyType
12128
- | { readonly state: "failed"; readonly error: unknown };
13297
+ const dataPathToArray = (
13298
+ path: DataPathNode | null,
13299
+ ): ReadonlyArray<PropertyKey> => {
13300
+ if (path === null) return emptyDataPath;
12129
13301
 
12130
- interface ResolvedLazyType {
12131
- readonly state: "resolved";
12132
- readonly target: RuntimeTypeNode;
12133
- readonly targetFromInput: RuntimeOperation<Result<unknown, TypeError>>;
12134
- readonly root: RuntimeTypeNode;
12135
- }
13302
+ const keys: Array<PropertyKey> = [];
13303
+ let node: DataPathNode | null = path;
12136
13304
 
12137
- const lazyTypeNodes = /*#__PURE__*/ new WeakSet<TypeNode>();
13305
+ while (node !== null) {
13306
+ keys.push(node.key);
13307
+ node = node.parent;
13308
+ }
12138
13309
 
12139
- const assertLazyReferencesAreGuarded = (type: RuntimeTypeNode): void => {
12140
- for (;;) {
12141
- assert(
12142
- !lazyTypeNodes.has(type),
12143
- "A Lazy Type definition must place every Lazy Type behind a structural boundary.",
12144
- );
13310
+ keys.reverse();
13311
+ return globalThis.Object.freeze(keys);
13312
+ };
12145
13313
 
12146
- if ("members" in type && globalThis.Array.isArray(type.members)) {
12147
- for (const member of type.members as ReadonlyArray<RuntimeTypeNode>)
12148
- 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;
12149
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;
12150
13359
 
12151
- if ("output" in type) {
12152
- 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;
12153
13523
  }
12154
13524
 
12155
- if (!type.parent) return;
12156
- 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
+ }
12157
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>;
12158
13585
  };
12159
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
+
12160
13618
  /**
12161
13619
  * A candidate JSON value before exact runtime validation.
12162
13620
  *
@@ -12428,7 +13886,7 @@ const validateJsonValue = (
12428
13886
  continue;
12429
13887
  }
12430
13888
 
12431
- const isArray = globalThis.Array.isArray(value);
13889
+ const isArray = Array.isArray(value);
12432
13890
 
12433
13891
  if (!isArray && !isPlainObject(value)) {
12434
13892
  if (
@@ -12634,8 +14092,7 @@ const getJsonValueRuntimeTypeIssues: RuntimeGetTypeIssues = (error, mode) => {
12634
14092
  })) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>;
12635
14093
  };
12636
14094
 
12637
- const parseJson = (value: string): JsonValue =>
12638
- globalThis.JSON.parse(value) as JsonValue;
14095
+ const parseJson = (value: string): JsonValue => JSON.parse(value) as JsonValue;
12639
14096
 
12640
14097
  const jsonToJsonValueResult = (value: string): Result<JsonValue, JsonError> => {
12641
14098
  const result = trySync((): unknown => parseJson(value));
@@ -12667,7 +14124,7 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12667
14124
  // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- JsonValue excludes the additional runtime types reported by tsgolint.
12668
14125
  switch (typeof value) {
12669
14126
  case "string":
12670
- chunks.push(globalThis.JSON.stringify(value));
14127
+ chunks.push(JSON.stringify(value));
12671
14128
  break;
12672
14129
  case "number":
12673
14130
  chunks.push(
@@ -12678,7 +14135,7 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12678
14135
  chunks.push(value ? "true" : "false");
12679
14136
  break;
12680
14137
  case "object": {
12681
- if (globalThis.Array.isArray(value)) {
14138
+ if (Array.isArray(value)) {
12682
14139
  const array = value as JsonArray;
12683
14140
  chunks.push("[");
12684
14141
  work.push({ kind: "Text", value: "]" });
@@ -12701,7 +14158,7 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12701
14158
  work.push({ kind: "Text", value: ":" });
12702
14159
  work.push({
12703
14160
  kind: "Text",
12704
- value: globalThis.JSON.stringify(key),
14161
+ value: JSON.stringify(key),
12705
14162
  });
12706
14163
  if (index > 0) work.push({ kind: "Text", value: "," });
12707
14164
  }
@@ -12719,7 +14176,12 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12719
14176
  * ### Example
12720
14177
  *
12721
14178
  * ```ts
12722
- * import { JsonValue, type JsonValueInput } from "@evolu/common";
14179
+ * import {
14180
+ * assertOk,
14181
+ * assertType,
14182
+ * JsonValue,
14183
+ * type JsonValueInput,
14184
+ * } from "@evolu/common";
12723
14185
  *
12724
14186
  * const input: JsonValueInput = {
12725
14187
  * name: "Ada",
@@ -12727,8 +14189,8 @@ const stringifyJsonValue = (value: JsonValue): Json => {
12727
14189
  * };
12728
14190
  * const result = JsonValue.fromUnknown(input);
12729
14191
  *
12730
- * expectOk(result, input);
12731
- * expectTypeOf(result.value).toEqualTypeOf<JsonValue>();
14192
+ * assertOk(result, input);
14193
+ * assertType<JsonValue, typeof result.value>();
12732
14194
  * ```
12733
14195
  *
12734
14196
  * @group JSON
@@ -12790,11 +14252,11 @@ export type Json = typeof Json.Output;
12790
14252
  * ### Example
12791
14253
  *
12792
14254
  * ```ts
12793
- * import { Json, jsonToJsonValue } from "@evolu/common";
14255
+ * import { assertEqual, Json, jsonToJsonValue } from "@evolu/common";
12794
14256
  *
12795
14257
  * const value = jsonToJsonValue(Json.orThrow('{"name":"Ada"}'));
12796
14258
  *
12797
- * expect(value).toEqual({ name: "Ada" });
14259
+ * assertEqual(value, { name: "Ada" });
12798
14260
  * ```
12799
14261
  *
12800
14262
  * @group JSON
@@ -12807,11 +14269,15 @@ export const jsonToJsonValue = (value: Json): JsonValue => parseJson(value);
12807
14269
  * ### Example
12808
14270
  *
12809
14271
  * ```ts
12810
- * import { jsonValueToJson, type JsonValue } from "@evolu/common";
14272
+ * import {
14273
+ * assertEqual,
14274
+ * jsonValueToJson,
14275
+ * type JsonValue,
14276
+ * } from "@evolu/common";
12811
14277
  *
12812
14278
  * const value: JsonValue = { name: "Ada" };
12813
14279
  *
12814
- * expect(jsonValueToJson(value)).toBe('{"name":"Ada"}');
14280
+ * assertEqual(jsonValueToJson(value), '{"name":"Ada"}');
12815
14281
  * ```
12816
14282
  *
12817
14283
  * @group JSON
@@ -12828,12 +14294,12 @@ export const jsonValueToJson = (value: JsonValue): Json =>
12828
14294
  * ### Example
12829
14295
  *
12830
14296
  * ```ts
12831
- * import { JsonValueFromJson } from "@evolu/common";
14297
+ * import { assertEqual, assertOk, JsonValueFromJson } from "@evolu/common";
12832
14298
  *
12833
14299
  * const result = JsonValueFromJson.fromUnknown('{ "name": "Ada" }');
12834
14300
  *
12835
- * expectOk(result, { name: "Ada" });
12836
- * expect(JsonValueFromJson.to(result.value)).toBe('{"name":"Ada"}');
14301
+ * assertOk(result, { name: "Ada" });
14302
+ * assertEqual(JsonValueFromJson.to(result.value), '{"name":"Ada"}');
12837
14303
  * ```
12838
14304
  *
12839
14305
  * @group JSON
@@ -12860,6 +14326,8 @@ export const JsonValueFromJson = /*#__PURE__*/ transform(
12860
14326
  *
12861
14327
  * ```ts
12862
14328
  * import {
14329
+ * assertEqual,
14330
+ * assertType,
12863
14331
  * Age,
12864
14332
  * NonEmptyTrimmedString100,
12865
14333
  * json,
@@ -12880,11 +14348,13 @@ export const JsonValueFromJson = /*#__PURE__*/ transform(
12880
14348
  * const user = User.orThrow({ name: "Ada", age: 37 });
12881
14349
  * const userJson = userToUserJson(user);
12882
14350
  *
12883
- * expectTypeOf(userJson).toEqualTypeOf<
12884
- * string & Brand<"Json"> & Brand<"UserJson">
14351
+ * assertType<
14352
+ * string & Brand<"Json"> & Brand<"UserJson">,
14353
+ * typeof userJson
12885
14354
  * >();
12886
- * expect(userJson).toBe('{"name":"Ada","age":37}');
12887
- * expect(userJsonToUser(userJson)).toEqual(user);
14355
+ * assertEqual(userJson, '{"name":"Ada","age":37}');
14356
+ * assertEqual(UserJson.orThrow(userJson), userJson);
14357
+ * assertEqual(userJsonToUser(userJson), user);
12888
14358
  * ```
12889
14359
  *
12890
14360
  * The supplied Type must have a JSON-compatible `CanonicalInput`. The branded
@@ -13076,14 +14546,12 @@ type JsonCompatibleMember<
13076
14546
 
13077
14547
  type AllowedJsonArrayKeys<Value extends ReadonlyArray<unknown>> =
13078
14548
  | keyof ReadonlyArray<unknown>
13079
- | (Value extends globalThis.Array<unknown>
13080
- ? keyof globalThis.Array<unknown>
13081
- : never)
14549
+ | (Value extends Array<unknown> ? keyof Array<unknown> : never)
13082
14550
  | JsonTupleIndexKeys<Value>;
13083
14551
 
13084
14552
  type JsonTupleIndexKeys<Value extends ReadonlyArray<unknown>> =
13085
14553
  Value extends readonly [...infer Elements]
13086
- ? Exclude<keyof Elements, keyof globalThis.Array<unknown>>
14554
+ ? Exclude<keyof Elements, keyof Array<unknown>>
13087
14555
  : never;
13088
14556
 
13089
14557
  type IsJsonCompatibilityCycle<
@@ -13095,17 +14563,6 @@ type IsJsonCompatibilityCycle<
13095
14563
  : IsJsonCompatibilityCycle<Value, Rest>
13096
14564
  : false;
13097
14565
 
13098
- /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters -- The
13099
- generic-function comparison distinguishes recursive types that mutual
13100
- assignability cannot. */
13101
- type IsSameType<A, B> =
13102
- (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
13103
- ? (<T>() => T extends B ? 1 : 2) extends <T>() => T extends A ? 1 : 2
13104
- ? true
13105
- : false
13106
- : false;
13107
- /* eslint-enable @typescript-eslint/no-unnecessary-type-parameters */
13108
-
13109
14566
  type JsonNumberCompatible<Value extends number> = [Value] extends [FiniteNumber]
13110
14567
  ? true
13111
14568
  : [Value] extends [Brand<string>]