@evolu/common 5.4.7 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +30 -34
  2. package/dist/src/Array.d.ts +17 -0
  3. package/dist/src/Array.d.ts.map +1 -0
  4. package/dist/src/Array.js +12 -0
  5. package/dist/src/Assert.d.ts +68 -0
  6. package/dist/src/Assert.d.ts.map +1 -0
  7. package/dist/src/Assert.js +77 -0
  8. package/dist/src/BigInt.d.ts +20 -0
  9. package/dist/src/BigInt.d.ts.map +1 -0
  10. package/dist/src/BigInt.js +18 -0
  11. package/dist/src/Buffer.d.ts +92 -0
  12. package/dist/src/Buffer.d.ts.map +1 -0
  13. package/dist/src/Buffer.js +62 -0
  14. package/dist/src/Callbacks.d.ts +20 -0
  15. package/dist/src/Callbacks.d.ts.map +1 -0
  16. package/dist/src/Callbacks.js +18 -0
  17. package/dist/src/Console.d.ts +78 -0
  18. package/dist/src/Console.d.ts.map +1 -0
  19. package/dist/src/Console.js +103 -0
  20. package/dist/src/Crypto.d.ts +72 -39
  21. package/dist/src/Crypto.d.ts.map +1 -1
  22. package/dist/src/Crypto.js +89 -54
  23. package/dist/src/Eq.d.ts +97 -0
  24. package/dist/src/Eq.d.ts.map +1 -0
  25. package/dist/src/Eq.js +167 -0
  26. package/dist/src/Error.d.ts +14 -10
  27. package/dist/src/Error.d.ts.map +1 -1
  28. package/dist/src/Error.js +43 -11
  29. package/dist/src/Evolu/Config.d.ts +69 -0
  30. package/dist/src/Evolu/Config.d.ts.map +1 -0
  31. package/dist/src/Evolu/Config.js +9 -0
  32. package/dist/src/Evolu/Db.d.ts +126 -0
  33. package/dist/src/Evolu/Db.d.ts.map +1 -0
  34. package/dist/src/Evolu/Db.js +774 -0
  35. package/dist/src/Evolu/Diff.d.ts +43 -0
  36. package/dist/src/Evolu/Diff.d.ts.map +1 -0
  37. package/dist/src/Evolu/Diff.js +95 -0
  38. package/dist/src/Evolu/Evolu.d.ts +334 -0
  39. package/dist/src/Evolu/Evolu.d.ts.map +1 -0
  40. package/dist/src/Evolu/Evolu.js +434 -0
  41. package/dist/src/Evolu/Internal.d.ts +26 -0
  42. package/dist/src/Evolu/Internal.d.ts.map +1 -0
  43. package/dist/src/Evolu/Internal.js +25 -0
  44. package/dist/src/Evolu/Kysely.d.ts +6 -0
  45. package/dist/src/Evolu/Kysely.d.ts.map +1 -0
  46. package/dist/src/Evolu/Kysely.js +21 -0
  47. package/dist/src/Evolu/Owner.d.ts +155 -0
  48. package/dist/src/Evolu/Owner.d.ts.map +1 -0
  49. package/dist/src/Evolu/Owner.js +126 -0
  50. package/dist/src/Evolu/Platform.d.ts +23 -0
  51. package/dist/src/Evolu/Platform.d.ts.map +1 -0
  52. package/dist/src/Evolu/Platform.js +1 -0
  53. package/dist/src/Evolu/Protocol.d.ts +401 -0
  54. package/dist/src/Evolu/Protocol.d.ts.map +1 -0
  55. package/dist/src/Evolu/Protocol.js +1151 -0
  56. package/dist/src/Evolu/Public.d.ts +18 -0
  57. package/dist/src/Evolu/Public.d.ts.map +1 -0
  58. package/dist/src/Evolu/Public.js +11 -0
  59. package/dist/src/Evolu/PublicKysely.d.ts +148 -0
  60. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
  61. package/dist/src/Evolu/PublicKysely.js +185 -0
  62. package/dist/src/Evolu/Query.d.ts +63 -0
  63. package/dist/src/Evolu/Query.d.ts.map +1 -0
  64. package/dist/src/Evolu/Query.js +61 -0
  65. package/dist/src/Evolu/Relay.d.ts +13 -0
  66. package/dist/src/Evolu/Relay.d.ts.map +1 -0
  67. package/dist/src/Evolu/Relay.js +109 -0
  68. package/dist/src/Evolu/Schema.d.ts +201 -0
  69. package/dist/src/Evolu/Schema.d.ts.map +1 -0
  70. package/dist/src/Evolu/Schema.js +150 -0
  71. package/dist/src/Evolu/Storage.d.ts +49 -0
  72. package/dist/src/Evolu/Storage.d.ts.map +1 -0
  73. package/dist/src/Evolu/Storage.js +1111 -0
  74. package/dist/src/Evolu/Sync.d.ts +59 -0
  75. package/dist/src/Evolu/Sync.d.ts.map +1 -0
  76. package/dist/src/Evolu/Sync.js +29 -0
  77. package/dist/src/Evolu/Timestamp.d.ts +106 -0
  78. package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
  79. package/dist/src/Evolu/Timestamp.js +179 -0
  80. package/dist/src/Function.d.ts +54 -0
  81. package/dist/src/Function.d.ts.map +1 -0
  82. package/dist/src/Function.js +38 -0
  83. package/dist/src/ManyToManyMap.d.ts +26 -0
  84. package/dist/src/ManyToManyMap.d.ts.map +1 -0
  85. package/dist/src/ManyToManyMap.js +92 -0
  86. package/dist/src/NanoId.d.ts +27 -0
  87. package/dist/src/NanoId.d.ts.map +1 -0
  88. package/dist/src/NanoId.js +6 -0
  89. package/dist/src/Number.d.ts +42 -0
  90. package/dist/src/Number.d.ts.map +1 -0
  91. package/dist/src/Number.js +55 -0
  92. package/dist/src/Object.d.ts +35 -0
  93. package/dist/src/Object.d.ts.map +1 -0
  94. package/dist/src/Object.js +36 -0
  95. package/dist/src/Order.d.ts +90 -0
  96. package/dist/src/Order.d.ts.map +1 -0
  97. package/dist/src/Order.js +85 -0
  98. package/dist/src/Promise.d.ts +180 -0
  99. package/dist/src/Promise.d.ts.map +1 -0
  100. package/dist/src/Promise.js +176 -0
  101. package/dist/src/Random.d.ts +52 -0
  102. package/dist/src/Random.d.ts.map +1 -0
  103. package/dist/src/Random.js +29 -0
  104. package/dist/src/Ref.d.ts +40 -0
  105. package/dist/src/Ref.d.ts.map +1 -0
  106. package/dist/src/Ref.js +13 -0
  107. package/dist/src/Result.d.ts +421 -0
  108. package/dist/src/Result.d.ts.map +1 -0
  109. package/dist/src/Result.js +357 -0
  110. package/dist/src/Skiplist.d.ts +23 -0
  111. package/dist/src/Skiplist.d.ts.map +1 -0
  112. package/dist/src/Skiplist.js +58 -0
  113. package/dist/src/Sqlite.d.ts +116 -52
  114. package/dist/src/Sqlite.d.ts.map +1 -1
  115. package/dist/src/Sqlite.js +183 -67
  116. package/dist/src/Store.d.ts +45 -8
  117. package/dist/src/Store.d.ts.map +1 -1
  118. package/dist/src/Store.js +33 -17
  119. package/dist/src/String.d.ts +2 -0
  120. package/dist/src/String.d.ts.map +1 -0
  121. package/dist/src/String.js +14 -0
  122. package/dist/src/Time.d.ts +20 -0
  123. package/dist/src/Time.d.ts.map +1 -0
  124. package/dist/src/Time.js +25 -0
  125. package/dist/src/Type.d.ts +1937 -0
  126. package/dist/src/Type.d.ts.map +1 -0
  127. package/dist/src/Type.js +2002 -0
  128. package/dist/src/Types.d.ts +188 -0
  129. package/dist/src/Types.d.ts.map +1 -0
  130. package/dist/src/Types.js +6 -0
  131. package/dist/src/WebSocket.d.ts +112 -0
  132. package/dist/src/WebSocket.d.ts.map +1 -0
  133. package/dist/src/WebSocket.js +139 -0
  134. package/dist/src/Worker.d.ts +44 -0
  135. package/dist/src/Worker.d.ts.map +1 -0
  136. package/dist/src/Worker.js +66 -0
  137. package/dist/src/index.d.ts +24 -11
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +24 -11
  140. package/package.json +29 -38
  141. package/src/Array.ts +39 -0
  142. package/src/Assert.ts +116 -0
  143. package/src/BigInt.ts +29 -0
  144. package/src/Buffer.ts +175 -0
  145. package/src/Callbacks.ts +43 -0
  146. package/src/Console.ts +159 -0
  147. package/src/Crypto.ts +169 -115
  148. package/src/Eq.ts +204 -0
  149. package/src/Error.ts +57 -20
  150. package/src/Evolu/Config.ts +83 -0
  151. package/src/Evolu/Db.ts +1275 -0
  152. package/src/Evolu/Diff.ts +142 -0
  153. package/src/Evolu/Evolu.ts +947 -0
  154. package/src/Evolu/Internal.ts +26 -0
  155. package/src/Evolu/Kysely.ts +38 -0
  156. package/src/Evolu/Owner.ts +296 -0
  157. package/src/Evolu/Platform.ts +27 -0
  158. package/src/Evolu/Protocol.ts +1857 -0
  159. package/src/Evolu/Public.ts +43 -0
  160. package/src/Evolu/PublicKysely.ts +240 -0
  161. package/src/Evolu/Query.ts +167 -0
  162. package/src/Evolu/Relay.ts +142 -0
  163. package/src/Evolu/Schema.ts +417 -0
  164. package/src/Evolu/Storage.ts +1281 -0
  165. package/src/Evolu/Sync.ts +105 -0
  166. package/src/Evolu/Timestamp.ts +311 -0
  167. package/src/Function.ts +58 -0
  168. package/src/ManyToManyMap.ts +140 -0
  169. package/src/NanoId.ts +39 -0
  170. package/src/Number.ts +90 -0
  171. package/src/Object.ts +64 -0
  172. package/src/Order.ts +113 -0
  173. package/src/Promise.ts +295 -0
  174. package/src/Random.ts +68 -0
  175. package/src/Ref.ts +63 -0
  176. package/src/Result.ts +453 -0
  177. package/src/Skiplist.ts +102 -0
  178. package/src/Sqlite.ts +366 -153
  179. package/src/Store.ts +79 -36
  180. package/src/String.ts +10 -0
  181. package/src/Time.ts +36 -0
  182. package/src/Type.ts +3978 -0
  183. package/src/Types.ts +209 -0
  184. package/src/WebSocket.ts +273 -0
  185. package/src/Worker.ts +129 -0
  186. package/src/index.ts +24 -11
  187. package/dist/src/Config.d.ts +0 -56
  188. package/dist/src/Config.d.ts.map +0 -1
  189. package/dist/src/Config.js +0 -39
  190. package/dist/src/Crdt.d.ts +0 -89
  191. package/dist/src/Crdt.d.ts.map +0 -1
  192. package/dist/src/Crdt.js +0 -181
  193. package/dist/src/Db.d.ts +0 -107
  194. package/dist/src/Db.d.ts.map +0 -1
  195. package/dist/src/Db.js +0 -443
  196. package/dist/src/Diff.d.ts +0 -27
  197. package/dist/src/Diff.d.ts.map +0 -1
  198. package/dist/src/Diff.js +0 -84
  199. package/dist/src/Evolu.d.ts +0 -426
  200. package/dist/src/Evolu.d.ts.map +0 -1
  201. package/dist/src/Evolu.js +0 -333
  202. package/dist/src/Model.d.ts +0 -141
  203. package/dist/src/Model.d.ts.map +0 -1
  204. package/dist/src/Model.js +0 -125
  205. package/dist/src/Murmurhash.d.ts +0 -2
  206. package/dist/src/Murmurhash.d.ts.map +0 -1
  207. package/dist/src/Murmurhash.js +0 -60
  208. package/dist/src/Owner.d.ts +0 -33
  209. package/dist/src/Owner.d.ts.map +0 -1
  210. package/dist/src/Owner.js +0 -26
  211. package/dist/src/Platform.d.ts +0 -37
  212. package/dist/src/Platform.d.ts.map +0 -1
  213. package/dist/src/Platform.js +0 -11
  214. package/dist/src/Protobuf.d.ts +0 -81
  215. package/dist/src/Protobuf.d.ts.map +0 -1
  216. package/dist/src/Protobuf.js +0 -92
  217. package/dist/src/Public.d.ts +0 -13
  218. package/dist/src/Public.d.ts.map +0 -1
  219. package/dist/src/Public.js +0 -6
  220. package/dist/src/Socket.d.ts +0 -8
  221. package/dist/src/Socket.d.ts.map +0 -1
  222. package/dist/src/Socket.js +0 -51
  223. package/dist/src/Sql.d.ts +0 -12
  224. package/dist/src/Sql.d.ts.map +0 -1
  225. package/dist/src/Sql.js +0 -30
  226. package/dist/src/Sync.d.ts +0 -70
  227. package/dist/src/Sync.d.ts.map +0 -1
  228. package/dist/src/Sync.js +0 -127
  229. package/src/Config.ts +0 -119
  230. package/src/Crdt.ts +0 -361
  231. package/src/Db.ts +0 -955
  232. package/src/Diff.ts +0 -114
  233. package/src/Evolu.ts +0 -1016
  234. package/src/Model.ts +0 -233
  235. package/src/Murmurhash.ts +0 -70
  236. package/src/Owner.ts +0 -69
  237. package/src/Platform.ts +0 -47
  238. package/src/Protobuf.ts +0 -155
  239. package/src/Public.ts +0 -12
  240. package/src/Socket.ts +0 -83
  241. package/src/Sql.ts +0 -41
  242. package/src/Sync.ts +0 -315
@@ -0,0 +1,2002 @@
1
+ /**
2
+ * 🧩 Validation, Parsing, and Transformation
3
+ *
4
+ * ## Intro
5
+ *
6
+ * You probably know [Zod](https://zod.dev). Evolu has {@link Type}.
7
+ *
8
+ * Evolu Type exists because no existing validation/parsing/transformation
9
+ * library fully met our needs:
10
+ *
11
+ * - **Result-based error handling**: Leveraging {@link Result} instead of throwing
12
+ * exceptions.
13
+ * - **Consistent constraints**: Enforcing {@link Brand} for all constraints.
14
+ * - **Typed errors with decoupled formatters**: Avoiding coupling error messages
15
+ * with validators.
16
+ * - **No user-land chaining**: Designed with ES pipe operator in mind.
17
+ * - **Selective validation/transformation**: Skipping parent Type validations and
18
+ * transformations when TypeScript's type system can be relied upon.
19
+ * - **Bidirectional transformations**: Supporting transformations in both
20
+ * directions.
21
+ * - **Minimal and transparent code**: No runtime dependencies or hidden magic.
22
+ *
23
+ * **Note**: A proper quickstart guide is on the way. In the meantime, each type
24
+ * includes its own usage example, and you can (and should) check the tests for
25
+ * practical demonstrations of the API. Or dang, just read the code. It's
26
+ * simple.
27
+ *
28
+ * - Evolu `Type` is:
29
+ *
30
+ * - A TypeScript type with a {@link Brand} whenever it's possible.
31
+ * - A function to create a value of that type, which may fail.
32
+ * - A function to transform value back to its original representation, which
33
+ * cannot fail.
34
+ *
35
+ * Types are chainable. The chain starts with a Base Type that refines an
36
+ * unknown value into something and can continue with further refinements or
37
+ * transformations. For example, `NonEmptyTrimmedString100` chain looks like
38
+ * this:
39
+ *
40
+ * `Unknown` -> `String` -> `TrimmedString` -> `NonEmptyTrimmedString100`
41
+ *
42
+ * For `NonEmptyTrimmedString100`, the parent Type is `TrimmedString`. For
43
+ * `TrimmedString`, the parent Type is `String`.
44
+ *
45
+ * The parent of the `String` Type is the `String` Type itself. All Base Types
46
+ * `fromParent` functions are just a typed alias to `fromUnknown` to ensure that
47
+ * `fromParent` and `toParent` can be called on any Type.
48
+ *
49
+ * Speaking of `fromParent` and `toParent`, those functions exist to bypass
50
+ * parent Types when we can rely on TypeScript types.
51
+ *
52
+ * `Type` transformations should be reversible. If you need an irreversible
53
+ * transformation, such as `TrimString` (trimming is not reversible as `untrim`
54
+ * can't know what has been trimmed), you can do that, but note in JSDoc that
55
+ * `to` will not restore the original representation. You can also use
56
+ * {@link assert}: `assert(false, "Untrim is not possible")`.
57
+ *
58
+ * ### Tip
59
+ *
60
+ * If necessary, write `globalThis.String` instead of `String` to avoid naming
61
+ * clashes with Base Types.
62
+ *
63
+ * ### Design Decision:
64
+ *
65
+ * While the `from` function can fail, the `to` function cannot. This simplifies
66
+ * the model by ensuring that every valid input has a corresponding valid
67
+ * output, eliminating the risk of edge cases caused by irreversible
68
+ * operations.
69
+ *
70
+ * @module
71
+ */
72
+ import * as bip39 from "@scure/bip39";
73
+ import { wordlist } from "@scure/bip39/wordlists/english";
74
+ import { assert } from "./Assert.js";
75
+ import { identity } from "./Function.js";
76
+ import { isPlainObject } from "./Object.js";
77
+ import { err, ok, trySync } from "./Result.js";
78
+ import { safelyStringifyUnknownValue } from "./String.js";
79
+ const EvoluTypeSymbol = Symbol("evolu.Type");
80
+ /**
81
+ * Checks if the given value is an {@link Type}.
82
+ *
83
+ * @category Utilities
84
+ */
85
+ export const isType = (value) => typeof value === "object" && value !== null && EvoluTypeSymbol in value;
86
+ const createType = (name, definition) => ({
87
+ ...definition,
88
+ name,
89
+ is: (value) => definition.fromUnknown(value).ok,
90
+ from: definition.fromUnknown,
91
+ [EvoluTypeSymbol]: true,
92
+ Type: undefined,
93
+ Input: undefined,
94
+ Error: undefined,
95
+ Parent: undefined,
96
+ ParentError: undefined,
97
+ Errors: undefined,
98
+ });
99
+ /**
100
+ * Creates a formatter function for {@link TypeError}.
101
+ *
102
+ * The formatter generates human-readable error messages using a custom
103
+ * formatting function and a safely stringified error value.
104
+ *
105
+ * ### Example
106
+ *
107
+ * ```ts
108
+ * const formatStringError = createTypeErrorFormatter<StringError>(
109
+ * (value) => `A value ${value} is not a string.`,
110
+ * );
111
+ * ```
112
+ *
113
+ * @category Utilities
114
+ */
115
+ export const createTypeErrorFormatter = (format) => (error) => format({ ...error, value: safelyStringifyUnknownValue(error.value) });
116
+ /**
117
+ * Base {@link Type}.
118
+ *
119
+ * A Base Type validates that a value conforms to a specific TypeScript type.
120
+ * Unlike refinements or transformations, Base Types establish the fundamental
121
+ * shape of a value before any branding or transformation occurs.
122
+ *
123
+ * - To **refine** a Base Type further, use the {@link brand} Type Factory.
124
+ * - To **transform** a Base Type into a different representation, use the
125
+ * {@link transform} Type Factory.
126
+ *
127
+ * ### Example
128
+ *
129
+ * ```ts
130
+ * const String = base("String", (value) =>
131
+ * typeof value === "string"
132
+ * ? ok(value)
133
+ * : err<StringError>({ type: "String", value }),
134
+ * );
135
+ *
136
+ * interface StringError extends TypeError<"String"> {}
137
+ *
138
+ * const formatStringError = createTypeErrorFormatter<StringError>(
139
+ * (error) => `A value ${error.value} is not a string`,
140
+ * );
141
+ * ```
142
+ *
143
+ * @category Base Factories
144
+ */
145
+ export const base = (name, fromUnknown) => createType(name, {
146
+ fromUnknown,
147
+ to: identity,
148
+ fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
149
+ toParent: identity,
150
+ });
151
+ /**
152
+ * Creates a formatter function for a base {@link TypeError}.
153
+ *
154
+ * This formatter is specifically for Base Types that only need a simple error
155
+ * message indicating that the value is not of the expected type.
156
+ *
157
+ * ### Example
158
+ *
159
+ * ```ts
160
+ * export const formatStringError =
161
+ * createBaseTypeErrorFormatter<StringError>();
162
+ * ```
163
+ *
164
+ * @category Utilities
165
+ */
166
+ export const createBaseTypeErrorFormatter = () => createTypeErrorFormatter((error) => `A value ${error.value} is not a ${error.type.toLowerCase()}.`);
167
+ /** @category Base Types */
168
+ export const Unknown = base("Unknown", ok);
169
+ /**
170
+ * @category Base Types
171
+ * @category String
172
+ */
173
+ export const String = base("String", (value) => typeof value === "string"
174
+ ? ok(value)
175
+ : err({ type: "String", value }));
176
+ export const formatStringError = createBaseTypeErrorFormatter();
177
+ /** @category Base Types */
178
+ export const Number = base("Number", (value) => typeof value === "number"
179
+ ? ok(value)
180
+ : err({ type: "Number", value }));
181
+ export const formatNumberError = createBaseTypeErrorFormatter();
182
+ /** @category Base Types */
183
+ export const BigInt = base("BigInt", (value) => typeof value === "bigint"
184
+ ? ok(value)
185
+ : err({ type: "BigInt", value }));
186
+ export const formatBigIntError = createBaseTypeErrorFormatter();
187
+ /** @category Base Types */
188
+ export const Boolean = base("Boolean", (value) => typeof value === "boolean"
189
+ ? ok(value)
190
+ : err({ type: "Boolean", value }));
191
+ export const formatBooleanError = createBaseTypeErrorFormatter();
192
+ /** @category Base Types */
193
+ export const Undefined = base("Undefined", (value) => value === undefined
194
+ ? ok(value)
195
+ : err({ type: "Undefined", value }));
196
+ export const formatUndefinedError = createBaseTypeErrorFormatter();
197
+ /** @category Base Types */
198
+ export const Null = base("Null", (value) => value === null ? ok(value) : err({ type: "Null", value }));
199
+ export const formatNullError = createBaseTypeErrorFormatter();
200
+ /** @category Base Types */
201
+ export const Function = base("Function", (value) => typeof value === "function"
202
+ ? ok(value)
203
+ : err({ type: "Function", value }));
204
+ export const formatFunctionError = createBaseTypeErrorFormatter();
205
+ /** @category Base Types */
206
+ export const Uint8Array = base("Uint8Array", (value) => value instanceof globalThis.Uint8Array
207
+ ? ok(value)
208
+ : err({ type: "Uint8Array", value }));
209
+ export const formatUint8ArrayError = createBaseTypeErrorFormatter();
210
+ /**
211
+ * `instanceof` {@link Type}.
212
+ *
213
+ * Ensures that a value is an instance of the given class constructor.
214
+ *
215
+ * ### Example
216
+ *
217
+ * ```ts
218
+ * class User {
219
+ * constructor(public name: string) {}
220
+ * }
221
+ *
222
+ * const UserInstance = instanceOf(User);
223
+ *
224
+ * const result = UserInstance.from(new User("Alice")); // ok
225
+ * const error = UserInstance.from({}); // err
226
+ * ```
227
+ *
228
+ * @category Base Factories
229
+ */
230
+ export const instanceOf = (ctor) => ({
231
+ ...base("InstanceOf", (value) => value instanceof ctor
232
+ ? ok(value)
233
+ : err({ type: "InstanceOf", value, ctor: ctor.name })),
234
+ ctor,
235
+ });
236
+ export const formatInstanceOfError = createTypeErrorFormatter((error) => `Value ${error.value} is not an instance of ${error.ctor}`);
237
+ /**
238
+ * JavaScript Date.
239
+ *
240
+ * @category Base Types
241
+ */
242
+ export const Date = instanceOf(globalThis.Date);
243
+ /**
244
+ * Validates that an unknown value is an Evolu {@link Type} (i.e., satisfies
245
+ * `AnyType`).
246
+ *
247
+ * ### Example
248
+ *
249
+ * ```ts
250
+ * const result = EvoluType.from(String); // ok(String)
251
+ * const error = EvoluType.from("not a Type"); // err
252
+ * ```
253
+ */
254
+ // TODO: Rename to TypeInstance or something like that.
255
+ export const EvoluType = base("EvoluType", (value) => isType(value)
256
+ ? ok(value)
257
+ : err({
258
+ type: "EvoluType",
259
+ value,
260
+ }));
261
+ export const formatIsTypeError = createTypeErrorFormatter((error) => `Value ${error.value} is not a valid Evolu Type.`);
262
+ export function brand(name, parent, refine) {
263
+ const fromUnknown = refine
264
+ ? (value) => {
265
+ const parentResult = parent.fromUnknown(value);
266
+ if (!parentResult.ok)
267
+ return parentResult;
268
+ return refine(parentResult.value);
269
+ }
270
+ : (value) => {
271
+ const parentResult = parent.fromUnknown(value);
272
+ if (!parentResult.ok)
273
+ return err({
274
+ type: name,
275
+ value,
276
+ parentError: parentResult.error,
277
+ });
278
+ return ok(parentResult.value);
279
+ };
280
+ return {
281
+ ...createType("Brand", {
282
+ fromUnknown,
283
+ to: identity,
284
+ fromParent: (refine ?? ok),
285
+ toParent: identity,
286
+ }),
287
+ brand: name,
288
+ parentType: parent,
289
+ };
290
+ }
291
+ /**
292
+ * A three-letter ISO 4217 currency code (e.g., USD, EUR).
293
+ *
294
+ * @category String
295
+ */
296
+ export const CurrencyCode = brand("CurrencyCode", String, (value) => /^[A-Z]{3}$/.test(value)
297
+ ? ok(value)
298
+ : err({ type: "CurrencyCode", value }));
299
+ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}`);
300
+ /**
301
+ * ISO 8601 date-time string.
302
+ *
303
+ * This {@link Type} represents a date-time string that follows the ISO 8601
304
+ * format and is compatible with SQLite, which lacks a native date type and
305
+ * relies on ISO 8601 strings for sorting. Enforcing a 24-character format
306
+ * ensures correct lexicographic ordering.
307
+ *
308
+ * It must be a valid JavaScript Date string that can be parsed.
309
+ *
310
+ * Valid range: `"0000-01-01T00:00:00.000Z"` to `"9999-12-31T23:59:59.999Z"`.
311
+ *
312
+ * ### Example
313
+ *
314
+ * ```ts
315
+ * const result = DateIsoString.from("2023-01-01T12:00:00.000Z"); // ok
316
+ * const error = DateIsoString.from("10000-01-01T00:00:00.000Z"); // err
317
+ * ```
318
+ *
319
+ * @category String
320
+ */
321
+ export const DateIsoString = brand("DateIso", String, (value) => {
322
+ if (value.length !== 24) {
323
+ return err({ type: "DateIsoString", value });
324
+ }
325
+ if (isNaN(globalThis.Date.parse(value))) {
326
+ return err({ type: "DateIsoString", value });
327
+ }
328
+ return ok(value);
329
+ });
330
+ export const formatDateIsoStringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
331
+ /**
332
+ * Trimmed string.
333
+ *
334
+ * This Type Factory does not transform; it only validates whether a string has
335
+ * no leading or trailing whitespaces. To trim a string, use {@link trim} Type
336
+ * Factory.
337
+ *
338
+ * ### Examples
339
+ *
340
+ * ```ts
341
+ * // this Type already exists
342
+ * const TrimmedString = trimmed(String);
343
+ * type TrimmedString = typeof TrimmedString.Type;
344
+ *
345
+ * // we can make any branded Type trimmed:
346
+ * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
347
+ * // string & Brand<"MinLength1"> & Brand<"Trimmed">
348
+ * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
349
+ * ```
350
+ *
351
+ * @category String
352
+ */
353
+ export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.trim().length === value.length
354
+ ? ok(value)
355
+ : err({ type: "Trimmed", value }));
356
+ export const formatTrimmedError = createTypeErrorFormatter((error) => `A value ${error.value} is not trimmed`);
357
+ /**
358
+ * Trims leading and trailing whitespace from a string.
359
+ *
360
+ * This Type Factory **transforms** the input string by removing whitespace from
361
+ * both ends. For validation only, use {@link trimmed} Type Factory.
362
+ *
363
+ * ### Example
364
+ *
365
+ * ```ts
366
+ * const TrimString = trim(String);
367
+ * expect(TrimString.from("a ")).toEqual(ok("a"));
368
+ * expect(TrimString.fromParent("a ").value).toEqual("a");
369
+ *
370
+ * const TrimNonEmptyString = trim(NonEmptyString);
371
+ * expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
372
+ * expect(
373
+ * TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
374
+ * ).toEqual("a");
375
+ * ```
376
+ *
377
+ * **Note:** This transformation is irreversible. Calling `toParent` will not
378
+ * restore the original representation.
379
+ *
380
+ * @category String
381
+ */
382
+ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok(value.trim()), (value) => value);
383
+ /**
384
+ * Trimmed string
385
+ *
386
+ * - Use `TrimmedString.is` to check if an unknown value is trimmed.
387
+ * - Use `TrimmedString.from` to check if a string is trimmed.
388
+ *
389
+ * @category String
390
+ */
391
+ export const TrimmedString = trimmed(String);
392
+ /**
393
+ * Minimum length.
394
+ *
395
+ * ### Example
396
+ *
397
+ * ```ts
398
+ * // string & Brand<"MinLength1">
399
+ * const NonEmptyString = minLength(1)(String);
400
+ * ```
401
+ *
402
+ * @category String
403
+ * @category Array
404
+ */
405
+ export const minLength = (min) => (parent) => brand(`MinLength${min}`, parent, (value) => value.length >= min ? ok(value) : err({ type: "MinLength", value, min }));
406
+ export const formatMinLengthError = createTypeErrorFormatter((error) => `Value ${error.value} does not meet the minimum length of ${error.min}.`);
407
+ /**
408
+ * Maximum length.
409
+ *
410
+ * ### Example
411
+ *
412
+ * ```ts
413
+ * // string & Brand<"MaxLength100">
414
+ * const String100 = maxLength(100)(String);
415
+ * ```
416
+ *
417
+ * @category String
418
+ * @category Array
419
+ */
420
+ export const maxLength = (max) => (parent) => brand(`MaxLength${max}`, parent, (value) => value.length <= max ? ok(value) : err({ type: "MaxLength", value, max }));
421
+ export const formatMaxLengthError = createTypeErrorFormatter((error) => `Value ${error.value} exceeds the maximum length of ${error.max}.`);
422
+ /**
423
+ * Exact length.
424
+ *
425
+ * ### Example
426
+ *
427
+ * ```ts
428
+ * // string & Brand<"Length1">
429
+ * const Length1String = length(1)(String);
430
+ * ```
431
+ *
432
+ * @category String
433
+ * @category Array
434
+ */
435
+ export const length = (exact) => (parent) => brand(`Length${exact}`, parent, (value) => value.length === exact
436
+ ? ok(value)
437
+ : err({ type: "Length", value, exact }));
438
+ export const formatLengthError = createTypeErrorFormatter((error) => `Value ${error.value} does not have the required length of ${error.exact}.`);
439
+ /** @category String */
440
+ export const NonEmptyString = minLength(1)(String);
441
+ /** @category String */
442
+ export const String100 = maxLength(100)(String);
443
+ /** @category String */
444
+ export const String1000 = maxLength(1000)(String);
445
+ /** @category String */
446
+ export const NonEmptyString100 = minLength(1)(String100);
447
+ /** @category String */
448
+ export const NonEmptyString1000 = minLength(1)(String1000);
449
+ /** @category String */
450
+ export const NonEmptyTrimmedString = minLength(1)(TrimmedString);
451
+ /** @category String */
452
+ export const TrimmedString100 = maxLength(100)(TrimmedString);
453
+ /** @category String */
454
+ export const TrimmedString1000 = maxLength(1000)(TrimmedString);
455
+ /** @category String */
456
+ export const NonEmptyTrimmedString100 = minLength(1)(TrimmedString100);
457
+ /** @category String */
458
+ export const NonEmptyTrimmedString1000 = minLength(1)(TrimmedString1000);
459
+ /**
460
+ * The mnemonic, also known as a "seed phrase," is a set of 12 words in a
461
+ * specific order chosen from a predefined list (BIP39). It provides a
462
+ * human-readable way to store a private key securely. The mnemonic is generated
463
+ * safely on the user's device using cryptographically secure random number
464
+ * generation, ensuring it remains private and unique.
465
+ *
466
+ * @category String
467
+ */
468
+ export const Mnemonic = brand("Mnemonic", NonEmptyTrimmedString, (value) => bip39.validateMnemonic(value, wordlist)
469
+ ? ok(value)
470
+ : err({ type: "Mnemonic", value }));
471
+ export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}`);
472
+ /**
473
+ * String matching a regular expression.
474
+ *
475
+ * ### Example
476
+ *
477
+ * ```ts
478
+ * const Alphanumeric = regex("Alphanumeric", /^[a-z0-9]+$/i)(String);
479
+ * ```
480
+ *
481
+ * @category String
482
+ */
483
+ export const regex = (name, pattern) => {
484
+ // Clone the regex to avoid shared state.
485
+ const clonedPattern = new RegExp(pattern.source, pattern.flags);
486
+ return (parent) => brand(name, parent, (value) => {
487
+ // Reset `lastIndex` before each use to ensure the regex starts matching
488
+ // from the beginning of the string.
489
+ clonedPattern.lastIndex = 0;
490
+ return clonedPattern.test(value)
491
+ ? ok(value)
492
+ : err({ type: "Regex", name, value, pattern });
493
+ });
494
+ };
495
+ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`);
496
+ /**
497
+ * URL-safe Base64 string.
498
+ *
499
+ * A `Base64Url` string uses a limited alphabet that is URL-safe:
500
+ *
501
+ * - Uppercase letters (`A-Z`)
502
+ * - Lowercase letters (`a-z`)
503
+ * - Digits (`0-9`)
504
+ * - Dash (`-`)
505
+ * - Underscore (`_`)
506
+ *
507
+ * ### Example
508
+ *
509
+ * ```ts
510
+ * const result = Base64Url.from("abc123_-");
511
+ * if (result.ok) {
512
+ * console.log("Valid Base64Url string:", result.value);
513
+ * } else {
514
+ * console.error("Invalid Base64Url string:", result.error);
515
+ * }
516
+ * ```
517
+ *
518
+ * @category String
519
+ */
520
+ export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
521
+ /**
522
+ * Simple alphanumeric string for naming.
523
+ *
524
+ * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
525
+ *
526
+ * - Uppercase letters (`A-Z`)
527
+ * - Lowercase letters (`a-z`)
528
+ * - Digits (`0-9`)
529
+ * - Dash (`-`)
530
+ *
531
+ * The string must be between 1 and 42 characters.
532
+ *
533
+ * ### Example
534
+ *
535
+ * ```ts
536
+ * const result = SimpleName.from("data-report-123");
537
+ * if (result.ok) {
538
+ * console.log("Valid SimpleName string:", result.value);
539
+ * } else {
540
+ * console.error("Invalid SimpleName string:", result.error);
541
+ * }
542
+ * ```
543
+ *
544
+ * @category String
545
+ */
546
+ export const SimpleName = regex("SimpleName", /^[a-z0-9-]{1,42}$/i)(String);
547
+ /**
548
+ * Default NanoId.
549
+ *
550
+ * @category String
551
+ */
552
+ export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
553
+ /**
554
+ * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
555
+ *
556
+ * @category String
557
+ */
558
+ export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
559
+ export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
560
+ /**
561
+ * `Id` {@link Type}.
562
+ *
563
+ * Represents a unique identifier with exactly 21 characters, using NanoID's
564
+ * standard format (`A-Za-z0-9_-`).
565
+ *
566
+ * @category String
567
+ */
568
+ export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
569
+ export const idTypeValueLength = 21;
570
+ /**
571
+ * Creates an {@link Id}.
572
+ *
573
+ * ### Example
574
+ *
575
+ * ```ts
576
+ * // string & Brand<"Id">
577
+ * const id = createId(deps);
578
+ * ```
579
+ */
580
+ export const createId = (deps) => deps.nanoIdLib.nanoid();
581
+ /**
582
+ * Type Factory to create branded {@link Id} Type for a specific table.
583
+ *
584
+ * ### Example
585
+ *
586
+ * ```ts
587
+ * const TodoId = id("Todo");
588
+ * // string & Brand<"Id"> & Brand<"Todo">
589
+ * type TodoId = typeof TodoId.Type;
590
+ * ```
591
+ *
592
+ * @category String
593
+ */
594
+ export const id = (table) => {
595
+ const fromParent = (value) => {
596
+ const idResult = Id.fromParent(value);
597
+ if (!idResult.ok) {
598
+ return err({ type: "Id", value, table });
599
+ }
600
+ return ok(idResult.value);
601
+ };
602
+ const fromUnknown = (value) => {
603
+ const parentResult = String.fromUnknown(value);
604
+ if (!parentResult.ok)
605
+ return parentResult;
606
+ return fromParent(parentResult.value);
607
+ };
608
+ return {
609
+ ...createType("Id", {
610
+ fromUnknown,
611
+ to: (value) => value,
612
+ fromParent,
613
+ toParent: (value) => value,
614
+ }),
615
+ table,
616
+ };
617
+ };
618
+ export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
619
+ /**
620
+ * Positive number.
621
+ *
622
+ * ### Example
623
+ *
624
+ * ```ts
625
+ * const PositiveNumber = positive(Number);
626
+ *
627
+ * const result = PositiveNumber.from(42); // ok
628
+ * const errorResult = PositiveNumber.from(-5); // err
629
+ * ```
630
+ *
631
+ * @category Number
632
+ */
633
+ export const positive = (parent) => brand("Positive", parent, (value) => value > 0 ? ok(value) : err({ type: "Positive", value }));
634
+ export const formatPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} is not positive.`);
635
+ /**
636
+ * Negative number.
637
+ *
638
+ * ### Example
639
+ *
640
+ * ```ts
641
+ * const NegativeNumber = negative(Number);
642
+ * ```
643
+ *
644
+ * @category Number
645
+ */
646
+ export const negative = (parent) => brand("Negative", parent, (value) => value < 0 ? ok(value) : err({ type: "Negative", value }));
647
+ export const formatNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} is not negative.`);
648
+ /**
649
+ * Non-positive number.
650
+ *
651
+ * ### Example
652
+ *
653
+ * ```ts
654
+ * const NonPositiveNumber = nonPositive(Number);
655
+ * ```
656
+ *
657
+ * @category Number
658
+ */
659
+ export const nonPositive = (parent) => brand("NonPositive", parent, (value) => value <= 0
660
+ ? ok(value)
661
+ : err({ type: "NonPositive", value }));
662
+ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} is not non-positive.`);
663
+ /**
664
+ * Non-negative number.
665
+ *
666
+ * ### Example
667
+ *
668
+ * ```ts
669
+ * const NonNegativeNumber = nonNegative(Number);
670
+ * ```
671
+ *
672
+ * @category Number
673
+ */
674
+ export const nonNegative = (parent) => brand("NonNegative", parent, (value) => value >= 0
675
+ ? ok(value)
676
+ : err({ type: "NonNegative", value }));
677
+ export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} is not non-negative.`);
678
+ /** @category Number */
679
+ export const NonNegativeNumber = nonNegative(Number);
680
+ /** @category Number */
681
+ export const PositiveNumber = positive(NonNegativeNumber);
682
+ /** @category Number */
683
+ export const NonPositiveNumber = nonPositive(Number);
684
+ /** @category Number */
685
+ export const NegativeNumber = negative(NonPositiveNumber);
686
+ /**
687
+ * Integer within the safe range of JavaScript numbers.
688
+ *
689
+ * ### Example
690
+ *
691
+ * ```ts
692
+ * const Int = int(Number);
693
+ * ```
694
+ *
695
+ * @category Number
696
+ */
697
+ export const int = (parent) => brand("Int", parent, (value) => globalThis.Number.isSafeInteger(value)
698
+ ? ok(value)
699
+ : err({ type: "Int", value }));
700
+ export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} is not an integer.`);
701
+ /**
702
+ * Integer within the safe range of JavaScript numbers.
703
+ *
704
+ * @category Number
705
+ */
706
+ export const Int = int(Number);
707
+ /** @category Number */
708
+ export const NonNegativeInt = nonNegative(Int);
709
+ /** @category Number */
710
+ export const PositiveInt = positive(NonNegativeInt);
711
+ /** @category Number */
712
+ export const NonPositiveInt = nonPositive(Int);
713
+ /** @category Number */
714
+ export const NegativeInt = negative(NonPositiveInt);
715
+ /**
716
+ * Number greater than a specified value.
717
+ *
718
+ * @category Number
719
+ */
720
+ export const greaterThan = (min) => (parent) => brand(`GreaterThan${min}`, parent, (value) => value > min ? ok(value) : err({ type: "GreaterThan", value, min }));
721
+ export const formatGreaterThanError = createTypeErrorFormatter((error) => `The value ${error.value} is not > ${error.min}.`);
722
+ /**
723
+ * Number less than a specified value.
724
+ *
725
+ * @category Number
726
+ */
727
+ export const lessThan = (max) => (parent) => brand(`LessThan${max}`, parent, (value) => value < max ? ok(value) : err({ type: "LessThan", value, max }));
728
+ export const formatLessThanError = createTypeErrorFormatter((error) => `The value ${error.value} is not < ${error.max}.`);
729
+ /**
730
+ * Number ≥ a specified value.
731
+ *
732
+ * @category Number
733
+ */
734
+ export const greaterThanOrEqualTo = (min) => (parent) => brand(`GreaterThanOrEqualTo${min}`, parent, (value) => value >= min
735
+ ? ok(value)
736
+ : err({ type: "GreaterThanOrEqualTo", value, min }));
737
+ export const formatGreaterThanOrEqualToError = createTypeErrorFormatter((error) => `The value ${error.value} is not >= ${error.min}.`);
738
+ /**
739
+ * Number ≤ a specified value.
740
+ *
741
+ * @category Number
742
+ */
743
+ export const lessThanOrEqualTo = (max) => (parent) => brand(`LessThanOrEqualTo${max}`, parent, (value) => value <= max ? ok(value) : err({ type: "LessThanOrEqualTo", value, max }));
744
+ export const formatLessThanOrEqualToError = createTypeErrorFormatter((error) => `The value ${error.value} is not <= ${error.max}.`);
745
+ /**
746
+ * Number that is not NaN.
747
+ *
748
+ * @category Number
749
+ */
750
+ export const nonNaN = (parent) => brand("NonNaN", parent, (value) => !globalThis.Number.isNaN(value)
751
+ ? ok(value)
752
+ : err({ type: "NonNaN", value }));
753
+ export const formatNonNaNError = createTypeErrorFormatter((error) => `The value ${error.value} is NaN (not a number).`);
754
+ /** @category Number */
755
+ export const NonNaNNumber = nonNaN(Number);
756
+ /**
757
+ * Finite number.
758
+ *
759
+ * @category Number
760
+ */
761
+ export const finite = (parent) => brand("Finite", parent, (value) => globalThis.Number.isFinite(value)
762
+ ? ok(value)
763
+ : err({ type: "Finite", value }));
764
+ export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} is not finite.`);
765
+ /**
766
+ * Finite number.
767
+ *
768
+ * This Type ensures that a number is finite.
769
+ *
770
+ * **Why is this important?**
771
+ *
772
+ * `JSON.stringify` serializes JavaScript numbers into `null` if they are not
773
+ * finite (e.g., `Infinity`, `-Infinity`, or `NaN`). Using `FiniteNumber` helps
774
+ * prevent these unexpected behaviors when working with JSON serialization.
775
+ *
776
+ * @category Number
777
+ */
778
+ export const FiniteNumber = finite(Number);
779
+ /**
780
+ * Number that is a multiple of a divisor.
781
+ *
782
+ * @category Number
783
+ */
784
+ export const multipleOf = (divisor) => (parent) => brand(`MultipleOf${divisor}`, parent, (value) => value % divisor === 0
785
+ ? ok(value)
786
+ : err({ type: "MultipleOf", value, divisor }));
787
+ export const formatMultipleOfError = createTypeErrorFormatter((error) => `The value ${error.value} is not a multiple of ${error.divisor}.`);
788
+ /**
789
+ * Number within a range, inclusive.
790
+ *
791
+ * ### Example
792
+ *
793
+ * ```ts
794
+ * const Between1And10 = between(1, 10)(PositiveNumber);
795
+ * const result = Between1And10.from(5); // ok(5)
796
+ * const errorResult = Between1And10.from(11); // err
797
+ * ```
798
+ *
799
+ * @category Number
800
+ */
801
+ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, parent, (value) => value >= min && value <= max
802
+ ? ok(value)
803
+ : err({ type: "Between", value, min, max }));
804
+ export const formatBetweenError = createTypeErrorFormatter((error) => `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`);
805
+ /** @category Number */
806
+ export const Between1And10 = between(1, 10)(Number);
807
+ /**
808
+ * Literal {@link Type}.
809
+ *
810
+ * https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
811
+ *
812
+ * ### Example
813
+ *
814
+ * ```ts
815
+ * const LiteralHello = literal("Hello");
816
+ * const result = LiteralHello.from("Hello"); // ok("Hello")
817
+ * const errorResult = LiteralHello.from("World"); // err
818
+ * ```
819
+ *
820
+ * TODO: Add JsonValue
821
+ *
822
+ * @category Base Factories
823
+ */
824
+ export const literal = (expected) => {
825
+ const fromUnknown = (value) => value === expected
826
+ ? ok(expected)
827
+ : err({ type: "Literal", value, expected });
828
+ return {
829
+ ...createType("Literal", {
830
+ fromUnknown,
831
+ to: identity,
832
+ fromParent: ok,
833
+ toParent: identity,
834
+ }),
835
+ expected,
836
+ };
837
+ };
838
+ export const formatLiteralError = createTypeErrorFormatter((error) => `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`);
839
+ /**
840
+ * {@link Type} that transforms values between `FromType` and `ToType`.
841
+ *
842
+ * - `fromParent`: Converts `FromType` to `ToType`, may fail.
843
+ * - `toParent`: Converts `ToType` back to `FromType`, must not fail.
844
+ *
845
+ * ### Example
846
+ *
847
+ * // TODO: Examples
848
+ *
849
+ * @category Base Factories
850
+ */
851
+ export const transform = (fromType, toType, fromParent, toParent) => {
852
+ const fromUnknown = (value) => {
853
+ const parentResult = fromType.fromUnknown(value);
854
+ if (!parentResult.ok)
855
+ return parentResult;
856
+ return fromParent(parentResult.value);
857
+ };
858
+ const to = (value) => fromType.to(toParent(value));
859
+ return {
860
+ ...createType("Transform", {
861
+ fromUnknown,
862
+ to,
863
+ fromParent,
864
+ toParent,
865
+ }),
866
+ is: toType.is,
867
+ fromType,
868
+ toType,
869
+ };
870
+ };
871
+ /**
872
+ * Trims leading and trailing whitespace from a string.
873
+ *
874
+ * ### Example
875
+ *
876
+ * ```ts
877
+ * expect(TrimString.from("a ")).toEqual(ok("a"));
878
+ * expect(TrimString.fromParent("a ").value).toEqual("a");
879
+ * ```
880
+ *
881
+ * @category String
882
+ */
883
+ export const TrimString = trim(String);
884
+ /**
885
+ * Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
886
+ *
887
+ * ### Example
888
+ *
889
+ * TODO:
890
+ *
891
+ * @category String
892
+ */
893
+ export const DateIso = transform(Date, DateIsoString, (value) => DateIsoString.fromParent(value.toISOString()), (value) => new globalThis.Date(value));
894
+ /**
895
+ * Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
896
+ *
897
+ * ### Example
898
+ *
899
+ * ```ts
900
+ * NumberFromString.from("42"); // ok(42)
901
+ * NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
902
+ * ```
903
+ *
904
+ * @category Number
905
+ */
906
+ export const NumberFromString = transform(NonEmptyTrimmedString, FiniteNumber, (value) => {
907
+ const result = FiniteNumber.fromParent(globalThis.Number(value));
908
+ if (!result.ok)
909
+ return err({ type: "NumberFromString", value });
910
+ return result;
911
+ }, (num) => num.toString());
912
+ export const formatNumberFromStringError = createTypeErrorFormatter((error) => {
913
+ return `The value ${error.value} could not be converted to a finite number.`;
914
+ });
915
+ /**
916
+ * Array of a specific {@link Type}.
917
+ *
918
+ * Usage:
919
+ *
920
+ * ```ts
921
+ * const NumberArray = array(Number);
922
+ *
923
+ * const result1 = NumberArray.from([1, 2, 3]); // ok([1, 2, 3])
924
+ * const result2 = NumberArray.from(["a", "b"]); // err(...)
925
+ * ```
926
+ *
927
+ * @category Base Factories
928
+ * @category Array
929
+ */
930
+ export const array = (element) => {
931
+ const fromUnknown = (value) => {
932
+ if (!Array.isArray(value)) {
933
+ return err({
934
+ type: "Array",
935
+ value,
936
+ reason: { kind: "NotArray" },
937
+ });
938
+ }
939
+ const result = [];
940
+ for (let i = 0; i < value.length; i++) {
941
+ const elementResult = element.fromUnknown(value[i]);
942
+ if (!elementResult.ok) {
943
+ return err({
944
+ type: "Array",
945
+ value,
946
+ reason: {
947
+ kind: "Element",
948
+ index: i,
949
+ error: elementResult.error,
950
+ },
951
+ });
952
+ }
953
+ result.push(elementResult.value);
954
+ }
955
+ return ok(result);
956
+ };
957
+ const to = (value) => value.map(element.to);
958
+ const fromParent = (value) => {
959
+ const result = [];
960
+ for (let i = 0; i < value.length; i++) {
961
+ const elementResult = element.fromParent(value[i]);
962
+ if (!elementResult.ok) {
963
+ return err({
964
+ type: "Array",
965
+ value,
966
+ reason: {
967
+ kind: "Element",
968
+ index: i,
969
+ error: elementResult.error,
970
+ },
971
+ });
972
+ }
973
+ result.push(elementResult.value);
974
+ }
975
+ return ok(result);
976
+ };
977
+ const toParent = (values) => values.map(element.toParent);
978
+ return {
979
+ ...createType("Array", {
980
+ fromUnknown,
981
+ to,
982
+ fromParent,
983
+ toParent,
984
+ }),
985
+ element,
986
+ };
987
+ };
988
+ export const formatArrayError = (formatTypeError) => createTypeErrorFormatter((error) => {
989
+ switch (error.reason.kind) {
990
+ case "NotArray":
991
+ return `Expected an array but received ${error.value}.`;
992
+ case "Element":
993
+ return `Invalid element at index ${error.reason.index}: ${formatTypeError(error.reason.error)}`;
994
+ }
995
+ });
996
+ /**
997
+ * Record of a key {@link Type} and value {@link Type}.
998
+ *
999
+ * - The input must be a plain object (validated by {@link isPlainObject}).
1000
+ * - Each key is validated/transformed by the `key` Type.
1001
+ * - Each value is validated/transformed by the `value` Type.
1002
+ *
1003
+ * The resulting type is `Readonly<Record<KeyT, ValueT>>`.
1004
+ *
1005
+ * ### Example
1006
+ *
1007
+ * ```ts
1008
+ * const StringToNumberRecord = record(String, Number);
1009
+ *
1010
+ * // ok({ "a": 1, "b": 2 })
1011
+ * StringToNumberRecord.from({ a: 1, b: 2 });
1012
+ *
1013
+ * // err => "Key" because 42 is not a string key
1014
+ * StringToNumberRecord.from({ 42: 1, b: 2 });
1015
+ *
1016
+ * // err => "Value" because "x" is not a number
1017
+ * StringToNumberRecord.from({ a: "x", b: 2 });
1018
+ * ```
1019
+ *
1020
+ * @category Base Factories
1021
+ * @category Object
1022
+ */
1023
+ export const record = (keyType, valueType) => {
1024
+ const fromUnknown = (value) => {
1025
+ if (!isPlainObject(value)) {
1026
+ return err({
1027
+ type: "Record",
1028
+ value,
1029
+ reason: { kind: "NotRecord" },
1030
+ });
1031
+ }
1032
+ const result = {};
1033
+ for (const [rawKey, rawValue] of Object.entries(value)) {
1034
+ const keyResult = keyType.fromUnknown(rawKey);
1035
+ if (!keyResult.ok) {
1036
+ return err({
1037
+ type: "Record",
1038
+ value,
1039
+ reason: { kind: "Key", key: rawKey, error: keyResult.error },
1040
+ });
1041
+ }
1042
+ const valueResult = valueType.fromUnknown(rawValue);
1043
+ if (!valueResult.ok) {
1044
+ return err({
1045
+ type: "Record",
1046
+ value,
1047
+ reason: {
1048
+ kind: "Value",
1049
+ key: rawKey,
1050
+ error: valueResult.error,
1051
+ },
1052
+ });
1053
+ }
1054
+ result[keyResult.value] = valueResult.value;
1055
+ }
1056
+ return ok(result);
1057
+ };
1058
+ const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1059
+ keyType.to(key),
1060
+ valueType.to(val),
1061
+ ]));
1062
+ const fromParent = (value) => {
1063
+ const result = {};
1064
+ for (const [rawKey, rawValue] of Object.entries(value)) {
1065
+ const keyResult = keyType.fromParent(rawKey);
1066
+ if (!keyResult.ok) {
1067
+ return err({
1068
+ type: "Record",
1069
+ value,
1070
+ reason: {
1071
+ kind: "Key",
1072
+ key: rawKey,
1073
+ error: keyResult.error,
1074
+ },
1075
+ });
1076
+ }
1077
+ const valueResult = valueType.fromParent(rawValue);
1078
+ if (!valueResult.ok) {
1079
+ return err({
1080
+ type: "Record",
1081
+ value,
1082
+ reason: {
1083
+ kind: "Value",
1084
+ key: keyResult.value,
1085
+ error: valueResult.error,
1086
+ },
1087
+ });
1088
+ }
1089
+ result[keyResult.value] = valueResult.value;
1090
+ }
1091
+ return ok(result);
1092
+ };
1093
+ const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
1094
+ keyType.toParent(key),
1095
+ valueType.toParent(val),
1096
+ ]));
1097
+ return {
1098
+ ...createType("Record", {
1099
+ fromUnknown,
1100
+ to,
1101
+ fromParent,
1102
+ toParent,
1103
+ }),
1104
+ key: keyType,
1105
+ value: valueType,
1106
+ };
1107
+ };
1108
+ export const formatRecordError = (formatTypeError) => createTypeErrorFormatter((error) => {
1109
+ switch (error.reason.kind) {
1110
+ case "NotRecord":
1111
+ return `Expected a record (plain object) but received ${error.value}.`;
1112
+ case "Key":
1113
+ return `Invalid key ${error.reason.key}: ${formatTypeError(error.reason.error)}`;
1114
+ case "Value":
1115
+ return `Invalid value for key ${error.reason.key}: ${formatTypeError(error.reason.error)}`;
1116
+ }
1117
+ });
1118
+ export function object(props, record) {
1119
+ /* eslint-disable */
1120
+ const propKeys = Object.keys(props);
1121
+ const fromUnknown = (value) => {
1122
+ if (!isPlainObject(value)) {
1123
+ return err({
1124
+ type: record ? "ObjectWithRecord" : "Object",
1125
+ value,
1126
+ reason: { kind: "NotObject" },
1127
+ });
1128
+ }
1129
+ const errors = {};
1130
+ const result = {};
1131
+ for (const key of propKeys) {
1132
+ if (!(key in value) && isOptionalType(props[key])) {
1133
+ continue;
1134
+ }
1135
+ const propResult = props[key].fromUnknown(value[key]);
1136
+ if (!propResult.ok) {
1137
+ errors[key] = propResult.error;
1138
+ }
1139
+ else {
1140
+ result[key] = propResult.value;
1141
+ }
1142
+ }
1143
+ const extraKeys = Object.keys(value).filter((key) => !propKeys.includes(key));
1144
+ if (record) {
1145
+ for (const key of extraKeys) {
1146
+ const keyResult = record.key.fromUnknown(key);
1147
+ if (!keyResult.ok) {
1148
+ return err({
1149
+ type: "ObjectWithRecord",
1150
+ value,
1151
+ reason: {
1152
+ kind: "IndexKey",
1153
+ key,
1154
+ error: keyResult.error,
1155
+ },
1156
+ });
1157
+ }
1158
+ const valueResult = record.value.fromUnknown(value[key]);
1159
+ if (!valueResult.ok) {
1160
+ return err({
1161
+ type: "ObjectWithRecord",
1162
+ value,
1163
+ reason: {
1164
+ kind: "IndexValue",
1165
+ key,
1166
+ error: valueResult.error,
1167
+ },
1168
+ });
1169
+ }
1170
+ result[keyResult.value] = valueResult.value;
1171
+ }
1172
+ }
1173
+ else if (extraKeys.length > 0) {
1174
+ return err({
1175
+ type: "Object",
1176
+ value,
1177
+ reason: { kind: "ExtraKeys", extraKeys },
1178
+ });
1179
+ }
1180
+ if (Object.keys(errors).length > 0) {
1181
+ return err({
1182
+ type: record ? "ObjectWithRecord" : "Object",
1183
+ value,
1184
+ reason: { kind: "Props", errors },
1185
+ });
1186
+ }
1187
+ return ok(result);
1188
+ };
1189
+ const to = (value) => {
1190
+ const entries = [];
1191
+ for (const key of propKeys) {
1192
+ if (!(key in value) && isOptionalType(props[key])) {
1193
+ continue;
1194
+ }
1195
+ entries.push([key, props[key].to(value[key])]);
1196
+ }
1197
+ if (record) {
1198
+ const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
1199
+ for (const [key, val] of recordEntries) {
1200
+ entries.push([record.key.to(key), record.value.to(val)]);
1201
+ }
1202
+ }
1203
+ return Object.fromEntries(entries);
1204
+ };
1205
+ const fromParent = (value) => {
1206
+ const errors = {};
1207
+ const result = {};
1208
+ for (const key of propKeys) {
1209
+ if (!(key in value) && isOptionalType(props[key])) {
1210
+ continue;
1211
+ }
1212
+ const propResult = props[key].fromParent(value[key]);
1213
+ if (!propResult.ok) {
1214
+ errors[key] = propResult.error;
1215
+ }
1216
+ else {
1217
+ result[key] = propResult.value;
1218
+ }
1219
+ }
1220
+ if (record) {
1221
+ for (const [key, val] of Object.entries(value)) {
1222
+ if (!propKeys.includes(key)) {
1223
+ const keyResult = record.key.fromParent(key);
1224
+ if (!keyResult.ok) {
1225
+ return err({
1226
+ type: "ObjectWithRecord",
1227
+ value,
1228
+ reason: { kind: "IndexKey", key, error: keyResult.error },
1229
+ });
1230
+ }
1231
+ const valueResult = record.value.fromParent(val);
1232
+ if (!valueResult.ok) {
1233
+ return err({
1234
+ type: "ObjectWithRecord",
1235
+ value,
1236
+ reason: { kind: "IndexValue", key, error: valueResult.error },
1237
+ });
1238
+ }
1239
+ result[keyResult.value] = valueResult.value;
1240
+ }
1241
+ }
1242
+ }
1243
+ if (Object.keys(errors).length > 0) {
1244
+ return err({
1245
+ type: record ? "ObjectWithRecord" : "Object",
1246
+ value,
1247
+ reason: { kind: "Props", errors },
1248
+ });
1249
+ }
1250
+ return ok(result);
1251
+ };
1252
+ const toParent = (value) => {
1253
+ const entries = [];
1254
+ for (const key of propKeys) {
1255
+ if (!(key in value) && isOptionalType(props[key])) {
1256
+ continue;
1257
+ }
1258
+ entries.push([key, props[key].toParent(value[key])]);
1259
+ }
1260
+ if (record) {
1261
+ const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
1262
+ for (const [key, val] of recordEntries) {
1263
+ entries.push([record.key.toParent(key), record.value.toParent(val)]);
1264
+ }
1265
+ }
1266
+ return Object.fromEntries(entries);
1267
+ };
1268
+ return {
1269
+ ...createType(record ? "ObjectWithRecord" : "Object", {
1270
+ fromUnknown,
1271
+ to,
1272
+ fromParent,
1273
+ toParent,
1274
+ }),
1275
+ props,
1276
+ ...(record ? { record } : {}),
1277
+ };
1278
+ /* eslint-enable */
1279
+ }
1280
+ export const formatObjectError = (formatTypeError) => createTypeErrorFormatter((error) => {
1281
+ switch (error.reason.kind) {
1282
+ case "NotObject":
1283
+ return `Expected a plain object but received ${error.value}`;
1284
+ case "ExtraKeys":
1285
+ return `Unexpected extra keys: ${error.reason.extraKeys.join(", ")}`;
1286
+ case "Props": {
1287
+ const formattedErrors = Object.entries(error.reason.errors)
1288
+ .filter(([, error]) => error !== undefined)
1289
+ .map(([key, error]) => `- ${key}: ${formatTypeError(error)}`)
1290
+ .join("\n");
1291
+ return `Invalid object properties:\n${formattedErrors}`;
1292
+ }
1293
+ }
1294
+ });
1295
+ export const formatObjectWithRecordError = (formatTypeError) => createTypeErrorFormatter((error) => {
1296
+ switch (error.reason.kind) {
1297
+ case "NotObject":
1298
+ return `Expected an object, but received ${error.value}.`;
1299
+ case "Props":
1300
+ return formatObjectError(formatTypeError)({
1301
+ type: "Object",
1302
+ value: error.value,
1303
+ reason: { kind: "Props", errors: error.reason.errors },
1304
+ });
1305
+ case "IndexKey":
1306
+ return `Invalid index key ${error.reason.key}: ${formatTypeError(error.reason.error)}`;
1307
+ case "IndexValue":
1308
+ return `Invalid value at index key ${error.reason.key}: ${formatTypeError(error.reason.error)}`;
1309
+ }
1310
+ });
1311
+ export function union(...args) {
1312
+ /**
1313
+ * Good First Issue:
1314
+ *
1315
+ * 1. Optimize for microbenchmarks:
1316
+ *
1317
+ * - Detect if all arguments are literals and generate a fast-check function.
1318
+ * - This could significantly improve performance for unions of literals.
1319
+ * 2. Enhance tagged union support:
1320
+ *
1321
+ * - Detect if all arguments are objects with the same property but different
1322
+ * literal values (tagged unions).
1323
+ * - Generate a specialized function to improve validation performance for such
1324
+ * cases.
1325
+ */
1326
+ /* eslint-disable */
1327
+ const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
1328
+ const fromUnknown = (value) => {
1329
+ const errors = [];
1330
+ for (const member of members) {
1331
+ const result = member.fromUnknown(value);
1332
+ if (result.ok)
1333
+ return result;
1334
+ errors.push(result.error);
1335
+ }
1336
+ return err({
1337
+ type: "Union",
1338
+ value,
1339
+ errors,
1340
+ });
1341
+ };
1342
+ const to = (value) => {
1343
+ for (const member of members) {
1344
+ if (member.is(value))
1345
+ return member.to(value);
1346
+ }
1347
+ assert(false, "No matching member found in Union Type `to` function");
1348
+ };
1349
+ return {
1350
+ ...createType("Union", {
1351
+ fromUnknown,
1352
+ to,
1353
+ fromParent: fromUnknown,
1354
+ toParent: to,
1355
+ }),
1356
+ members,
1357
+ };
1358
+ /* eslint-enable */
1359
+ }
1360
+ export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
1361
+ const formattedErrors = error.errors
1362
+ .map((e, i) => ` ${i + 1}. ${formatTypeError(e)}`)
1363
+ .join("\n");
1364
+ return `Value ${error.value} does not match any member of the union.\nErrors:\n${formattedErrors}`;
1365
+ });
1366
+ export const isUnionType = (t) => t.name === "Union" && Array.isArray(t.members);
1367
+ /**
1368
+ * Recursive {@link Type}.
1369
+ *
1370
+ * Recursive types can't be inferred, so we must define them using an interface
1371
+ * and `recursive` Type Factory that returns a {@link Type}.
1372
+ *
1373
+ * ### Example
1374
+ *
1375
+ * ```ts
1376
+ * interface Category {
1377
+ * readonly name: string;
1378
+ * readonly subcategories: ReadonlyArray<Category>;
1379
+ * }
1380
+ *
1381
+ * interface CategoryInput {
1382
+ * readonly name: string;
1383
+ * readonly subcategories: ReadonlyArray<CategoryInput>;
1384
+ * }
1385
+ *
1386
+ * type CategoryError = ObjectError<{
1387
+ * readonly name: typeof String.Error;
1388
+ * readonly subcategories: ArrayError<CategoryError>;
1389
+ * }>;
1390
+ *
1391
+ * const Category = recursive(
1392
+ * (): Type<"Object", Category, CategoryInput, CategoryError> =>
1393
+ * object({
1394
+ * name: String,
1395
+ * subcategories: array(Category),
1396
+ * }),
1397
+ * );
1398
+ * ```
1399
+ *
1400
+ * @category Base Factories
1401
+ */
1402
+ export const recursive = (create) => {
1403
+ let type;
1404
+ return {
1405
+ name: "Recursive",
1406
+ from: (value) => {
1407
+ type ??= create();
1408
+ return type.from(value);
1409
+ },
1410
+ fromUnknown: (value) => {
1411
+ type ??= create();
1412
+ return type.fromUnknown(value);
1413
+ },
1414
+ to: (value) => {
1415
+ type ??= create();
1416
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1417
+ return type.to(value);
1418
+ },
1419
+ fromParent: (value) => {
1420
+ type ??= create();
1421
+ return type.fromParent(value);
1422
+ },
1423
+ toParent: (value) => {
1424
+ type ??= create();
1425
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1426
+ return type.toParent(value);
1427
+ },
1428
+ is: (value) => {
1429
+ type ??= create();
1430
+ return type.is(value);
1431
+ },
1432
+ [EvoluTypeSymbol]: true,
1433
+ getParentType: () => {
1434
+ type ??= create();
1435
+ return type;
1436
+ },
1437
+ };
1438
+ };
1439
+ /**
1440
+ * `union(null, T)` {@link Type}.
1441
+ *
1442
+ * ### Example
1443
+ *
1444
+ * ```ts
1445
+ * const NullOrString = nullOr(String);
1446
+ * NullOrString.from("hello"); // ok("hello")
1447
+ * NullOrString.from(null); // ok(null)
1448
+ * NullOrString.from(42); // err(...)
1449
+ * ```
1450
+ *
1451
+ * @category Base Factories
1452
+ */
1453
+ export const nullOr = (type) => union(Null, type);
1454
+ /**
1455
+ * `union(undefined, T)` {@link Type}.
1456
+ *
1457
+ * ### Example
1458
+ *
1459
+ * ```ts
1460
+ * const UndefinedOrString = undefinedOr(String);
1461
+ * UndefinedOrString.from("world"); // ok("world")
1462
+ * UndefinedOrString.from(undefined); // ok()
1463
+ * UndefinedOrString.from(42); // err(...)
1464
+ * ```
1465
+ *
1466
+ * @category Base Factories
1467
+ */
1468
+ export const undefinedOr = (type) => union(Undefined, type);
1469
+ /**
1470
+ * `union(undefined, null, T)` {@link Type}.
1471
+ *
1472
+ * Learn more:
1473
+ * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing
1474
+ *
1475
+ * ### Example
1476
+ *
1477
+ * ```ts
1478
+ * const NullishOrString = nullishOr(String);
1479
+ * NullishOrString.from("test"); // ok("test")
1480
+ * NullishOrString.from(null); // ok(null)
1481
+ * NullishOrString.from(undefined); // ok()
1482
+ * NullishOrString.from(42); // err(...)
1483
+ * ```
1484
+ *
1485
+ * @category Base Factories
1486
+ */
1487
+ export const nullishOr = (type) => union(Undefined, Null, type);
1488
+ /**
1489
+ * Tuple {@link Type}.
1490
+ *
1491
+ * Represents a tuple of specific Types.
1492
+ *
1493
+ * ### Example
1494
+ *
1495
+ * ```ts
1496
+ * const NameAndAge = tuple(NonEmptyTrimmedString, PositiveNumber);
1497
+ *
1498
+ * const result = NameAndAge.from(["Alice", 25]); // ok(["Alice", 25])
1499
+ * const error = NameAndAge.from(["Alice", -10]); // err
1500
+ * ```
1501
+ *
1502
+ * @category Base Factories
1503
+ */
1504
+ export const tuple = (...elements) => {
1505
+ const fromUnknown = (value) => {
1506
+ if (!Array.isArray(value) || value.length !== elements.length) {
1507
+ return err({
1508
+ type: "Tuple",
1509
+ value,
1510
+ reason: { kind: "InvalidLength", expected: elements.length },
1511
+ });
1512
+ }
1513
+ const result = [];
1514
+ for (let i = 0; i < elements.length; i++) {
1515
+ const elementResult = elements[i].fromUnknown(value[i]);
1516
+ if (!elementResult.ok) {
1517
+ return err({
1518
+ type: "Tuple",
1519
+ value,
1520
+ reason: {
1521
+ kind: "Element",
1522
+ index: i,
1523
+ error: elementResult.error,
1524
+ },
1525
+ });
1526
+ }
1527
+ result[i] = elementResult.value;
1528
+ }
1529
+ return ok(result);
1530
+ };
1531
+ const to = (value) => value.map((val, index) => elements[index].to(val));
1532
+ const fromParent = (value) => {
1533
+ if (!Array.isArray(value) || value.length !== elements.length) {
1534
+ return err({
1535
+ type: "Tuple",
1536
+ value,
1537
+ reason: { kind: "InvalidLength", expected: elements.length },
1538
+ });
1539
+ }
1540
+ const result = [];
1541
+ for (let i = 0; i < elements.length; i++) {
1542
+ const elementResult = elements[i].fromParent(value[i]);
1543
+ if (!elementResult.ok) {
1544
+ return err({
1545
+ type: "Tuple",
1546
+ value,
1547
+ reason: {
1548
+ kind: "Element",
1549
+ index: i,
1550
+ error: elementResult.error,
1551
+ },
1552
+ });
1553
+ }
1554
+ result.push(elementResult.value);
1555
+ }
1556
+ return ok(result);
1557
+ };
1558
+ const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
1559
+ return {
1560
+ ...createType("Tuple", {
1561
+ fromUnknown,
1562
+ to,
1563
+ fromParent,
1564
+ toParent,
1565
+ }),
1566
+ elements,
1567
+ };
1568
+ };
1569
+ export const formatTupleError = (formatTypeError) => createTypeErrorFormatter((error) => {
1570
+ switch (error.reason.kind) {
1571
+ case "InvalidLength":
1572
+ return `Expected a tuple of length ${error.reason.expected}, but received ${error.value}.`;
1573
+ case "Element":
1574
+ return `Invalid element at index ${error.reason.index}:\n ${formatTypeError(error.reason.error)}`;
1575
+ }
1576
+ });
1577
+ /**
1578
+ * 64-bit signed integer.
1579
+ *
1580
+ * `Int64` represents a `BigInt` constrained to a 64-bit signed integer range,
1581
+ * which is useful for platforms that do not support the `bigint` type, such as
1582
+ * SQLite.
1583
+ *
1584
+ * Because SQLite lacks a dedicated `bigint` type, it may return `number` or
1585
+ * 'Int64` depending on the stored value or even a wrong value if a platform
1586
+ * wrapper does not support it. A workaround for SQLite is to insert 'Int64`
1587
+ * serialized as a string (SQLite will convert it to int) and manually cast the
1588
+ * result to a string in SQL query and then to `Int64` in JS.
1589
+ *
1590
+ * https://www.sqlite.org/c3ref/int64.html
1591
+ *
1592
+ * @category Number
1593
+ */
1594
+ export const Int64 = brand("Int64", BigInt, (value) => value >= -9223372036854775808n && value <= 9223372036854775807n
1595
+ ? ok(value)
1596
+ : err({ type: "Int64", value }));
1597
+ export const formatInt64Error = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid 64-bit signed integer (Int64).`);
1598
+ export const BigIntFromString = transform(String, BigInt, (value) => trySync(() => globalThis.BigInt(value), () => ({ type: "BigIntFromString", value })), (value) => value.toString());
1599
+ export const formatBigIntFromStringError = createTypeErrorFormatter((error) => `The value ${error.value} could not be converted to a BigInt.`);
1600
+ /**
1601
+ * Stringified {@link Int64}.
1602
+ *
1603
+ * @category Number
1604
+ */
1605
+ export const Int64String = brand("Int64", String, (value) => {
1606
+ const bigint = BigIntFromString.fromParent(value);
1607
+ if (bigint.ok) {
1608
+ const int64 = Int64.fromParent(globalThis.BigInt(value));
1609
+ if (int64.ok)
1610
+ return ok(value);
1611
+ }
1612
+ return err({ type: "Int64String", value });
1613
+ });
1614
+ export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
1615
+ /**
1616
+ * JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
1617
+ * {@link JsonArray}, or {@link JsonObject}.
1618
+ *
1619
+ * @category Base Types
1620
+ */
1621
+ export const JsonValue = recursive(() => union(String, FiniteNumber, Boolean, Null, JsonArray, JsonObject));
1622
+ /**
1623
+ * JSON-compatible array of {@link JsonValue} elements.
1624
+ *
1625
+ * @category Array
1626
+ */
1627
+ export const JsonArray = array(JsonValue);
1628
+ /**
1629
+ * JSON-compatible object with string keys and {@link JsonValue} values.
1630
+ *
1631
+ * @category Object
1632
+ */
1633
+ export const JsonObject = record(String, JsonValue);
1634
+ /**
1635
+ * Transform Type that parses a JSON into a {@link JsonValue} and serializes a
1636
+ * JsonValue back into a JSON string.
1637
+ *
1638
+ * ### Example
1639
+ *
1640
+ * ```ts
1641
+ * JsonValueFromString.from(`{"key":"value"}`); // -> ok({ key: "value" })
1642
+ * JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
1643
+ * ```
1644
+ *
1645
+ * @category String
1646
+ */
1647
+ export const JsonValueFromString = transform(String, JsonValue, (value) => trySync(() => JSON.parse(value), (error) => ({
1648
+ type: `JsonValueFromString`,
1649
+ value,
1650
+ message: globalThis.String(error),
1651
+ })), (value) => JSON.stringify(value));
1652
+ export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
1653
+ /**
1654
+ * JSON-string {@link Type}.
1655
+ *
1656
+ * ### Example
1657
+ *
1658
+ * ```ts
1659
+ * const result = Json.from('{"key":"value"}'); // -> ok('{"key":"value"}')
1660
+ * const error = Json.from("invalid json"); // -> err({ type: "Json", value: "invalid json", message: "Unexpected token i in JSON at position 0" })
1661
+ * ```
1662
+ *
1663
+ * @category String
1664
+ */
1665
+ export const Json = brand("Json", String, (value) => {
1666
+ const result = JsonValueFromString.fromParent(value);
1667
+ if (!result.ok)
1668
+ return err({
1669
+ type: "Json",
1670
+ value,
1671
+ message: result.error.message,
1672
+ });
1673
+ return ok(value);
1674
+ });
1675
+ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
1676
+ /**
1677
+ * Optional {@link Type}.
1678
+ *
1679
+ * Marks a `Type` as **optional**, meaning:
1680
+ *
1681
+ * - If the value is **present**, it must match the given `Type`.
1682
+ * - If the value is **absent**, it is **not included** in the final object.
1683
+ *
1684
+ * This is different from {@link undefinedOr}, which allows explicit `undefined`
1685
+ * but **still requires the key to exist**.
1686
+ *
1687
+ * ### Example:
1688
+ *
1689
+ * ```ts
1690
+ * const Todo = object({
1691
+ * id: TodoId,
1692
+ * title: NonEmptyString1000,
1693
+ * isCompleted: optional(SqliteBoolean),
1694
+ * });
1695
+ * ```
1696
+ */
1697
+ export const optional = (type) => ({
1698
+ ...createType("Optional", {
1699
+ fromUnknown: type.fromUnknown,
1700
+ to: type.to,
1701
+ fromParent: type.fromParent,
1702
+ toParent: type.toParent,
1703
+ }),
1704
+ parent: type,
1705
+ });
1706
+ /** Determines if a given type is an {@link OptionalType}. */
1707
+ export const isOptionalType = (x) => typeof x === "object" && x != null && "name" in x && x.name === "Optional";
1708
+ /**
1709
+ * Creates a partial object type where all properties are optional.
1710
+ *
1711
+ * This is useful when you want to validate an object in which none of the keys
1712
+ * are required, but if they are present they must conform to their
1713
+ * corresponding Types.
1714
+ *
1715
+ * ### Example
1716
+ *
1717
+ * ```ts
1718
+ * const PartialUser = partial({
1719
+ * name: NonEmptyString,
1720
+ * age: PositiveNumber,
1721
+ * });
1722
+ *
1723
+ * // Valid: an empty object is accepted
1724
+ * PartialUser.from({});
1725
+ *
1726
+ * // Valid: when provided, the properties must validate correctly
1727
+ * PartialUser.from({ name: "Alice" });
1728
+ *
1729
+ * // Invalid: if a property is present but fails validation it returns an error
1730
+ * PartialUser.from({ age: -5 });
1731
+ * ```
1732
+ *
1733
+ * @category Object
1734
+ */
1735
+ export const partial = (props) => {
1736
+ const optionalProps = {};
1737
+ for (const key in props) {
1738
+ if (Object.prototype.hasOwnProperty.call(props, key)) {
1739
+ optionalProps[key] = optional(props[key]);
1740
+ }
1741
+ }
1742
+ return object(optionalProps);
1743
+ };
1744
+ /**
1745
+ * Converts each “nullable” property (a union that includes {@link Null}) into an
1746
+ * {@link optional} property. This means consumers can omit the property
1747
+ * entirely, or set it to `null`, or set it to the non-null member of the
1748
+ * union.
1749
+ *
1750
+ * @category Object
1751
+ */
1752
+ export const nullableToOptional = (props) => {
1753
+ const transformedProps = {};
1754
+ for (const key in props) {
1755
+ const type = props[key];
1756
+ if (isUnionType(type)) {
1757
+ const hasNull = type.members.some((m) => m === Null);
1758
+ if (hasNull) {
1759
+ transformedProps[key] = optional(type);
1760
+ continue;
1761
+ }
1762
+ }
1763
+ transformedProps[key] = type;
1764
+ }
1765
+ return object(transformedProps);
1766
+ };
1767
+ /**
1768
+ * Create a new `object` {@link Type} by omitting some keys.
1769
+ *
1770
+ * @category Object
1771
+ */
1772
+ export function omit(objectType, ...keys) {
1773
+ const newProps = {};
1774
+ for (const key in objectType.props) {
1775
+ if (!keys.includes(key)) {
1776
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
1777
+ newProps[key] = objectType.props[key];
1778
+ }
1779
+ }
1780
+ return object(newProps);
1781
+ }
1782
+ /**
1783
+ * Creates a transform Type that serializes a given `Type` into a branded JSON
1784
+ * string. The transformation is reversible, ensuring that we can safely parse
1785
+ * it back.
1786
+ *
1787
+ * ### Example
1788
+ *
1789
+ * ```ts
1790
+ * const Person = object({
1791
+ * name: NonEmptyString50,
1792
+ * age: FiniteNumber,
1793
+ * });
1794
+ * type Person = typeof Person.Type;
1795
+ *
1796
+ * const PersonJson = json(Person, "PersonJson");
1797
+ * // string & Brand<"PersonJson">
1798
+ * type PersonJson = typeof PersonJson.Type;
1799
+ *
1800
+ * // Person -> string & Brand<"PersonJson">
1801
+ * const personJson = PersonJson.from({ name: "Alice", age: 30 });
1802
+ * expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
1803
+ *
1804
+ * // string & Brand<"PersonJson"> -> Person
1805
+ * const person = PersonJson.to(personJson);
1806
+ *
1807
+ * // serialize/parse any JSON value
1808
+ * const AnyJson = json(JsonValue, "AnyJson");
1809
+ * ```
1810
+ */
1811
+ export const json = (type, name) => {
1812
+ const BrandedJsonString = brand(name, String, (value) => {
1813
+ const jsonValue = JsonValueFromString.fromParent(value);
1814
+ if (!jsonValue.ok)
1815
+ return jsonValue;
1816
+ const parsed = type.fromUnknown(jsonValue.value);
1817
+ if (!parsed.ok)
1818
+ return parsed;
1819
+ return ok(value);
1820
+ });
1821
+ return transform(type, BrandedJsonString, (value) => ok(JSON.stringify(value)),
1822
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
1823
+ (value) => JSON.parse(value));
1824
+ };
1825
+ /**
1826
+ * Creates a unified error formatter that handles both Evolu Type's built-in
1827
+ * {@link TypeErrors} and custom errors. It also lets us override the default
1828
+ * formatting for specific errors.
1829
+ *
1830
+ * If you prefer not to reuse any built-in error formatters, you can write your
1831
+ * own `formatTypeError` function from scratch.
1832
+ *
1833
+ * ### Examples
1834
+ *
1835
+ * ```ts
1836
+ * const formatError = createFormatTypeError();
1837
+ * console.log(formatError({ type: "String", value: 42 }));
1838
+ * // "A value 42 is not a string."
1839
+ * ```
1840
+ *
1841
+ * A custom `formatTypeError` function:
1842
+ *
1843
+ * ```ts
1844
+ * type AppErrors =
1845
+ * | ValidMutationSizeError
1846
+ * | StringError
1847
+ * | MinLengthError
1848
+ * | MaxLengthError
1849
+ * | NullError
1850
+ * | IdError
1851
+ * | TrimmedError
1852
+ * | MnemonicError
1853
+ * | LiteralError
1854
+ * // Composite errors
1855
+ * | ObjectError<Record<string, AppErrors>>
1856
+ * | UnionError<AppErrors>;
1857
+ *
1858
+ * const formatTypeError: TypeErrorFormatter<AppErrors> = (error) => {
1859
+ * // In the real code, we would use the createTypeErrorFormatter helper
1860
+ * // that safely stringifies error value.
1861
+ * switch (error.type) {
1862
+ * case "Id":
1863
+ * return `Invalid Id on table: ${error.table}.`;
1864
+ * case "MaxLength":
1865
+ * return `Max length is ${error.max}.`;
1866
+ * case "MinLength":
1867
+ * return `Min length is ${error.min}.`;
1868
+ * case "Mnemonic":
1869
+ * return `Invalid mnemonic: ${String(error.value)}`;
1870
+ * case "Null":
1871
+ * return `Not null`;
1872
+ * case "String":
1873
+ * // We can reuse existing formatter.
1874
+ * return formatStringError(error);
1875
+ * case "Trimmed":
1876
+ * return "Value is not trimmed.";
1877
+ * case "ValidMutationSize":
1878
+ * return "A developer made an error, this should not happen.";
1879
+ * case "Literal":
1880
+ * return formatLiteralError(error);
1881
+ * // Composite Types
1882
+ * case "Union":
1883
+ * return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
1884
+ * case "Object": {
1885
+ * if (
1886
+ * error.reason.kind === "ExtraKeys" ||
1887
+ * error.reason.kind === "NotObject"
1888
+ * )
1889
+ * return "A developer made an error, this should not happen.";
1890
+ * const firstError = Object.values(error.reason.errors).find(
1891
+ * (e) => e !== undefined,
1892
+ * )!;
1893
+ * return formatTypeError(firstError);
1894
+ * }
1895
+ * }
1896
+ * };
1897
+ * ```
1898
+ *
1899
+ * @category Utilities
1900
+ */
1901
+ export const createFormatTypeError = (extraFormatter) => {
1902
+ const formatTypeError = (error) => {
1903
+ const extraMessage = extraFormatter?.(error);
1904
+ if (extraMessage != null)
1905
+ return extraMessage;
1906
+ error = error;
1907
+ switch (error.type) {
1908
+ case "String":
1909
+ return formatStringError(error);
1910
+ case "Number":
1911
+ return formatNumberError(error);
1912
+ case "BigInt":
1913
+ return formatBigIntError(error);
1914
+ case "Boolean":
1915
+ return formatBooleanError(error);
1916
+ case "Undefined":
1917
+ return formatUndefinedError(error);
1918
+ case "Null":
1919
+ return formatNullError(error);
1920
+ case "Function":
1921
+ return formatFunctionError(error);
1922
+ case "Uint8Array":
1923
+ return formatUint8ArrayError(error);
1924
+ case "InstanceOf":
1925
+ return formatInstanceOfError(error);
1926
+ case "EvoluType":
1927
+ return formatIsTypeError(error);
1928
+ case "CurrencyCode":
1929
+ return formatCurrencyCodeError(error);
1930
+ case "DateIsoString":
1931
+ return formatDateIsoStringError(error);
1932
+ case "Trimmed":
1933
+ return formatTrimmedError(error);
1934
+ case "MinLength":
1935
+ return formatMinLengthError(error);
1936
+ case "MaxLength":
1937
+ return formatMaxLengthError(error);
1938
+ case "Length":
1939
+ return formatLengthError(error);
1940
+ case "Mnemonic":
1941
+ return formatMnemonicError(error);
1942
+ case "Regex":
1943
+ return formatRegexError(error);
1944
+ case "Id":
1945
+ return formatIdError(error);
1946
+ case "Positive":
1947
+ return formatPositiveError(error);
1948
+ case "Negative":
1949
+ return formatNegativeError(error);
1950
+ case "NonPositive":
1951
+ return formatNonPositiveError(error);
1952
+ case "NonNegative":
1953
+ return formatNonNegativeError(error);
1954
+ case "Int":
1955
+ return formatIntError(error);
1956
+ case "GreaterThan":
1957
+ return formatGreaterThanError(error);
1958
+ case "LessThan":
1959
+ return formatLessThanError(error);
1960
+ case "GreaterThanOrEqualTo":
1961
+ return formatGreaterThanOrEqualToError(error);
1962
+ case "LessThanOrEqualTo":
1963
+ return formatLessThanOrEqualToError(error);
1964
+ case "NonNaN":
1965
+ return formatNonNaNError(error);
1966
+ case "Finite":
1967
+ return formatFiniteError(error);
1968
+ case "MultipleOf":
1969
+ return formatMultipleOfError(error);
1970
+ case "Between":
1971
+ return formatBetweenError(error);
1972
+ case "Literal":
1973
+ return formatLiteralError(error);
1974
+ case "Int64":
1975
+ return formatInt64Error(error);
1976
+ case "BigIntFromString":
1977
+ return formatBigIntFromStringError(error);
1978
+ case "Int64String":
1979
+ return formatInt64StringError(error);
1980
+ case "JsonValueFromString":
1981
+ return formatJsonValueFromStringError(error);
1982
+ case "Json":
1983
+ return formatJsonError(error);
1984
+ // Composite Types
1985
+ case "SimplePassword":
1986
+ return formatSimplePasswordError(formatTypeError)(error);
1987
+ case "Array":
1988
+ return formatArrayError(formatTypeError)(error);
1989
+ case "Record":
1990
+ return formatRecordError(formatTypeError)(error);
1991
+ case "Object":
1992
+ return formatObjectError(formatTypeError)(error);
1993
+ case "ObjectWithRecord":
1994
+ return formatObjectWithRecordError(formatTypeError)(error);
1995
+ case "Union":
1996
+ return formatUnionError(formatTypeError)(error);
1997
+ case "Tuple":
1998
+ return formatTupleError(formatTypeError)(error);
1999
+ }
2000
+ };
2001
+ return formatTypeError;
2002
+ };