@evolu/common 5.4.8 → 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
package/src/Type.ts ADDED
@@ -0,0 +1,3978 @@
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
+
73
+ import * as bip39 from "@scure/bip39";
74
+ import { wordlist } from "@scure/bip39/wordlists/english";
75
+ import { assert } from "./Assert.js";
76
+ import { identity } from "./Function.js";
77
+ import { NanoIdLibDep } from "./NanoId.js";
78
+ import { isPlainObject } from "./Object.js";
79
+ import { Err, err, Ok, ok, Result, trySync } from "./Result.js";
80
+ import { safelyStringifyUnknownValue } from "./String.js";
81
+ import type { Brand, Literal, Simplify, WidenLiteral } from "./Types.js";
82
+ import { IntentionalNever } from "./Types.js";
83
+
84
+ export interface Type<
85
+ Name extends TypeName,
86
+ /** The type this Type resolves to. */
87
+ T,
88
+ /** The type expected by `from` and `fromUnknown`. */
89
+ Input,
90
+ /** The specific error introduced by this Type. */
91
+ Error extends TypeError = never,
92
+ /** The parent type. */
93
+ Parent = T,
94
+ /** The parent's error. */
95
+ ParentError extends TypeError = Error,
96
+ > {
97
+ readonly name: Name;
98
+
99
+ /**
100
+ * Creates `T` from an `Input` value.
101
+ *
102
+ * This is useful when we have a typed value.
103
+ *
104
+ * `from` is a typed alias of `fromUnknown`.
105
+ */
106
+ readonly from: (value: Input) => Result<T, ParentError | Error>;
107
+
108
+ /**
109
+ * Creates `T` from an unknown value.
110
+ *
111
+ * This is useful when a value is unknown.
112
+ */
113
+ readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
114
+
115
+ /**
116
+ * The opposite of `from` and `fromUnknown`.
117
+ *
118
+ * This is useful to transform `T` back to its `Input` representation.
119
+ *
120
+ * For `refine`, it only removes the brand. For `transform`, it changes value.
121
+ */
122
+ readonly to: (value: T) => Input;
123
+
124
+ /**
125
+ * Creates `T` from `Parent` type.
126
+ *
127
+ * This function skips parent Types validations/transformations when we have
128
+ * already partially validated/transformed value.
129
+ *
130
+ * For example, `TrimString.from` checks whether a value is a string and trims
131
+ * it. If we only want to trim a string, we can use `fromParent`.
132
+ *
133
+ * ### Example
134
+ *
135
+ * ```ts
136
+ * // string & Brand<"Trimmed">
137
+ * const value = TrimString.fromParent("a ").value; // as efficient as foo.trim()
138
+ * ```
139
+ */
140
+ readonly fromParent: (value: Parent) => Result<T, Error>;
141
+
142
+ /** The opposite of `fromParent`. */
143
+ readonly toParent: (value: T) => Parent;
144
+
145
+ /**
146
+ * A **type guard** that checks whether an unknown value satisfies the
147
+ * {@link Type}.
148
+ *
149
+ * ### Example
150
+ *
151
+ * ```ts
152
+ * const value: unknown = "hello";
153
+ * if (String.is(value)) {
154
+ * // TypeScript now knows `value` is a `string` here.
155
+ * console.log("This is a valid string!");
156
+ * }
157
+ *
158
+ * const strings: unknown[] = [1, "hello", true, "world"];
159
+ * const filteredStrings = strings.filter(String.is);
160
+ *
161
+ * console.log(filteredStrings); // ["hello", "world"]
162
+ * ```
163
+ */
164
+ readonly is: (value: unknown) => value is T;
165
+
166
+ readonly [EvoluTypeSymbol]: true;
167
+
168
+ /**
169
+ * The type this Type resolves to.
170
+ *
171
+ * ### Example
172
+ *
173
+ * ```ts
174
+ * type String = typeof String.Type;
175
+ * ```
176
+ */
177
+ readonly Type: T;
178
+
179
+ /**
180
+ * The type expected by `from` and `fromUnknown`.
181
+ *
182
+ * ### Example
183
+ *
184
+ * ```ts
185
+ * type StringInput = typeof String.Input;
186
+ * ```
187
+ */
188
+ readonly Input: Input;
189
+
190
+ /**
191
+ * The specific error introduced by this Type.
192
+ *
193
+ * ### Example
194
+ *
195
+ * ```ts
196
+ * type StringError = typeof String.Error;
197
+ * ```
198
+ */
199
+ readonly Error: Error;
200
+
201
+ /**
202
+ * The parent type.
203
+ *
204
+ * ### Example
205
+ *
206
+ * ```ts
207
+ * type StringParent = typeof String.Parent;
208
+ * ```
209
+ */
210
+ readonly Parent: Parent;
211
+
212
+ /**
213
+ * The parent's error.
214
+ *
215
+ * ### Example
216
+ *
217
+ * ```ts
218
+ * type StringParentError = typeof String.ParentError;
219
+ * ```
220
+ */
221
+ readonly ParentError: ParentError;
222
+
223
+ /**
224
+ * Error | ParentError
225
+ *
226
+ * ### Example
227
+ *
228
+ * ```ts
229
+ * type StringParentErrors = typeof String.Errors;
230
+ * ```
231
+ */
232
+ readonly Errors: Error | ParentError;
233
+ }
234
+
235
+ /**
236
+ * Unique identifier for a {@link Type}.
237
+ *
238
+ * @category Utilities
239
+ */
240
+ export type TypeName = Capitalize<string>;
241
+
242
+ export interface TypeError<Name extends TypeName = TypeName> {
243
+ readonly type: Name;
244
+
245
+ /**
246
+ * The value that was received and caused the error. Provides additional
247
+ * context for debugging and validation feedback.
248
+ */
249
+ readonly value: unknown;
250
+ }
251
+
252
+ export interface TypeErrorWithReason<
253
+ Name extends TypeName = TypeName,
254
+ Reason extends { readonly kind: Capitalize<string> } = {
255
+ readonly kind: Capitalize<string>;
256
+ },
257
+ > extends TypeError<Name> {
258
+ /** The detailed reason for the error, represented as a tagged union. */
259
+ readonly reason: Reason;
260
+ }
261
+
262
+ export type AnyType = Type<any, any, any, any, any, any>;
263
+
264
+ export type InferName<A extends AnyType> =
265
+ A extends Type<infer Name, any, any, any, any, any> ? Name : never;
266
+
267
+ export type InferType<A extends AnyType> =
268
+ A extends Type<any, infer T, any, any, any, any> ? T : never;
269
+
270
+ export type InferInput<A extends AnyType> =
271
+ A extends Type<any, any, infer Input, any, any, any> ? Input : never;
272
+
273
+ export type InferError<A extends AnyType> =
274
+ A extends Type<any, any, any, infer Error, any, any> ? Error : never;
275
+
276
+ export type InferParent<A extends AnyType> =
277
+ A extends Type<any, any, any, any, infer Parent, any> ? Parent : never;
278
+
279
+ export type InferParentError<A extends AnyType> =
280
+ A extends Type<any, any, any, any, any, infer ParentError>
281
+ ? ParentError
282
+ : never;
283
+
284
+ export type InferErrors<T extends AnyType> =
285
+ T extends Type<any, any, any, infer Error, any, infer ParentError>
286
+ ? Error | ParentError
287
+ : never;
288
+
289
+ const EvoluTypeSymbol = Symbol("evolu.Type");
290
+
291
+ /**
292
+ * Checks if the given value is an {@link Type}.
293
+ *
294
+ * @category Utilities
295
+ */
296
+ export const isType = (value: unknown): value is AnyType =>
297
+ typeof value === "object" && value !== null && EvoluTypeSymbol in value;
298
+
299
+ const createType = <
300
+ Name extends TypeName,
301
+ T,
302
+ Input,
303
+ Error extends TypeError = never,
304
+ Parent = T,
305
+ ParentError extends TypeError = never,
306
+ >(
307
+ name: Name,
308
+ definition: Omit<
309
+ Type<Name, T, Input, Error, Parent, ParentError>,
310
+ | "name"
311
+ | "is"
312
+ | "from"
313
+ | typeof EvoluTypeSymbol
314
+ | "Type"
315
+ | "Input"
316
+ | "Error"
317
+ | "Parent"
318
+ | "ParentError"
319
+ | "Errors"
320
+ >,
321
+ ): Type<Name, T, Input, Error, Parent, ParentError> => ({
322
+ ...definition,
323
+ name,
324
+ is: (value: unknown): value is T => definition.fromUnknown(value).ok,
325
+ from: definition.fromUnknown,
326
+ [EvoluTypeSymbol]: true,
327
+ Type: undefined as unknown as T,
328
+ Input: undefined as unknown as Input,
329
+ Error: undefined as unknown as Error,
330
+ Parent: undefined as unknown as Parent,
331
+ ParentError: undefined as unknown as ParentError,
332
+ Errors: undefined as unknown as Error | ParentError,
333
+ });
334
+
335
+ /**
336
+ * Creates a formatter function for {@link TypeError}.
337
+ *
338
+ * The formatter generates human-readable error messages using a custom
339
+ * formatting function and a safely stringified error value.
340
+ *
341
+ * ### Example
342
+ *
343
+ * ```ts
344
+ * const formatStringError = createTypeErrorFormatter<StringError>(
345
+ * (value) => `A value ${value} is not a string.`,
346
+ * );
347
+ * ```
348
+ *
349
+ * @category Utilities
350
+ */
351
+ export const createTypeErrorFormatter =
352
+ <Error extends TypeError>(
353
+ format: (
354
+ error: Simplify<Omit<Error, "value"> & { value: string }>,
355
+ ) => string,
356
+ ): TypeErrorFormatter<Error> =>
357
+ (error) =>
358
+ format({ ...error, value: safelyStringifyUnknownValue(error.value) });
359
+
360
+ export type TypeErrorFormatter<Error extends TypeError> = (
361
+ error: Error,
362
+ ) => string;
363
+
364
+ /**
365
+ * Base {@link Type}.
366
+ *
367
+ * A Base Type validates that a value conforms to a specific TypeScript type.
368
+ * Unlike refinements or transformations, Base Types establish the fundamental
369
+ * shape of a value before any branding or transformation occurs.
370
+ *
371
+ * - To **refine** a Base Type further, use the {@link brand} Type Factory.
372
+ * - To **transform** a Base Type into a different representation, use the
373
+ * {@link transform} Type Factory.
374
+ *
375
+ * ### Example
376
+ *
377
+ * ```ts
378
+ * const String = base("String", (value) =>
379
+ * typeof value === "string"
380
+ * ? ok(value)
381
+ * : err<StringError>({ type: "String", value }),
382
+ * );
383
+ *
384
+ * interface StringError extends TypeError<"String"> {}
385
+ *
386
+ * const formatStringError = createTypeErrorFormatter<StringError>(
387
+ * (error) => `A value ${error.value} is not a string`,
388
+ * );
389
+ * ```
390
+ *
391
+ * @category Base Factories
392
+ */
393
+ export const base = <Name extends TypeName, T, Error extends TypeError>(
394
+ name: Name,
395
+ fromUnknown: (value: unknown) => Result<T, Error>,
396
+ ): Type<Name, T, T, Error> =>
397
+ createType(name, {
398
+ fromUnknown,
399
+ to: identity,
400
+ fromParent: ok<T>, // `fromParent` relies on types, so it can't fail for the Base Type
401
+ toParent: identity,
402
+ });
403
+
404
+ /**
405
+ * Creates a formatter function for a base {@link TypeError}.
406
+ *
407
+ * This formatter is specifically for Base Types that only need a simple error
408
+ * message indicating that the value is not of the expected type.
409
+ *
410
+ * ### Example
411
+ *
412
+ * ```ts
413
+ * export const formatStringError =
414
+ * createBaseTypeErrorFormatter<StringError>();
415
+ * ```
416
+ *
417
+ * @category Utilities
418
+ */
419
+ export const createBaseTypeErrorFormatter = <
420
+ Error extends TypeError,
421
+ >(): TypeErrorFormatter<Error> =>
422
+ createTypeErrorFormatter<Error>(
423
+ (error) => `A value ${error.value} is not a ${error.type.toLowerCase()}.`,
424
+ );
425
+
426
+ /** @category Base Types */
427
+ export const Unknown = base<"Unknown", unknown, never>("Unknown", ok);
428
+
429
+ /**
430
+ * @category Base Types
431
+ * @category String
432
+ */
433
+ export const String = base("String", (value) =>
434
+ typeof value === "string"
435
+ ? ok(value)
436
+ : err<StringError>({ type: "String", value }),
437
+ );
438
+
439
+ export interface StringError extends TypeError<"String"> {}
440
+
441
+ export const formatStringError = createBaseTypeErrorFormatter<StringError>();
442
+
443
+ /** @category Base Types */
444
+ export const Number = base("Number", (value) =>
445
+ typeof value === "number"
446
+ ? ok(value)
447
+ : err<NumberError>({ type: "Number", value }),
448
+ );
449
+
450
+ export interface NumberError extends TypeError<"Number"> {}
451
+
452
+ export const formatNumberError = createBaseTypeErrorFormatter<NumberError>();
453
+
454
+ /** @category Base Types */
455
+ export const BigInt = base("BigInt", (value) =>
456
+ typeof value === "bigint"
457
+ ? ok(value)
458
+ : err<BigIntError>({ type: "BigInt", value }),
459
+ );
460
+
461
+ export interface BigIntError extends TypeError<"BigInt"> {}
462
+
463
+ export const formatBigIntError = createBaseTypeErrorFormatter<BigIntError>();
464
+
465
+ /** @category Base Types */
466
+ export const Boolean = base("Boolean", (value) =>
467
+ typeof value === "boolean"
468
+ ? ok(value)
469
+ : err<BooleanError>({ type: "Boolean", value }),
470
+ );
471
+
472
+ export interface BooleanError extends TypeError<"Boolean"> {}
473
+
474
+ export const formatBooleanError = createBaseTypeErrorFormatter<BooleanError>();
475
+
476
+ /** @category Base Types */
477
+ export const Undefined = base("Undefined", (value) =>
478
+ value === undefined
479
+ ? ok(value)
480
+ : err<UndefinedError>({ type: "Undefined", value }),
481
+ );
482
+
483
+ export interface UndefinedError extends TypeError<"Undefined"> {}
484
+
485
+ export const formatUndefinedError =
486
+ createBaseTypeErrorFormatter<UndefinedError>();
487
+
488
+ /** @category Base Types */
489
+ export const Null = base("Null", (value) =>
490
+ value === null ? ok(value) : err<NullError>({ type: "Null", value }),
491
+ );
492
+
493
+ export interface NullError extends TypeError<"Null"> {}
494
+
495
+ export const formatNullError = createBaseTypeErrorFormatter<NullError>();
496
+
497
+ /** @category Base Types */
498
+ export const Function = base("Function", (value) =>
499
+ typeof value === "function"
500
+ ? ok(value)
501
+ : err<FunctionError>({ type: "Function", value }),
502
+ );
503
+
504
+ export interface FunctionError extends TypeError<"Function"> {}
505
+
506
+ export const formatFunctionError =
507
+ createBaseTypeErrorFormatter<FunctionError>();
508
+
509
+ /** @category Base Types */
510
+ export const Uint8Array = base("Uint8Array", (value) =>
511
+ value instanceof globalThis.Uint8Array
512
+ ? ok(value)
513
+ : err<Uint8ArrayError>({ type: "Uint8Array", value }),
514
+ );
515
+
516
+ export interface Uint8ArrayError extends TypeError<"Uint8Array"> {}
517
+
518
+ export const formatUint8ArrayError =
519
+ createBaseTypeErrorFormatter<Uint8ArrayError>();
520
+
521
+ /**
522
+ * `instanceof` {@link Type}.
523
+ *
524
+ * Ensures that a value is an instance of the given class constructor.
525
+ *
526
+ * ### Example
527
+ *
528
+ * ```ts
529
+ * class User {
530
+ * constructor(public name: string) {}
531
+ * }
532
+ *
533
+ * const UserInstance = instanceOf(User);
534
+ *
535
+ * const result = UserInstance.from(new User("Alice")); // ok
536
+ * const error = UserInstance.from({}); // err
537
+ * ```
538
+ *
539
+ * @category Base Factories
540
+ */
541
+ export const instanceOf = <T extends abstract new (...args: any) => any>(
542
+ ctor: T,
543
+ ): InstanceOfType<T> => ({
544
+ ...base("InstanceOf", (value) =>
545
+ value instanceof ctor
546
+ ? ok(value)
547
+ : err<InstanceOfError>({ type: "InstanceOf", value, ctor: ctor.name }),
548
+ ),
549
+ ctor,
550
+ });
551
+
552
+ export interface InstanceOfError extends TypeError<"InstanceOf"> {
553
+ readonly ctor: string;
554
+ }
555
+
556
+ export interface InstanceOfType<T extends abstract new (...args: any) => any>
557
+ extends Type<
558
+ "InstanceOf",
559
+ InstanceType<T>,
560
+ InstanceType<T>,
561
+ InstanceOfError
562
+ > {
563
+ ctor: T;
564
+ }
565
+
566
+ export const formatInstanceOfError = createTypeErrorFormatter<InstanceOfError>(
567
+ (error) => `Value ${error.value} is not an instance of ${error.ctor}`,
568
+ );
569
+
570
+ /**
571
+ * JavaScript Date.
572
+ *
573
+ * @category Base Types
574
+ */
575
+ export const Date = instanceOf(globalThis.Date);
576
+
577
+ /**
578
+ * Validates that an unknown value is an Evolu {@link Type} (i.e., satisfies
579
+ * `AnyType`).
580
+ *
581
+ * ### Example
582
+ *
583
+ * ```ts
584
+ * const result = EvoluType.from(String); // ok(String)
585
+ * const error = EvoluType.from("not a Type"); // err
586
+ * ```
587
+ */
588
+ // TODO: Rename to TypeInstance or something like that.
589
+ export const EvoluType = base("EvoluType", (value) =>
590
+ isType(value)
591
+ ? ok(value)
592
+ : err<EvoluTypeError>({
593
+ type: "EvoluType",
594
+ value,
595
+ }),
596
+ );
597
+
598
+ export interface EvoluTypeError extends TypeError<"EvoluType"> {}
599
+
600
+ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
601
+ (error) => `Value ${error.value} is not a valid Evolu Type.`,
602
+ );
603
+
604
+ /**
605
+ * Branded {@link Type}.
606
+ *
607
+ * The `brand` Type Factory takes the name of a new {@link Brand}, a parent Type
608
+ * to be branded, and the optional `refine` function for additional constraint.
609
+ *
610
+ * If the `refine` function is omited, TODO:
611
+ *
612
+ * ### Examples
613
+ *
614
+ * A simple `CurrencyCode` Type:
615
+ *
616
+ * ```ts
617
+ * const CurrencyCode = brand("CurrencyCode", String, (value) =>
618
+ * /^[A-Z]{3}$/.test(value)
619
+ * ? ok(value)
620
+ * : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
621
+ * );
622
+ *
623
+ * // string & Brand<"CurrencyCode">
624
+ * type CurrencyCode = typeof CurrencyCode.Type;
625
+ *
626
+ * interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
627
+ *
628
+ * const formatCurrencyCodeError =
629
+ * createTypeErrorFormatter<CurrencyCodeError>(
630
+ * (error) => `Invalid currency code: ${error.value}`,
631
+ * );
632
+ *
633
+ * // Usage
634
+ * const result = CurrencyCode.from("USD");
635
+ * if (result.ok) {
636
+ * console.log("Valid currency code:", result.value);
637
+ * } else {
638
+ * console.error(formatCurrencyCodeError(result.error));
639
+ * }
640
+ * ```
641
+ *
642
+ * Often, we want to make a branded Type reusable. For example, instead of
643
+ * `TrimmedString`, we want the `trimmed` Type Factory:
644
+ *
645
+ * ```ts
646
+ * const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
647
+ * parent,
648
+ * ) =>
649
+ * brand("Trimmed", parent, (value) =>
650
+ * value.trim().length === value.length
651
+ * ? ok(value)
652
+ * : err<TrimmedError>({ type: "Trimmed", value }),
653
+ * );
654
+ *
655
+ * interface TrimmedError extends TypeError<"Trimmed"> {}
656
+ *
657
+ * const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
658
+ * (error) => `A value ${error.value} is not trimmed`,
659
+ * );
660
+ *
661
+ * const TrimmedString = trimmed(String);
662
+ *
663
+ * // string & Brand<"Trimmed">
664
+ * type TrimmedString = typeof TrimmedString.Type;
665
+ *
666
+ * const TrimmedNote = trimmed(Note);
667
+ * ```
668
+ *
669
+ * As noted earlier, the `refine` function is optional. That's useful to add
670
+ * semantic meaning to the existing Type without altering its functionality:
671
+ *
672
+ * ```ts
673
+ * const SimplePassword = brand(
674
+ * "SimplePassword",
675
+ * minLength(8)(maxLength(64)(TrimmedString)),
676
+ * );
677
+ * // string & Brand<"Trimmed"> & Brand<"MinLength8"> & Brand<"MaxLength64"> & Brand<"SimplePassword">
678
+ * type SimplePassword = typeof SimplePassword.Type;
679
+ * ```
680
+ *
681
+ * We can use `brand` to enforce valid object as well:
682
+ *
683
+ * ```ts
684
+ * const Form = object({
685
+ * password: SimplePassword,
686
+ * confirmPassword: SimplePassword,
687
+ * });
688
+ *
689
+ * const ValidForm = brand("Valid", Form, (value) => {
690
+ * if (value.password !== value.confirmPassword)
691
+ * return err<ValidFormError>({
692
+ * type: "ValidForm",
693
+ * value,
694
+ * reason: { kind: "PasswordMismatch" },
695
+ * });
696
+ * return ok(value);
697
+ * });
698
+ * type ValidForm = typeof ValidForm.Type;
699
+ *
700
+ * interface ValidFormError extends TypeError<"ValidForm"> {
701
+ * readonly reason: { kind: "PasswordMismatch" };
702
+ * }
703
+ *
704
+ * const result = ValidForm.from({
705
+ * password: "abcde123",
706
+ * confirmPassword: "bbcde123",
707
+ * });
708
+ *
709
+ * const safeForm = (_form: ValidForm) => {
710
+ * //
711
+ * };
712
+ *
713
+ * if (result.ok) {
714
+ * safeForm(result.value);
715
+ * }
716
+ *
717
+ * expect(result).toEqual(
718
+ * err({
719
+ * type: "ValidForm",
720
+ * value: {
721
+ * confirmPassword: "bbcde123",
722
+ * password: "abcde123",
723
+ * },
724
+ * reason: {
725
+ * kind: "PasswordMismatch",
726
+ * },
727
+ * }),
728
+ * );
729
+ * ```
730
+ *
731
+ * @category Base Factories
732
+ */
733
+ export function brand<
734
+ Name extends TypeName,
735
+ ParentType extends AnyType,
736
+ Parent = InferType<ParentType>,
737
+ RefineError extends TypeError = never,
738
+ >(
739
+ name: Name,
740
+ parent: ParentType,
741
+ refine: (value: Parent) => Result<Parent, RefineError>,
742
+ ): BrandType<ParentType, Name, RefineError, InferErrors<ParentType>>;
743
+
744
+ export function brand<Name extends TypeName, ParentType extends AnyType>(
745
+ name: Name,
746
+ parent: ParentType,
747
+ ): BrandType<
748
+ ParentType,
749
+ Name,
750
+ BrandWithoutRefineError<Name, InferErrors<ParentType>>
751
+ >;
752
+
753
+ export function brand<
754
+ Name extends TypeName,
755
+ ParentType extends AnyType,
756
+ Parent = InferType<ParentType>,
757
+ RefineError extends TypeError = never,
758
+ >(
759
+ name: Name,
760
+ parent: ParentType,
761
+ refine?: (value: Parent) => Result<Parent, RefineError>,
762
+ ): BrandType<ParentType, Name, RefineError, InferErrors<ParentType>> {
763
+ const fromUnknown = refine
764
+ ? (value: unknown) => {
765
+ const parentResult = parent.fromUnknown(value);
766
+ if (!parentResult.ok) return parentResult;
767
+ return refine(parentResult.value as IntentionalNever);
768
+ }
769
+ : (value: unknown) => {
770
+ const parentResult = parent.fromUnknown(value);
771
+ if (!parentResult.ok)
772
+ return err<BrandWithoutRefineError<Name, IntentionalNever>>({
773
+ type: name,
774
+ value,
775
+ parentError: parentResult.error as IntentionalNever,
776
+ });
777
+ return ok(parentResult.value);
778
+ };
779
+
780
+ return {
781
+ ...createType("Brand", {
782
+ fromUnknown,
783
+ to: identity,
784
+ fromParent: (refine ?? ok) as IntentionalNever,
785
+ toParent: identity,
786
+ }),
787
+ brand: name,
788
+ parentType: parent,
789
+ };
790
+ }
791
+
792
+ export interface BrandType<
793
+ ParentType extends AnyType,
794
+ Name extends TypeName,
795
+ Error extends TypeError = never,
796
+ ParentError extends TypeError = never,
797
+ > extends Type<
798
+ "Brand",
799
+ InferType<ParentType> & Brand<Name>,
800
+ InferInput<ParentType>,
801
+ Error,
802
+ InferType<ParentType>,
803
+ ParentError
804
+ > {
805
+ readonly brand: Name;
806
+ readonly parentType: ParentType;
807
+ }
808
+
809
+ export interface BrandWithoutRefineError<
810
+ Name extends TypeName,
811
+ ParentError extends TypeError,
812
+ > extends TypeError<Name> {
813
+ readonly parentError: ParentError;
814
+ }
815
+
816
+ /**
817
+ * A three-letter ISO 4217 currency code (e.g., USD, EUR).
818
+ *
819
+ * @category String
820
+ */
821
+ export const CurrencyCode = brand("CurrencyCode", String, (value) =>
822
+ /^[A-Z]{3}$/.test(value)
823
+ ? ok(value)
824
+ : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
825
+ );
826
+
827
+ export type CurrencyCode = typeof CurrencyCode.Type;
828
+
829
+ export interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
830
+
831
+ export const formatCurrencyCodeError =
832
+ createTypeErrorFormatter<CurrencyCodeError>(
833
+ (error) => `Invalid currency code: ${error.value}`,
834
+ );
835
+
836
+ /**
837
+ * ISO 8601 date-time string.
838
+ *
839
+ * This {@link Type} represents a date-time string that follows the ISO 8601
840
+ * format and is compatible with SQLite, which lacks a native date type and
841
+ * relies on ISO 8601 strings for sorting. Enforcing a 24-character format
842
+ * ensures correct lexicographic ordering.
843
+ *
844
+ * It must be a valid JavaScript Date string that can be parsed.
845
+ *
846
+ * Valid range: `"0000-01-01T00:00:00.000Z"` to `"9999-12-31T23:59:59.999Z"`.
847
+ *
848
+ * ### Example
849
+ *
850
+ * ```ts
851
+ * const result = DateIsoString.from("2023-01-01T12:00:00.000Z"); // ok
852
+ * const error = DateIsoString.from("10000-01-01T00:00:00.000Z"); // err
853
+ * ```
854
+ *
855
+ * @category String
856
+ */
857
+ export const DateIsoString = brand("DateIso", String, (value) => {
858
+ if (value.length !== 24) {
859
+ return err<DateIsoStringError>({ type: "DateIsoString", value });
860
+ }
861
+ if (isNaN(globalThis.Date.parse(value))) {
862
+ return err<DateIsoStringError>({ type: "DateIsoString", value });
863
+ }
864
+ return ok(value);
865
+ });
866
+
867
+ export type DateIsoString = typeof DateIsoString.Type;
868
+
869
+ export interface DateIsoStringError extends TypeError<"DateIsoString"> {}
870
+
871
+ export const formatDateIsoStringError =
872
+ createTypeErrorFormatter<DateIsoStringError>(
873
+ (error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
874
+ );
875
+
876
+ /**
877
+ * Helper type for Type Factory that creates a branded Type.
878
+ *
879
+ * ### Example
880
+ *
881
+ * ```ts
882
+ * const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
883
+ * parent,
884
+ * ) =>
885
+ * brand("Trimmed", parent, (value) =>
886
+ * value.trim().length === value.length
887
+ * ? ok(value)
888
+ * : err<TrimmedError>({ type: "Trimmed", value }),
889
+ * );
890
+ * ```
891
+ *
892
+ * @category Utilities
893
+ */
894
+ export type BrandFactory<
895
+ Name extends TypeName,
896
+ Input,
897
+ RefineError extends TypeError,
898
+ > = <
899
+ PName extends TypeName,
900
+ P extends Input,
901
+ PInput,
902
+ PParent,
903
+ PError extends TypeError = never,
904
+ PParentError extends TypeError = never,
905
+ >(
906
+ parent: Type<PName, P, PInput, PError, PParent, PParentError>,
907
+ ) => BrandType<
908
+ Type<PName, P, PInput, PError, PParent, PParentError>,
909
+ Name,
910
+ RefineError,
911
+ PError | PParentError
912
+ >;
913
+
914
+ /**
915
+ * Trimmed string.
916
+ *
917
+ * This Type Factory does not transform; it only validates whether a string has
918
+ * no leading or trailing whitespaces. To trim a string, use {@link trim} Type
919
+ * Factory.
920
+ *
921
+ * ### Examples
922
+ *
923
+ * ```ts
924
+ * // this Type already exists
925
+ * const TrimmedString = trimmed(String);
926
+ * type TrimmedString = typeof TrimmedString.Type;
927
+ *
928
+ * // we can make any branded Type trimmed:
929
+ * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
930
+ * // string & Brand<"MinLength1"> & Brand<"Trimmed">
931
+ * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
932
+ * ```
933
+ *
934
+ * @category String
935
+ */
936
+ export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
937
+ parent,
938
+ ) =>
939
+ brand("Trimmed", parent, (value) =>
940
+ value.trim().length === value.length
941
+ ? ok(value)
942
+ : err<TrimmedError>({ type: "Trimmed", value }),
943
+ );
944
+
945
+ export interface TrimmedError extends TypeError<"Trimmed"> {}
946
+
947
+ export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
948
+ (error) => `A value ${error.value} is not trimmed`,
949
+ );
950
+
951
+ export type TransformBrandFactory<
952
+ Name extends TypeName,
953
+ Input,
954
+ TransformError extends TypeError = never,
955
+ > = <
956
+ PName extends TypeName,
957
+ P extends Input,
958
+ PInput,
959
+ PParent,
960
+ PError extends TypeError = never,
961
+ PParentError extends TypeError = never,
962
+ >(
963
+ parent: Type<PName, P, PInput, PError, PParent, PParentError>,
964
+ ) => TransformType<
965
+ Type<PName, P, PInput, PError, PParent, PParentError>,
966
+ BrandType<
967
+ Type<PName, P, PInput, PError, PParent, PParentError>,
968
+ Name,
969
+ never,
970
+ PError | PParentError
971
+ >,
972
+ TransformError
973
+ >;
974
+
975
+ /**
976
+ * Trims leading and trailing whitespace from a string.
977
+ *
978
+ * This Type Factory **transforms** the input string by removing whitespace from
979
+ * both ends. For validation only, use {@link trimmed} Type Factory.
980
+ *
981
+ * ### Example
982
+ *
983
+ * ```ts
984
+ * const TrimString = trim(String);
985
+ * expect(TrimString.from("a ")).toEqual(ok("a"));
986
+ * expect(TrimString.fromParent("a ").value).toEqual("a");
987
+ *
988
+ * const TrimNonEmptyString = trim(NonEmptyString);
989
+ * expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
990
+ * expect(
991
+ * TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
992
+ * ).toEqual("a");
993
+ * ```
994
+ *
995
+ * **Note:** This transformation is irreversible. Calling `toParent` will not
996
+ * restore the original representation.
997
+ *
998
+ * @category String
999
+ */
1000
+
1001
+ export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
1002
+ transform(
1003
+ parent,
1004
+ trimmed(parent) as BrandType<typeof parent, "Trimmed">,
1005
+ (value) => ok(value.trim() as InferType<typeof parent> & Brand<"Trimmed">),
1006
+ (value) => value,
1007
+ );
1008
+
1009
+ /**
1010
+ * Trimmed string
1011
+ *
1012
+ * - Use `TrimmedString.is` to check if an unknown value is trimmed.
1013
+ * - Use `TrimmedString.from` to check if a string is trimmed.
1014
+ *
1015
+ * @category String
1016
+ */
1017
+ export const TrimmedString = trimmed(String);
1018
+ export type TrimmedString = typeof TrimmedString.Type;
1019
+
1020
+ /**
1021
+ * Minimum length.
1022
+ *
1023
+ * ### Example
1024
+ *
1025
+ * ```ts
1026
+ * // string & Brand<"MinLength1">
1027
+ * const NonEmptyString = minLength(1)(String);
1028
+ * ```
1029
+ *
1030
+ * @category String
1031
+ * @category Array
1032
+ */
1033
+ export const minLength: <Min extends number>(
1034
+ min: Min,
1035
+ ) => BrandFactory<`MinLength${Min}`, { length: number }, MinLengthError<Min>> =
1036
+ (min) => (parent) =>
1037
+ brand(`MinLength${min}`, parent, (value) =>
1038
+ value.length >= min ? ok(value) : err({ type: "MinLength", value, min }),
1039
+ );
1040
+
1041
+ export interface MinLengthError<Min extends number = number>
1042
+ extends TypeError<"MinLength"> {
1043
+ readonly min: Min;
1044
+ }
1045
+
1046
+ export const formatMinLengthError = createTypeErrorFormatter<MinLengthError>(
1047
+ (error) =>
1048
+ `Value ${error.value} does not meet the minimum length of ${error.min}.`,
1049
+ );
1050
+
1051
+ /**
1052
+ * Maximum length.
1053
+ *
1054
+ * ### Example
1055
+ *
1056
+ * ```ts
1057
+ * // string & Brand<"MaxLength100">
1058
+ * const String100 = maxLength(100)(String);
1059
+ * ```
1060
+ *
1061
+ * @category String
1062
+ * @category Array
1063
+ */
1064
+ export const maxLength: <Max extends number>(
1065
+ max: Max,
1066
+ ) => BrandFactory<`MaxLength${Max}`, { length: number }, MaxLengthError<Max>> =
1067
+ (max) => (parent) =>
1068
+ brand(`MaxLength${max}`, parent, (value) =>
1069
+ value.length <= max ? ok(value) : err({ type: "MaxLength", value, max }),
1070
+ );
1071
+
1072
+ export interface MaxLengthError<Max extends number = number>
1073
+ extends TypeError<"MaxLength"> {
1074
+ readonly max: Max;
1075
+ }
1076
+
1077
+ export const formatMaxLengthError = createTypeErrorFormatter<MaxLengthError>(
1078
+ (error) => `Value ${error.value} exceeds the maximum length of ${error.max}.`,
1079
+ );
1080
+
1081
+ /**
1082
+ * Exact length.
1083
+ *
1084
+ * ### Example
1085
+ *
1086
+ * ```ts
1087
+ * // string & Brand<"Length1">
1088
+ * const Length1String = length(1)(String);
1089
+ * ```
1090
+ *
1091
+ * @category String
1092
+ * @category Array
1093
+ */
1094
+ export const length: <Exact extends number>(
1095
+ exact: Exact,
1096
+ ) => BrandFactory<`Length${Exact}`, { length: number }, LengthError<Exact>> =
1097
+ (exact) => (parent) =>
1098
+ brand(`Length${exact}`, parent, (value) =>
1099
+ value.length === exact
1100
+ ? ok(value)
1101
+ : err({ type: "Length", value, exact }),
1102
+ );
1103
+
1104
+ export interface LengthError<Exact extends number = number>
1105
+ extends TypeError<"Length"> {
1106
+ readonly exact: Exact;
1107
+ }
1108
+
1109
+ export const formatLengthError = createTypeErrorFormatter<LengthError>(
1110
+ (error) =>
1111
+ `Value ${error.value} does not have the required length of ${error.exact}.`,
1112
+ );
1113
+
1114
+ /** @category String */
1115
+ export const NonEmptyString = minLength(1)(String);
1116
+ export type NonEmptyString = typeof NonEmptyString.Type;
1117
+
1118
+ /** @category String */
1119
+ export const String100 = maxLength(100)(String);
1120
+ export type String100 = typeof String100.Type;
1121
+
1122
+ /** @category String */
1123
+ export const String1000 = maxLength(1000)(String);
1124
+ export type String1000 = typeof String1000.Type;
1125
+
1126
+ /** @category String */
1127
+ export const NonEmptyString100 = minLength(1)(String100);
1128
+ export type NonEmptyString100 = typeof NonEmptyString100.Type;
1129
+
1130
+ /** @category String */
1131
+ export const NonEmptyString1000 = minLength(1)(String1000);
1132
+ export type NonEmptyString1000 = typeof NonEmptyString1000.Type;
1133
+
1134
+ /** @category String */
1135
+ export const NonEmptyTrimmedString = minLength(1)(TrimmedString);
1136
+ export type NonEmptyTrimmedString = typeof NonEmptyTrimmedString.Type;
1137
+
1138
+ /** @category String */
1139
+ export const TrimmedString100 = maxLength(100)(TrimmedString);
1140
+ export type TrimmedString100 = typeof TrimmedString100.Type;
1141
+
1142
+ /** @category String */
1143
+ export const TrimmedString1000 = maxLength(1000)(TrimmedString);
1144
+ export type TrimmedString1000 = typeof TrimmedString1000.Type;
1145
+
1146
+ /** @category String */
1147
+ export const NonEmptyTrimmedString100 = minLength(1)(TrimmedString100);
1148
+ export type NonEmptyTrimmedString100 = typeof NonEmptyTrimmedString100.Type;
1149
+
1150
+ /** @category String */
1151
+ export const NonEmptyTrimmedString1000 = minLength(1)(TrimmedString1000);
1152
+ export type NonEmptyTrimmedString1000 = typeof NonEmptyTrimmedString1000.Type;
1153
+
1154
+ /**
1155
+ * The mnemonic, also known as a "seed phrase," is a set of 12 words in a
1156
+ * specific order chosen from a predefined list (BIP39). It provides a
1157
+ * human-readable way to store a private key securely. The mnemonic is generated
1158
+ * safely on the user's device using cryptographically secure random number
1159
+ * generation, ensuring it remains private and unique.
1160
+ *
1161
+ * @category String
1162
+ */
1163
+ export const Mnemonic = brand("Mnemonic", NonEmptyTrimmedString, (value) =>
1164
+ bip39.validateMnemonic(value, wordlist)
1165
+ ? ok(value)
1166
+ : err<MnemonicError>({ type: "Mnemonic", value }),
1167
+ );
1168
+ export type Mnemonic = typeof Mnemonic.Type;
1169
+
1170
+ export interface MnemonicError extends TypeError<"Mnemonic"> {}
1171
+
1172
+ export const formatMnemonicError = createTypeErrorFormatter<MnemonicError>(
1173
+ (error) => `Invalid BIP39 mnemonic: ${error.value}`,
1174
+ );
1175
+
1176
+ /**
1177
+ * String matching a regular expression.
1178
+ *
1179
+ * ### Example
1180
+ *
1181
+ * ```ts
1182
+ * const Alphanumeric = regex("Alphanumeric", /^[a-z0-9]+$/i)(String);
1183
+ * ```
1184
+ *
1185
+ * @category String
1186
+ */
1187
+ export const regex: <Name extends TypeName>(
1188
+ name: Name,
1189
+ pattern: RegExp,
1190
+ ) => BrandFactory<Name, string, RegexError<Name>> = (name, pattern) => {
1191
+ // Clone the regex to avoid shared state.
1192
+ const clonedPattern = new RegExp(pattern.source, pattern.flags);
1193
+ return (parent) =>
1194
+ brand(name, parent, (value) => {
1195
+ // Reset `lastIndex` before each use to ensure the regex starts matching
1196
+ // from the beginning of the string.
1197
+ clonedPattern.lastIndex = 0;
1198
+ return clonedPattern.test(value)
1199
+ ? ok(value)
1200
+ : err({ type: "Regex", name, value, pattern });
1201
+ });
1202
+ };
1203
+
1204
+ export interface RegexError<Name extends TypeName = TypeName>
1205
+ extends TypeError<"Regex"> {
1206
+ readonly name: Name;
1207
+ readonly pattern: RegExp;
1208
+ }
1209
+
1210
+ export const formatRegexError = createTypeErrorFormatter<RegexError>(
1211
+ (error) =>
1212
+ `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`,
1213
+ );
1214
+
1215
+ /**
1216
+ * URL-safe Base64 string.
1217
+ *
1218
+ * A `Base64Url` string uses a limited alphabet that is URL-safe:
1219
+ *
1220
+ * - Uppercase letters (`A-Z`)
1221
+ * - Lowercase letters (`a-z`)
1222
+ * - Digits (`0-9`)
1223
+ * - Dash (`-`)
1224
+ * - Underscore (`_`)
1225
+ *
1226
+ * ### Example
1227
+ *
1228
+ * ```ts
1229
+ * const result = Base64Url.from("abc123_-");
1230
+ * if (result.ok) {
1231
+ * console.log("Valid Base64Url string:", result.value);
1232
+ * } else {
1233
+ * console.error("Invalid Base64Url string:", result.error);
1234
+ * }
1235
+ * ```
1236
+ *
1237
+ * @category String
1238
+ */
1239
+ export const Base64Url = regex(
1240
+ "Base64Url",
1241
+ /^[A-Za-z0-9_-]+$/, // URL-safe Base64 alphabet
1242
+ )(String);
1243
+ export type Base64Url = typeof Base64Url.Type;
1244
+ export type Base64UrlError = typeof Base64Url.Error;
1245
+
1246
+ /**
1247
+ * Simple alphanumeric string for naming.
1248
+ *
1249
+ * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
1250
+ *
1251
+ * - Uppercase letters (`A-Z`)
1252
+ * - Lowercase letters (`a-z`)
1253
+ * - Digits (`0-9`)
1254
+ * - Dash (`-`)
1255
+ *
1256
+ * The string must be between 1 and 42 characters.
1257
+ *
1258
+ * ### Example
1259
+ *
1260
+ * ```ts
1261
+ * const result = SimpleName.from("data-report-123");
1262
+ * if (result.ok) {
1263
+ * console.log("Valid SimpleName string:", result.value);
1264
+ * } else {
1265
+ * console.error("Invalid SimpleName string:", result.error);
1266
+ * }
1267
+ * ```
1268
+ *
1269
+ * @category String
1270
+ */
1271
+ export const SimpleName = regex("SimpleName", /^[a-z0-9-]{1,42}$/i)(String);
1272
+ export type SimpleName = typeof SimpleName.Type;
1273
+ export type SimpleNameError = typeof SimpleName.Error;
1274
+
1275
+ /**
1276
+ * Default NanoId.
1277
+ *
1278
+ * @category String
1279
+ */
1280
+ export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
1281
+ export type NanoId = typeof NanoId.Type;
1282
+ export type NanoIdError = typeof NanoId.Error;
1283
+
1284
+ /**
1285
+ * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
1286
+ *
1287
+ * @category String
1288
+ */
1289
+ export const SimplePassword = brand(
1290
+ "SimplePassword",
1291
+ minLength(8)(maxLength(64)(TrimmedString)),
1292
+ );
1293
+ export type SimplePassword = typeof SimplePassword.Type;
1294
+
1295
+ export type SimplePasswordError = typeof SimplePassword.Error;
1296
+
1297
+ export const formatSimplePasswordError = (
1298
+ formatTypeError: TypeErrorFormatter<
1299
+ StringError | MinLengthError<8> | MaxLengthError<64> | TrimmedError
1300
+ >,
1301
+ ): TypeErrorFormatter<SimplePasswordError> =>
1302
+ createTypeErrorFormatter<SimplePasswordError>(
1303
+ (error) => `Invalid password: ${formatTypeError(error.parentError)}`,
1304
+ );
1305
+
1306
+ /**
1307
+ * `Id` {@link Type}.
1308
+ *
1309
+ * Represents a unique identifier with exactly 21 characters, using NanoID's
1310
+ * standard format (`A-Za-z0-9_-`).
1311
+ *
1312
+ * @category String
1313
+ */
1314
+ export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
1315
+ export type Id = typeof Id.Type;
1316
+
1317
+ export const idTypeValueLength = 21;
1318
+
1319
+ /**
1320
+ * Creates an {@link Id}.
1321
+ *
1322
+ * ### Example
1323
+ *
1324
+ * ```ts
1325
+ * // string & Brand<"Id">
1326
+ * const id = createId(deps);
1327
+ * ```
1328
+ */
1329
+ export const createId = (deps: NanoIdLibDep): Id =>
1330
+ deps.nanoIdLib.nanoid() as Id;
1331
+
1332
+ /**
1333
+ * Type Factory to create branded {@link Id} Type for a specific table.
1334
+ *
1335
+ * ### Example
1336
+ *
1337
+ * ```ts
1338
+ * const TodoId = id("Todo");
1339
+ * // string & Brand<"Id"> & Brand<"Todo">
1340
+ * type TodoId = typeof TodoId.Type;
1341
+ * ```
1342
+ *
1343
+ * @category String
1344
+ */
1345
+ export const id = <Table extends TypeName>(table: Table): IdType<Table> => {
1346
+ const fromParent = (value: string) => {
1347
+ const idResult = Id.fromParent(value);
1348
+ if (!idResult.ok) {
1349
+ return err<IdError<Table>>({ type: "Id", value, table });
1350
+ }
1351
+ return ok(idResult.value as Id & Brand<Table>);
1352
+ };
1353
+
1354
+ const fromUnknown = (value: unknown) => {
1355
+ const parentResult = String.fromUnknown(value);
1356
+ if (!parentResult.ok) return parentResult;
1357
+ return fromParent(parentResult.value);
1358
+ };
1359
+
1360
+ return {
1361
+ ...createType("Id", {
1362
+ fromUnknown,
1363
+ to: (value: Id & Brand<Table>) => value as string,
1364
+ fromParent,
1365
+ toParent: (value: Id & Brand<Table>) => value as string,
1366
+ }),
1367
+ table,
1368
+ };
1369
+ };
1370
+
1371
+ export interface IdType<Table extends TypeName>
1372
+ extends Type<
1373
+ "Id",
1374
+ string & Brand<"Id"> & Brand<Table>,
1375
+ string,
1376
+ IdError<Table>,
1377
+ string,
1378
+ StringError
1379
+ > {
1380
+ table: Table;
1381
+ }
1382
+
1383
+ export interface IdError<Table extends TypeName = TypeName>
1384
+ extends TypeError<"Id"> {
1385
+ readonly table: Table;
1386
+ }
1387
+
1388
+ export const formatIdError = createTypeErrorFormatter<IdError>(
1389
+ (error) => `Invalid ${error.type} table Id: ${error.value}`,
1390
+ );
1391
+
1392
+ /**
1393
+ * Positive number.
1394
+ *
1395
+ * ### Example
1396
+ *
1397
+ * ```ts
1398
+ * const PositiveNumber = positive(Number);
1399
+ *
1400
+ * const result = PositiveNumber.from(42); // ok
1401
+ * const errorResult = PositiveNumber.from(-5); // err
1402
+ * ```
1403
+ *
1404
+ * @category Number
1405
+ */
1406
+ export const positive: BrandFactory<"Positive", number, PositiveError> = (
1407
+ parent,
1408
+ ) =>
1409
+ brand("Positive", parent, (value) =>
1410
+ value > 0 ? ok(value) : err<PositiveError>({ type: "Positive", value }),
1411
+ );
1412
+
1413
+ export interface PositiveError extends TypeError<"Positive"> {}
1414
+
1415
+ export const formatPositiveError = createTypeErrorFormatter<PositiveError>(
1416
+ (error) => `The value ${error.value} is not positive.`,
1417
+ );
1418
+
1419
+ /**
1420
+ * Negative number.
1421
+ *
1422
+ * ### Example
1423
+ *
1424
+ * ```ts
1425
+ * const NegativeNumber = negative(Number);
1426
+ * ```
1427
+ *
1428
+ * @category Number
1429
+ */
1430
+ export const negative: BrandFactory<"Negative", number, NegativeError> = (
1431
+ parent,
1432
+ ) =>
1433
+ brand("Negative", parent, (value) =>
1434
+ value < 0 ? ok(value) : err<NegativeError>({ type: "Negative", value }),
1435
+ );
1436
+
1437
+ export interface NegativeError extends TypeError<"Negative"> {}
1438
+
1439
+ export const formatNegativeError = createTypeErrorFormatter<NegativeError>(
1440
+ (error) => `The value ${error.value} is not negative.`,
1441
+ );
1442
+
1443
+ /**
1444
+ * Non-positive number.
1445
+ *
1446
+ * ### Example
1447
+ *
1448
+ * ```ts
1449
+ * const NonPositiveNumber = nonPositive(Number);
1450
+ * ```
1451
+ *
1452
+ * @category Number
1453
+ */
1454
+ export const nonPositive: BrandFactory<
1455
+ "NonPositive",
1456
+ number,
1457
+ NonPositiveError
1458
+ > = (parent) =>
1459
+ brand("NonPositive", parent, (value) =>
1460
+ value <= 0
1461
+ ? ok(value)
1462
+ : err<NonPositiveError>({ type: "NonPositive", value }),
1463
+ );
1464
+
1465
+ export interface NonPositiveError extends TypeError<"NonPositive"> {}
1466
+
1467
+ export const formatNonPositiveError =
1468
+ createTypeErrorFormatter<NonPositiveError>(
1469
+ (error) => `The value ${error.value} is not non-positive.`,
1470
+ );
1471
+
1472
+ /**
1473
+ * Non-negative number.
1474
+ *
1475
+ * ### Example
1476
+ *
1477
+ * ```ts
1478
+ * const NonNegativeNumber = nonNegative(Number);
1479
+ * ```
1480
+ *
1481
+ * @category Number
1482
+ */
1483
+ export const nonNegative: BrandFactory<
1484
+ "NonNegative",
1485
+ number,
1486
+ NonNegativeError
1487
+ > = (parent) =>
1488
+ brand("NonNegative", parent, (value) =>
1489
+ value >= 0
1490
+ ? ok(value)
1491
+ : err<NonNegativeError>({ type: "NonNegative", value }),
1492
+ );
1493
+
1494
+ export interface NonNegativeError extends TypeError<"NonNegative"> {}
1495
+
1496
+ export const formatNonNegativeError =
1497
+ createTypeErrorFormatter<NonNegativeError>(
1498
+ (error) => `The value ${error.value} is not non-negative.`,
1499
+ );
1500
+
1501
+ /** @category Number */
1502
+ export const NonNegativeNumber = nonNegative(Number);
1503
+ export type NonNegativeNumber = typeof NonNegativeNumber.Type;
1504
+
1505
+ /** @category Number */
1506
+ export const PositiveNumber = positive(NonNegativeNumber);
1507
+ export type PositiveNumber = typeof PositiveNumber.Type;
1508
+
1509
+ /** @category Number */
1510
+ export const NonPositiveNumber = nonPositive(Number);
1511
+ export type NonPositiveNumber = typeof NonPositiveNumber.Type;
1512
+
1513
+ /** @category Number */
1514
+ export const NegativeNumber = negative(NonPositiveNumber);
1515
+ export type NegativeNumber = typeof NegativeNumber.Type;
1516
+
1517
+ /**
1518
+ * Integer within the safe range of JavaScript numbers.
1519
+ *
1520
+ * ### Example
1521
+ *
1522
+ * ```ts
1523
+ * const Int = int(Number);
1524
+ * ```
1525
+ *
1526
+ * @category Number
1527
+ */
1528
+ export const int: BrandFactory<"Int", number, IntError> = (parent) =>
1529
+ brand("Int", parent, (value) =>
1530
+ globalThis.Number.isSafeInteger(value)
1531
+ ? ok(value)
1532
+ : err<IntError>({ type: "Int", value }),
1533
+ );
1534
+
1535
+ export interface IntError extends TypeError<"Int"> {}
1536
+
1537
+ export const formatIntError = createTypeErrorFormatter<IntError>(
1538
+ (error) => `The value ${error.value} is not an integer.`,
1539
+ );
1540
+
1541
+ /**
1542
+ * Integer within the safe range of JavaScript numbers.
1543
+ *
1544
+ * @category Number
1545
+ */
1546
+ export const Int = int(Number);
1547
+ export type Int = typeof Int.Type;
1548
+
1549
+ /** @category Number */
1550
+ export const NonNegativeInt = nonNegative(Int);
1551
+ export type NonNegativeInt = typeof NonNegativeInt.Type;
1552
+
1553
+ /** @category Number */
1554
+ export const PositiveInt = positive(NonNegativeInt);
1555
+ export type PositiveInt = typeof PositiveInt.Type;
1556
+
1557
+ /** @category Number */
1558
+ export const NonPositiveInt = nonPositive(Int);
1559
+ export type NonPositiveInt = typeof NonPositiveInt.Type;
1560
+
1561
+ /** @category Number */
1562
+ export const NegativeInt = negative(NonPositiveInt);
1563
+ export type NegativeInt = typeof NegativeInt.Type;
1564
+
1565
+ /**
1566
+ * Number greater than a specified value.
1567
+ *
1568
+ * @category Number
1569
+ */
1570
+ export const greaterThan: <Min extends number>(
1571
+ min: Min,
1572
+ ) => BrandFactory<`GreaterThan${Min}`, number, GreaterThanError<Min>> =
1573
+ (min) => (parent) =>
1574
+ brand(`GreaterThan${min}`, parent, (value) =>
1575
+ value > min ? ok(value) : err({ type: "GreaterThan", value, min }),
1576
+ );
1577
+
1578
+ export interface GreaterThanError<Min extends number = number>
1579
+ extends TypeError<"GreaterThan"> {
1580
+ readonly min: Min;
1581
+ }
1582
+
1583
+ export const formatGreaterThanError =
1584
+ createTypeErrorFormatter<GreaterThanError>(
1585
+ (error) => `The value ${error.value} is not > ${error.min}.`,
1586
+ );
1587
+
1588
+ /**
1589
+ * Number less than a specified value.
1590
+ *
1591
+ * @category Number
1592
+ */
1593
+ export const lessThan: <Max extends number>(
1594
+ max: Max,
1595
+ ) => BrandFactory<`LessThan${Max}`, number, LessThanError<Max>> =
1596
+ (max) => (parent) =>
1597
+ brand(`LessThan${max}`, parent, (value) =>
1598
+ value < max ? ok(value) : err({ type: "LessThan", value, max }),
1599
+ );
1600
+
1601
+ export interface LessThanError<Max extends number = number>
1602
+ extends TypeError<"LessThan"> {
1603
+ readonly max: Max;
1604
+ }
1605
+
1606
+ export const formatLessThanError = createTypeErrorFormatter<LessThanError>(
1607
+ (error) => `The value ${error.value} is not < ${error.max}.`,
1608
+ );
1609
+
1610
+ /**
1611
+ * Number ≥ a specified value.
1612
+ *
1613
+ * @category Number
1614
+ */
1615
+ export const greaterThanOrEqualTo: <Min extends number>(
1616
+ min: Min,
1617
+ ) => BrandFactory<
1618
+ `GreaterThanOrEqualTo${Min}`,
1619
+ number,
1620
+ GreaterThanOrEqualToError<Min>
1621
+ > = (min) => (parent) =>
1622
+ brand(`GreaterThanOrEqualTo${min}`, parent, (value) =>
1623
+ value >= min
1624
+ ? ok(value)
1625
+ : err({ type: "GreaterThanOrEqualTo", value, min }),
1626
+ );
1627
+
1628
+ export interface GreaterThanOrEqualToError<Min extends number = number>
1629
+ extends TypeError<"GreaterThanOrEqualTo"> {
1630
+ readonly min: Min;
1631
+ }
1632
+
1633
+ export const formatGreaterThanOrEqualToError =
1634
+ createTypeErrorFormatter<GreaterThanOrEqualToError>(
1635
+ (error) => `The value ${error.value} is not >= ${error.min}.`,
1636
+ );
1637
+
1638
+ /**
1639
+ * Number ≤ a specified value.
1640
+ *
1641
+ * @category Number
1642
+ */
1643
+ export const lessThanOrEqualTo: <Max extends number>(
1644
+ max: Max,
1645
+ ) => BrandFactory<
1646
+ `LessThanOrEqualTo${Max}`,
1647
+ number,
1648
+ LessThanOrEqualToError<Max>
1649
+ > = (max) => (parent) =>
1650
+ brand(`LessThanOrEqualTo${max}`, parent, (value) =>
1651
+ value <= max ? ok(value) : err({ type: "LessThanOrEqualTo", value, max }),
1652
+ );
1653
+
1654
+ export interface LessThanOrEqualToError<Max extends number = number>
1655
+ extends TypeError<"LessThanOrEqualTo"> {
1656
+ readonly max: Max;
1657
+ }
1658
+
1659
+ export const formatLessThanOrEqualToError =
1660
+ createTypeErrorFormatter<LessThanOrEqualToError>(
1661
+ (error) => `The value ${error.value} is not <= ${error.max}.`,
1662
+ );
1663
+
1664
+ /**
1665
+ * Number that is not NaN.
1666
+ *
1667
+ * @category Number
1668
+ */
1669
+ export const nonNaN: BrandFactory<"NonNaN", number, NonNaNError> = (parent) =>
1670
+ brand("NonNaN", parent, (value) =>
1671
+ !globalThis.Number.isNaN(value)
1672
+ ? ok(value)
1673
+ : err<NonNaNError>({ type: "NonNaN", value }),
1674
+ );
1675
+
1676
+ export interface NonNaNError extends TypeError<"NonNaN"> {}
1677
+
1678
+ export const formatNonNaNError = createTypeErrorFormatter<NonNaNError>(
1679
+ (error) => `The value ${error.value} is NaN (not a number).`,
1680
+ );
1681
+
1682
+ /** @category Number */
1683
+ export const NonNaNNumber = nonNaN(Number);
1684
+ export type NonNaNNumber = typeof NonNaNNumber.Type;
1685
+
1686
+ /**
1687
+ * Finite number.
1688
+ *
1689
+ * @category Number
1690
+ */
1691
+ export const finite: BrandFactory<"Finite", number, FiniteError> = (parent) =>
1692
+ brand("Finite", parent, (value) =>
1693
+ globalThis.Number.isFinite(value)
1694
+ ? ok(value)
1695
+ : err<FiniteError>({ type: "Finite", value }),
1696
+ );
1697
+
1698
+ export interface FiniteError extends TypeError<"Finite"> {}
1699
+
1700
+ export const formatFiniteError = createTypeErrorFormatter<FiniteError>(
1701
+ (error) => `The value ${error.value} is not finite.`,
1702
+ );
1703
+
1704
+ /**
1705
+ * Finite number.
1706
+ *
1707
+ * This Type ensures that a number is finite.
1708
+ *
1709
+ * **Why is this important?**
1710
+ *
1711
+ * `JSON.stringify` serializes JavaScript numbers into `null` if they are not
1712
+ * finite (e.g., `Infinity`, `-Infinity`, or `NaN`). Using `FiniteNumber` helps
1713
+ * prevent these unexpected behaviors when working with JSON serialization.
1714
+ *
1715
+ * @category Number
1716
+ */
1717
+ export const FiniteNumber = finite(Number);
1718
+ export type FiniteNumber = typeof FiniteNumber.Type;
1719
+
1720
+ /**
1721
+ * Number that is a multiple of a divisor.
1722
+ *
1723
+ * @category Number
1724
+ */
1725
+ export const multipleOf: <Divisor extends number>(
1726
+ divisor: Divisor,
1727
+ ) => BrandFactory<`MultipleOf${Divisor}`, number, MultipleOfError<Divisor>> =
1728
+ (divisor) => (parent) =>
1729
+ brand(`MultipleOf${divisor}`, parent, (value) =>
1730
+ value % divisor === 0
1731
+ ? ok(value)
1732
+ : err({ type: "MultipleOf", value, divisor }),
1733
+ );
1734
+
1735
+ export interface MultipleOfError<Divisor extends number = number>
1736
+ extends TypeError<"MultipleOf"> {
1737
+ readonly divisor: Divisor;
1738
+ }
1739
+
1740
+ export const formatMultipleOfError = createTypeErrorFormatter<MultipleOfError>(
1741
+ (error) => `The value ${error.value} is not a multiple of ${error.divisor}.`,
1742
+ );
1743
+
1744
+ /**
1745
+ * Number within a range, inclusive.
1746
+ *
1747
+ * ### Example
1748
+ *
1749
+ * ```ts
1750
+ * const Between1And10 = between(1, 10)(PositiveNumber);
1751
+ * const result = Between1And10.from(5); // ok(5)
1752
+ * const errorResult = Between1And10.from(11); // err
1753
+ * ```
1754
+ *
1755
+ * @category Number
1756
+ */
1757
+ export const between: <Min extends number, Max extends number>(
1758
+ min: Min,
1759
+ max: Max,
1760
+ ) => BrandFactory<`Between${Min}-${Max}`, number, BetweenError<Min, Max>> =
1761
+ (min, max) => (parent) =>
1762
+ brand(`Between${min}-${max}`, parent, (value) =>
1763
+ value >= min && value <= max
1764
+ ? ok(value)
1765
+ : err({ type: "Between", value, min, max }),
1766
+ );
1767
+
1768
+ export interface BetweenError<
1769
+ Min extends number = number,
1770
+ Max extends number = number,
1771
+ > extends TypeError<"Between"> {
1772
+ readonly min: Min;
1773
+ readonly max: Max;
1774
+ }
1775
+
1776
+ export const formatBetweenError = createTypeErrorFormatter<BetweenError>(
1777
+ (error) =>
1778
+ `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
1779
+ );
1780
+
1781
+ /** @category Number */
1782
+ export const Between1And10 = between(1, 10)(Number);
1783
+ export type Between1And10 = typeof Between1And10.Type;
1784
+
1785
+ /**
1786
+ * Literal {@link Type}.
1787
+ *
1788
+ * https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
1789
+ *
1790
+ * ### Example
1791
+ *
1792
+ * ```ts
1793
+ * const LiteralHello = literal("Hello");
1794
+ * const result = LiteralHello.from("Hello"); // ok("Hello")
1795
+ * const errorResult = LiteralHello.from("World"); // err
1796
+ * ```
1797
+ *
1798
+ * TODO: Add JsonValue
1799
+ *
1800
+ * @category Base Factories
1801
+ */
1802
+ export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
1803
+ const fromUnknown = (value: unknown): Result<T, LiteralError<T>> =>
1804
+ value === expected
1805
+ ? ok(expected)
1806
+ : err<LiteralError<T>>({ type: "Literal", value, expected });
1807
+
1808
+ return {
1809
+ ...createType("Literal", {
1810
+ fromUnknown,
1811
+ to: identity as (value: T) => WidenLiteral<T>,
1812
+ fromParent: ok,
1813
+ toParent: identity,
1814
+ }),
1815
+ expected,
1816
+ };
1817
+ };
1818
+
1819
+ export interface LiteralType<T extends Literal>
1820
+ extends Type<"Literal", T, WidenLiteral<T>, LiteralError<T>> {
1821
+ expected: T;
1822
+ }
1823
+
1824
+ export interface LiteralError<T extends Literal = Literal>
1825
+ extends TypeError<"Literal"> {
1826
+ readonly expected: T;
1827
+ }
1828
+
1829
+ export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
1830
+ (error) =>
1831
+ `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(
1832
+ error.expected,
1833
+ )}.`,
1834
+ );
1835
+
1836
+ /**
1837
+ * {@link Type} that transforms values between `FromType` and `ToType`.
1838
+ *
1839
+ * - `fromParent`: Converts `FromType` to `ToType`, may fail.
1840
+ * - `toParent`: Converts `ToType` back to `FromType`, must not fail.
1841
+ *
1842
+ * ### Example
1843
+ *
1844
+ * // TODO: Examples
1845
+ *
1846
+ * @category Base Factories
1847
+ */
1848
+ export const transform = <
1849
+ FromType extends AnyType,
1850
+ ToType extends AnyType,
1851
+ TransformError extends TypeError = never,
1852
+ >(
1853
+ fromType: FromType,
1854
+ toType: ToType,
1855
+ fromParent: (
1856
+ parentValue: InferType<FromType>,
1857
+ ) => Result<InferType<ToType>, TransformError>,
1858
+ toParent: (value: InferType<ToType>) => InferType<FromType>,
1859
+ ): TransformType<FromType, ToType, TransformError> => {
1860
+ const fromUnknown = (
1861
+ value: unknown,
1862
+ ): Result<InferType<ToType>, InferErrors<FromType> | TransformError> => {
1863
+ const parentResult = fromType.fromUnknown(value);
1864
+ if (!parentResult.ok) return parentResult;
1865
+ return fromParent(parentResult.value as IntentionalNever);
1866
+ };
1867
+
1868
+ const to = (value: InferType<ToType>): InferInput<FromType> =>
1869
+ fromType.to(toParent(value)) as IntentionalNever;
1870
+
1871
+ return {
1872
+ ...createType("Transform", {
1873
+ fromUnknown,
1874
+ to,
1875
+ fromParent,
1876
+ toParent,
1877
+ }),
1878
+ is: toType.is,
1879
+ fromType,
1880
+ toType,
1881
+ } as TransformType<FromType, ToType, TransformError>;
1882
+ };
1883
+
1884
+ /**
1885
+ * TransformType extends {@link Type} with additional `fromType` and `toType`
1886
+ * properties for reflection.
1887
+ */
1888
+ export interface TransformType<
1889
+ FromType extends AnyType,
1890
+ ToType extends AnyType,
1891
+ TransformError extends TypeError = never,
1892
+ > extends Type<
1893
+ "Transform",
1894
+ InferType<ToType>,
1895
+ InferInput<FromType>,
1896
+ TransformError,
1897
+ InferType<FromType>,
1898
+ InferErrors<FromType>
1899
+ > {
1900
+ readonly fromType: FromType;
1901
+ readonly toType: ToType;
1902
+
1903
+ readonly fromParent: (
1904
+ value: InferType<FromType>,
1905
+ ) => [TransformError] extends [never]
1906
+ ? Ok<InferType<ToType>>
1907
+ : Result<InferType<ToType>, TransformError>;
1908
+ }
1909
+
1910
+ /**
1911
+ * Trims leading and trailing whitespace from a string.
1912
+ *
1913
+ * ### Example
1914
+ *
1915
+ * ```ts
1916
+ * expect(TrimString.from("a ")).toEqual(ok("a"));
1917
+ * expect(TrimString.fromParent("a ").value).toEqual("a");
1918
+ * ```
1919
+ *
1920
+ * @category String
1921
+ */
1922
+ export const TrimString = trim(String);
1923
+
1924
+ /**
1925
+ * Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
1926
+ *
1927
+ * ### Example
1928
+ *
1929
+ * TODO:
1930
+ *
1931
+ * @category String
1932
+ */
1933
+ export const DateIso = transform(
1934
+ Date,
1935
+ DateIsoString,
1936
+ (value) => DateIsoString.fromParent(value.toISOString()),
1937
+ (value) => new globalThis.Date(value),
1938
+ );
1939
+
1940
+ /**
1941
+ * Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
1942
+ *
1943
+ * ### Example
1944
+ *
1945
+ * ```ts
1946
+ * NumberFromString.from("42"); // ok(42)
1947
+ * NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
1948
+ * ```
1949
+ *
1950
+ * @category Number
1951
+ */
1952
+ export const NumberFromString = transform(
1953
+ NonEmptyTrimmedString,
1954
+ FiniteNumber,
1955
+ (value) => {
1956
+ const result = FiniteNumber.fromParent(globalThis.Number(value));
1957
+ if (!result.ok)
1958
+ return err<NumberFromStringError>({ type: "NumberFromString", value });
1959
+ return result;
1960
+ },
1961
+ (num) => num.toString() as NonEmptyTrimmedString,
1962
+ );
1963
+
1964
+ export interface NumberFromStringError extends TypeError<"NumberFromString"> {}
1965
+
1966
+ export const formatNumberFromStringError =
1967
+ createTypeErrorFormatter<NumberFromStringError>((error) => {
1968
+ return `The value ${error.value} could not be converted to a finite number.`;
1969
+ });
1970
+
1971
+ /**
1972
+ * Array of a specific {@link Type}.
1973
+ *
1974
+ * Usage:
1975
+ *
1976
+ * ```ts
1977
+ * const NumberArray = array(Number);
1978
+ *
1979
+ * const result1 = NumberArray.from([1, 2, 3]); // ok([1, 2, 3])
1980
+ * const result2 = NumberArray.from(["a", "b"]); // err(...)
1981
+ * ```
1982
+ *
1983
+ * @category Base Factories
1984
+ * @category Array
1985
+ */
1986
+ export const array = <ElementType extends AnyType>(
1987
+ element: ElementType,
1988
+ ): ArrayType<ElementType> => {
1989
+ const fromUnknown = (
1990
+ value: unknown,
1991
+ ): Result<
1992
+ ReadonlyArray<InferType<ElementType>>,
1993
+ ArrayError<InferErrors<ElementType>>
1994
+ > => {
1995
+ if (!Array.isArray(value)) {
1996
+ return err<ArrayError<InferErrors<ElementType>>>({
1997
+ type: "Array",
1998
+ value,
1999
+ reason: { kind: "NotArray" },
2000
+ });
2001
+ }
2002
+
2003
+ const result: Array<InferType<ElementType>> = [];
2004
+ for (let i = 0; i < value.length; i++) {
2005
+ const elementResult = element.fromUnknown(value[i]);
2006
+ if (!elementResult.ok) {
2007
+ return err<ArrayError<InferErrors<ElementType>>>({
2008
+ type: "Array",
2009
+ value,
2010
+ reason: {
2011
+ kind: "Element",
2012
+ index: i,
2013
+ error: elementResult.error as InferErrors<ElementType>,
2014
+ },
2015
+ });
2016
+ }
2017
+ result.push(elementResult.value as InferType<ElementType>);
2018
+ }
2019
+
2020
+ return ok(result);
2021
+ };
2022
+
2023
+ const to = (value: ReadonlyArray<InferType<ElementType>>) =>
2024
+ value.map(element.to) as ReadonlyArray<InferInput<ElementType>>;
2025
+
2026
+ const fromParent = (
2027
+ value: ReadonlyArray<InferParent<ElementType>>,
2028
+ ): Result<
2029
+ ReadonlyArray<InferType<ElementType>>,
2030
+ ArrayError<InferError<ElementType>>
2031
+ > => {
2032
+ const result: Array<InferType<ElementType>> = [];
2033
+ for (let i = 0; i < value.length; i++) {
2034
+ const elementResult = element.fromParent(value[i]);
2035
+ if (!elementResult.ok) {
2036
+ return err({
2037
+ type: "Array",
2038
+ value,
2039
+ reason: {
2040
+ kind: "Element",
2041
+ index: i,
2042
+ error: elementResult.error as InferError<ElementType>,
2043
+ },
2044
+ });
2045
+ }
2046
+ result.push(elementResult.value as InferType<ElementType>);
2047
+ }
2048
+ return ok(result);
2049
+ };
2050
+
2051
+ const toParent = (values: ReadonlyArray<InferType<ElementType>>) =>
2052
+ values.map(element.toParent) as ReadonlyArray<InferParent<ElementType>>;
2053
+
2054
+ return {
2055
+ ...createType("Array", {
2056
+ fromUnknown,
2057
+ to,
2058
+ fromParent,
2059
+ toParent,
2060
+ }),
2061
+ element,
2062
+ } as ArrayType<ElementType>;
2063
+ };
2064
+
2065
+ /** ArrayType extends Type with an additional `element` property for reflection. */
2066
+ export interface ArrayType<ElementType extends AnyType>
2067
+ extends Type<
2068
+ "Array",
2069
+ ReadonlyArray<InferType<ElementType>>,
2070
+ ReadonlyArray<InferInput<ElementType>>,
2071
+ ArrayError<InferError<ElementType>>,
2072
+ ReadonlyArray<InferParent<ElementType>>,
2073
+ ArrayError<InferParentError<ElementType>>
2074
+ > {
2075
+ readonly element: ElementType;
2076
+ }
2077
+
2078
+ export interface ArrayError<Error extends TypeError = TypeError>
2079
+ extends TypeErrorWithReason<
2080
+ "Array",
2081
+ | { readonly kind: "NotArray" }
2082
+ | {
2083
+ readonly kind: "Element";
2084
+ readonly index: number;
2085
+ readonly error: Error;
2086
+ }
2087
+ > {}
2088
+
2089
+ export const formatArrayError = <Error extends TypeError>(
2090
+ formatTypeError: TypeErrorFormatter<Error>,
2091
+ ): TypeErrorFormatter<ArrayError<Error>> =>
2092
+ createTypeErrorFormatter((error) => {
2093
+ switch (error.reason.kind) {
2094
+ case "NotArray":
2095
+ return `Expected an array but received ${error.value}.`;
2096
+ case "Element":
2097
+ return `Invalid element at index ${error.reason.index}: ${formatTypeError(error.reason.error)}`;
2098
+ }
2099
+ });
2100
+
2101
+ /**
2102
+ * Record of a key {@link Type} and value {@link Type}.
2103
+ *
2104
+ * - The input must be a plain object (validated by {@link isPlainObject}).
2105
+ * - Each key is validated/transformed by the `key` Type.
2106
+ * - Each value is validated/transformed by the `value` Type.
2107
+ *
2108
+ * The resulting type is `Readonly<Record<KeyT, ValueT>>`.
2109
+ *
2110
+ * ### Example
2111
+ *
2112
+ * ```ts
2113
+ * const StringToNumberRecord = record(String, Number);
2114
+ *
2115
+ * // ok({ "a": 1, "b": 2 })
2116
+ * StringToNumberRecord.from({ a: 1, b: 2 });
2117
+ *
2118
+ * // err => "Key" because 42 is not a string key
2119
+ * StringToNumberRecord.from({ 42: 1, b: 2 });
2120
+ *
2121
+ * // err => "Value" because "x" is not a number
2122
+ * StringToNumberRecord.from({ a: "x", b: 2 });
2123
+ * ```
2124
+ *
2125
+ * @category Base Factories
2126
+ * @category Object
2127
+ */
2128
+ export const record = <
2129
+ KeyName extends TypeName,
2130
+ KeyT extends string,
2131
+ KeyInput extends string,
2132
+ KeyError extends TypeError,
2133
+ KeyParent extends string,
2134
+ KeyParentError extends TypeError,
2135
+ Value extends AnyType,
2136
+ >(
2137
+ keyType: Type<KeyName, KeyT, KeyInput, KeyError, KeyParent, KeyParentError>,
2138
+ valueType: Value,
2139
+ ): RecordType<
2140
+ KeyName,
2141
+ KeyT,
2142
+ KeyInput,
2143
+ KeyError,
2144
+ KeyParent,
2145
+ KeyParentError,
2146
+ Value
2147
+ > => {
2148
+ const fromUnknown = (
2149
+ value: unknown,
2150
+ ): Result<
2151
+ Record<KeyT, InferType<Value>>,
2152
+ RecordError<KeyError, InferError<Value>>
2153
+ > => {
2154
+ if (!isPlainObject(value)) {
2155
+ return err({
2156
+ type: "Record",
2157
+ value,
2158
+ reason: { kind: "NotRecord" },
2159
+ });
2160
+ }
2161
+
2162
+ const result: Record<KeyT, InferType<Value>> = {} as Record<
2163
+ KeyT,
2164
+ InferType<Value>
2165
+ >;
2166
+ for (const [rawKey, rawValue] of Object.entries(value)) {
2167
+ const keyResult = keyType.fromUnknown(rawKey);
2168
+ if (!keyResult.ok) {
2169
+ return err({
2170
+ type: "Record",
2171
+ value,
2172
+ reason: { kind: "Key", key: rawKey, error: keyResult.error },
2173
+ } as IntentionalNever);
2174
+ }
2175
+
2176
+ const valueResult = valueType.fromUnknown(rawValue);
2177
+ if (!valueResult.ok) {
2178
+ return err({
2179
+ type: "Record",
2180
+ value,
2181
+ reason: {
2182
+ kind: "Value",
2183
+ key: rawKey,
2184
+ error: valueResult.error as IntentionalNever,
2185
+ },
2186
+ });
2187
+ }
2188
+
2189
+ result[keyResult.value] = valueResult.value as IntentionalNever;
2190
+ }
2191
+
2192
+ return ok(result);
2193
+ };
2194
+
2195
+ const to = (
2196
+ value: Readonly<Record<KeyT, InferType<Value>>>,
2197
+ ): Readonly<Record<KeyInput, InferInput<Value>>> =>
2198
+ Object.fromEntries(
2199
+ Object.entries(value).map(([key, val]) => [
2200
+ keyType.to(key as KeyT),
2201
+ valueType.to(val),
2202
+ ]),
2203
+ ) as Readonly<Record<KeyInput, InferInput<Value>>>;
2204
+
2205
+ const fromParent = (
2206
+ value: Readonly<Record<KeyParent, InferParent<Value>>>,
2207
+ ): Result<
2208
+ Record<KeyT, InferType<Value>>,
2209
+ RecordError<KeyError, InferError<Value>>
2210
+ > => {
2211
+ const result: Record<KeyT, InferType<Value>> = {} as Record<
2212
+ KeyT,
2213
+ InferType<Value>
2214
+ >;
2215
+
2216
+ for (const [rawKey, rawValue] of Object.entries(value)) {
2217
+ const keyResult = keyType.fromParent(rawKey as KeyParent);
2218
+ if (!keyResult.ok) {
2219
+ return err<RecordError<KeyError, InferError<Value>>>({
2220
+ type: "Record",
2221
+ value,
2222
+ reason: {
2223
+ kind: "Key",
2224
+ key: rawKey,
2225
+ error: keyResult.error,
2226
+ },
2227
+ });
2228
+ }
2229
+
2230
+ const valueResult = valueType.fromParent(rawValue);
2231
+ if (!valueResult.ok) {
2232
+ return err({
2233
+ type: "Record",
2234
+ value,
2235
+ reason: {
2236
+ kind: "Value",
2237
+ key: keyResult.value,
2238
+ error: valueResult.error as InferError<Value>,
2239
+ },
2240
+ });
2241
+ }
2242
+
2243
+ result[keyResult.value] = valueResult.value as InferType<Value>;
2244
+ }
2245
+
2246
+ return ok(result);
2247
+ };
2248
+
2249
+ const toParent = (
2250
+ value: Readonly<Record<KeyT, InferType<Value>>>,
2251
+ ): Readonly<Record<KeyParent, InferParent<Value>>> =>
2252
+ Object.fromEntries(
2253
+ Object.entries(value).map(([key, val]) => [
2254
+ keyType.toParent(key as KeyT),
2255
+ valueType.toParent(val),
2256
+ ]),
2257
+ ) as Readonly<Record<KeyParent, InferParent<Value>>>;
2258
+
2259
+ return {
2260
+ ...createType("Record", {
2261
+ fromUnknown,
2262
+ to,
2263
+ fromParent,
2264
+ toParent,
2265
+ }),
2266
+ key: keyType,
2267
+ value: valueType,
2268
+ } as IntentionalNever;
2269
+ };
2270
+
2271
+ /**
2272
+ * RecordType extends {@link Type} with additional `key` and `value` properties
2273
+ * for reflection.
2274
+ */
2275
+ export interface RecordType<
2276
+ KeyName extends TypeName,
2277
+ KeyT extends string,
2278
+ KeyInput extends string,
2279
+ KeyError extends TypeError,
2280
+ KeyParent extends string,
2281
+ KeyParentError extends TypeError,
2282
+ Value extends AnyType = AnyType,
2283
+ > extends Type<
2284
+ "Record",
2285
+ Readonly<Record<KeyT, InferType<Value>>>,
2286
+ Readonly<Record<KeyInput, InferInput<Value>>>,
2287
+ RecordError<KeyError, InferError<Value>>,
2288
+ Readonly<Record<KeyParent, InferParent<Value>>>,
2289
+ RecordError<KeyParentError, InferParentError<Value>>
2290
+ > {
2291
+ readonly key: Type<
2292
+ KeyName,
2293
+ KeyT,
2294
+ KeyInput,
2295
+ KeyError,
2296
+ KeyParent,
2297
+ KeyParentError
2298
+ >;
2299
+ readonly value: Value;
2300
+ }
2301
+
2302
+ export interface RecordError<
2303
+ KeyError extends TypeError = TypeError,
2304
+ ValueError extends TypeError = TypeError,
2305
+ > extends TypeErrorWithReason<
2306
+ "Record",
2307
+ | { readonly kind: "NotRecord" }
2308
+ | {
2309
+ readonly kind: "Key";
2310
+ readonly key: unknown;
2311
+ readonly error: KeyError;
2312
+ }
2313
+ | {
2314
+ readonly kind: "Value";
2315
+ readonly key: unknown;
2316
+ readonly error: ValueError;
2317
+ }
2318
+ > {}
2319
+
2320
+ export const formatRecordError = <Error extends TypeError>(
2321
+ formatTypeError: TypeErrorFormatter<Error>,
2322
+ ): TypeErrorFormatter<RecordError<Error, Error>> =>
2323
+ createTypeErrorFormatter((error) => {
2324
+ switch (error.reason.kind) {
2325
+ case "NotRecord":
2326
+ return `Expected a record (plain object) but received ${error.value}.`;
2327
+ case "Key":
2328
+ return `Invalid key ${error.reason.key}: ${formatTypeError(error.reason.error)}`;
2329
+ case "Value":
2330
+ return `Invalid value for key ${error.reason.key}: ${formatTypeError(error.reason.error)}`;
2331
+ }
2332
+ });
2333
+
2334
+ /**
2335
+ * Object {@link Type}.
2336
+ *
2337
+ * This validates that:
2338
+ *
2339
+ * - The value is a plain object (checked with {@link isPlainObject}).
2340
+ * - The object has no extra properties beyond the specified keys unless an index
2341
+ * signature is provided.
2342
+ * - Each property's value matches the specified Type.
2343
+ *
2344
+ * When an index signature is included, the object can have additional keys that
2345
+ * conform to the specified key and value Types.
2346
+ *
2347
+ * The resulting `ObjectType` includes `props` for reflection, which defines the
2348
+ * expected structure, and optionally an `record` for flexible key/value pairs.
2349
+ *
2350
+ * https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures
2351
+ *
2352
+ * ### Examples
2353
+ *
2354
+ * #### Basic Object Validation
2355
+ *
2356
+ * ```ts
2357
+ * const User = object({
2358
+ * name: NonEmptyTrimmedString,
2359
+ * age: PositiveNumber,
2360
+ * });
2361
+ * type User = typeof User.Type;
2362
+ *
2363
+ * const result = User.from({ name: "John", age: 30 }); // ok({ name: "John", age: 30 })
2364
+ * const error = User.from({ name: "John", age: -5 }); // err
2365
+ * ```
2366
+ *
2367
+ * #### Optional Properties
2368
+ *
2369
+ * In this example the `age` property is marked as optional using
2370
+ * {@link optional}.
2371
+ *
2372
+ * ```ts
2373
+ * const User = object({
2374
+ * name: NonEmptyString, // Required
2375
+ * age: optional(PositiveNumber), // Optional
2376
+ * });
2377
+ * type User = typeof User.Type;
2378
+ * ```
2379
+ *
2380
+ * #### Allowing Additional Properties
2381
+ *
2382
+ * ```ts
2383
+ * const UserWithAnyExtraProperties = object(
2384
+ * {
2385
+ * name: NonEmptyString,
2386
+ * age: PositiveNumber,
2387
+ * },
2388
+ * record(String, Unknown),
2389
+ * );
2390
+ *
2391
+ * expect(
2392
+ * UserWithAnyExtraProperties.from({ name: "a", age: 1, foo: 1 }),
2393
+ * ).toEqual({
2394
+ * ok: true,
2395
+ * value: { age: 1, foo: 1, name: "a" },
2396
+ * });
2397
+ * ```
2398
+ *
2399
+ * #### Combining Fixed and Flexible Properties
2400
+ *
2401
+ * ```ts
2402
+ * const NumberDictionary = object(
2403
+ * { length: Number },
2404
+ * record(String, Number),
2405
+ * );
2406
+ *
2407
+ * const validInput = {
2408
+ * length: 5,
2409
+ * extraKey1: 10,
2410
+ * extraKey2: 15,
2411
+ * };
2412
+ *
2413
+ * const fromResult = NumberDictionary.from(validInput);
2414
+ * expect(fromResult).toEqual(ok(validInput));
2415
+ *
2416
+ * const invalidInput = {
2417
+ * length: 5,
2418
+ * extraKey1: "not a number",
2419
+ * extraKey2: 15,
2420
+ * };
2421
+ *
2422
+ * const invalidFromResult = NumberDictionary.fromUnknown(invalidInput);
2423
+ * expect(invalidFromResult).toEqual(
2424
+ * err({
2425
+ * type: "Object",
2426
+ * value: invalidInput,
2427
+ * reason: {
2428
+ * kind: "IndexValue",
2429
+ * key: "extraKey1",
2430
+ * error: { type: "Number", value: "not a number" },
2431
+ * },
2432
+ * }),
2433
+ * );
2434
+ * ```
2435
+ *
2436
+ * @category Base Factories
2437
+ * @category Object
2438
+ */
2439
+ export function object<Props extends Record<string, AnyType>>(
2440
+ props: Props,
2441
+ ): ObjectType<Props>;
2442
+
2443
+ export function object<
2444
+ Props extends Record<string, AnyType>,
2445
+ KeyName extends TypeName,
2446
+ KeyT extends string,
2447
+ KeyInput extends string,
2448
+ KeyError extends TypeError,
2449
+ KeyParent extends string,
2450
+ KeyParentError extends TypeError,
2451
+ Value extends AnyType,
2452
+ >(
2453
+ props: Props,
2454
+ record: RecordType<
2455
+ KeyName,
2456
+ KeyT,
2457
+ KeyInput,
2458
+ KeyError,
2459
+ KeyParent,
2460
+ KeyParentError,
2461
+ Value
2462
+ >,
2463
+ ): ObjectWithRecordType<
2464
+ Props,
2465
+ KeyName,
2466
+ KeyT,
2467
+ KeyInput,
2468
+ KeyError,
2469
+ KeyParent,
2470
+ KeyParentError,
2471
+ Value
2472
+ >;
2473
+
2474
+ export function object(
2475
+ props: Record<string, AnyType>,
2476
+ record?: RecordType<any, any, any, any, any, any>,
2477
+ ): any {
2478
+ /* eslint-disable */
2479
+ const propKeys = Object.keys(props);
2480
+
2481
+ const fromUnknown = (
2482
+ value: unknown,
2483
+ ): Result<
2484
+ any,
2485
+ ObjectError<Record<string, any>> | ObjectWithRecordError<any, any, any>
2486
+ > => {
2487
+ if (!isPlainObject(value)) {
2488
+ return err({
2489
+ type: record ? "ObjectWithRecord" : "Object",
2490
+ value,
2491
+ reason: { kind: "NotObject" },
2492
+ });
2493
+ }
2494
+
2495
+ const errors: Record<string, any> = {};
2496
+ const result: Record<string, any> = {};
2497
+
2498
+ for (const key of propKeys) {
2499
+ if (!(key in value) && isOptionalType(props[key])) {
2500
+ continue;
2501
+ }
2502
+
2503
+ const propResult = props[key].fromUnknown(value[key]);
2504
+ if (!propResult.ok) {
2505
+ errors[key] = propResult.error as IntentionalNever;
2506
+ } else {
2507
+ result[key] = propResult.value as IntentionalNever;
2508
+ }
2509
+ }
2510
+
2511
+ const extraKeys = Object.keys(value).filter(
2512
+ (key) => !propKeys.includes(key),
2513
+ );
2514
+
2515
+ if (record) {
2516
+ for (const key of extraKeys) {
2517
+ const keyResult = record.key.fromUnknown(key);
2518
+ if (!keyResult.ok) {
2519
+ return err({
2520
+ type: "ObjectWithRecord",
2521
+ value,
2522
+ reason: {
2523
+ kind: "IndexKey",
2524
+ key,
2525
+ error: keyResult.error as IntentionalNever,
2526
+ },
2527
+ });
2528
+ }
2529
+
2530
+ const valueResult = record.value.fromUnknown(value[key]);
2531
+ if (!valueResult.ok) {
2532
+ return err({
2533
+ type: "ObjectWithRecord",
2534
+ value,
2535
+ reason: {
2536
+ kind: "IndexValue",
2537
+ key,
2538
+ error: valueResult.error as IntentionalNever,
2539
+ },
2540
+ });
2541
+ }
2542
+
2543
+ result[keyResult.value] = valueResult.value as IntentionalNever;
2544
+ }
2545
+ } else if (extraKeys.length > 0) {
2546
+ return err({
2547
+ type: "Object",
2548
+ value,
2549
+ reason: { kind: "ExtraKeys", extraKeys },
2550
+ });
2551
+ }
2552
+
2553
+ if (Object.keys(errors).length > 0) {
2554
+ return err({
2555
+ type: record ? "ObjectWithRecord" : "Object",
2556
+ value,
2557
+ reason: { kind: "Props", errors },
2558
+ });
2559
+ }
2560
+
2561
+ return ok(result);
2562
+ };
2563
+
2564
+ const to = (value: any) => {
2565
+ const entries: [string, any][] = [];
2566
+ for (const key of propKeys) {
2567
+ if (!(key in value) && isOptionalType(props[key])) {
2568
+ continue;
2569
+ }
2570
+ entries.push([key, props[key].to(value[key])]);
2571
+ }
2572
+ if (record) {
2573
+ const recordEntries = Object.entries(value).filter(
2574
+ ([key]) => !propKeys.includes(key),
2575
+ );
2576
+ for (const [key, val] of recordEntries) {
2577
+ entries.push([record.key.to(key), record.value.to(val)]);
2578
+ }
2579
+ }
2580
+ return Object.fromEntries(entries);
2581
+ };
2582
+
2583
+ const fromParent = (value: any): Result<any, any> => {
2584
+ const errors: Record<string, any> = {};
2585
+ const result: Record<string, any> = {};
2586
+
2587
+ for (const key of propKeys) {
2588
+ if (!(key in value) && isOptionalType(props[key])) {
2589
+ continue;
2590
+ }
2591
+ const propResult = props[key].fromParent(value[key]);
2592
+ if (!propResult.ok) {
2593
+ errors[key] = propResult.error;
2594
+ } else {
2595
+ result[key] = propResult.value;
2596
+ }
2597
+ }
2598
+
2599
+ if (record) {
2600
+ for (const [key, val] of Object.entries(value)) {
2601
+ if (!propKeys.includes(key)) {
2602
+ const keyResult = record.key.fromParent(key);
2603
+ if (!keyResult.ok) {
2604
+ return err({
2605
+ type: "ObjectWithRecord",
2606
+ value,
2607
+ reason: { kind: "IndexKey", key, error: keyResult.error },
2608
+ });
2609
+ }
2610
+ const valueResult = record.value.fromParent(val);
2611
+ if (!valueResult.ok) {
2612
+ return err({
2613
+ type: "ObjectWithRecord",
2614
+ value,
2615
+ reason: { kind: "IndexValue", key, error: valueResult.error },
2616
+ });
2617
+ }
2618
+ result[keyResult.value] = valueResult.value;
2619
+ }
2620
+ }
2621
+ }
2622
+
2623
+ if (Object.keys(errors).length > 0) {
2624
+ return err({
2625
+ type: record ? "ObjectWithRecord" : "Object",
2626
+ value,
2627
+ reason: { kind: "Props", errors },
2628
+ });
2629
+ }
2630
+
2631
+ return ok(result);
2632
+ };
2633
+
2634
+ const toParent = (value: any) => {
2635
+ const entries: [string, any][] = [];
2636
+ for (const key of propKeys) {
2637
+ if (!(key in value) && isOptionalType(props[key])) {
2638
+ continue;
2639
+ }
2640
+ entries.push([key, props[key].toParent(value[key])]);
2641
+ }
2642
+ if (record) {
2643
+ const recordEntries = Object.entries(value).filter(
2644
+ ([key]) => !propKeys.includes(key),
2645
+ );
2646
+ for (const [key, val] of recordEntries) {
2647
+ entries.push([record.key.toParent(key), record.value.toParent(val)]);
2648
+ }
2649
+ }
2650
+ return Object.fromEntries(entries);
2651
+ };
2652
+
2653
+ return {
2654
+ ...createType(record ? "ObjectWithRecord" : "Object", {
2655
+ fromUnknown,
2656
+ to,
2657
+ fromParent,
2658
+ toParent,
2659
+ }),
2660
+ props,
2661
+ ...(record ? { record } : {}),
2662
+ };
2663
+ /* eslint-enable */
2664
+ }
2665
+
2666
+ /**
2667
+ * ObjectType extends {@link Type} with an additional `props` property for
2668
+ * reflection.
2669
+ */
2670
+ export interface ObjectType<Props extends Record<string, AnyType>>
2671
+ extends Type<
2672
+ "Object",
2673
+ Readonly<ObjectT<Props>>,
2674
+ Readonly<ObjectInput<Props>>,
2675
+ ObjectError<{ [K in keyof Props]: InferError<Props[K]> }>,
2676
+ Readonly<ObjectParent<Props>>,
2677
+ ObjectError<{ [K in keyof Props]: InferParentError<Props[K]> }>
2678
+ > {
2679
+ readonly props: Props;
2680
+ }
2681
+
2682
+ type ObjectT<Props extends Record<string, AnyType>> = Simplify<
2683
+ {
2684
+ [K in RequiredKeys<Props>]: InferType<Props[K]>;
2685
+ } & {
2686
+ [K in OptionalKeys<Props>]?: Props[K] extends OptionalType<infer U>
2687
+ ? InferType<U>
2688
+ : never;
2689
+ }
2690
+ >;
2691
+
2692
+ type ObjectInput<Props extends Record<string, AnyType>> = Simplify<
2693
+ {
2694
+ [K in RequiredKeys<Props>]: InferInput<Props[K]>;
2695
+ } & {
2696
+ [K in OptionalKeys<Props>]?: Props[K] extends OptionalType<infer U>
2697
+ ? InferInput<U>
2698
+ : never;
2699
+ }
2700
+ >;
2701
+
2702
+ type ObjectParent<Props extends Record<string, AnyType>> = Simplify<
2703
+ {
2704
+ [K in RequiredKeys<Props>]: InferParent<Props[K]>;
2705
+ } & {
2706
+ [K in OptionalKeys<Props>]?: Props[K] extends OptionalType<infer U>
2707
+ ? InferParent<U>
2708
+ : never;
2709
+ }
2710
+ >;
2711
+
2712
+ type RequiredKeys<Props extends Record<string, AnyType>> = Exclude<
2713
+ keyof Props,
2714
+ OptionalKeys<Props>
2715
+ >;
2716
+
2717
+ type OptionalKeys<Props extends Record<string, AnyType>> = {
2718
+ [K in keyof Props]: Props[K] extends OptionalType<any> ? K : never;
2719
+ }[keyof Props];
2720
+
2721
+ export interface ObjectError<
2722
+ PropsErrors extends Record<string, TypeError> = Record<string, TypeError>,
2723
+ > extends TypeErrorWithReason<
2724
+ "Object",
2725
+ | { readonly kind: "NotObject" }
2726
+ | {
2727
+ readonly kind: "Props";
2728
+ readonly errors: Partial<PropsErrors>;
2729
+ }
2730
+ | { readonly kind: "ExtraKeys"; readonly extraKeys: ReadonlyArray<string> }
2731
+ > {}
2732
+
2733
+ /**
2734
+ * Merge Error and ParentError into one ObjectError so tooltips and error
2735
+ * messages are easier to read.
2736
+ *
2737
+ * @category Utilities
2738
+ */
2739
+ export type MergeObjectTypeErrors<T extends ObjectType<any>> =
2740
+ T extends ObjectType<infer Props>
2741
+ ? ObjectError<{ [K in keyof Props]: InferErrors<Props[K]> }>
2742
+ : never;
2743
+
2744
+ export const formatObjectError = <Error extends TypeError>(
2745
+ formatTypeError: TypeErrorFormatter<Error>,
2746
+ ): TypeErrorFormatter<ObjectError<Record<string, Error>>> =>
2747
+ createTypeErrorFormatter((error) => {
2748
+ switch (error.reason.kind) {
2749
+ case "NotObject":
2750
+ return `Expected a plain object but received ${error.value}`;
2751
+ case "ExtraKeys":
2752
+ return `Unexpected extra keys: ${error.reason.extraKeys.join(", ")}`;
2753
+ case "Props": {
2754
+ const formattedErrors = Object.entries(error.reason.errors)
2755
+ .filter(([, error]) => error !== undefined)
2756
+ .map(([key, error]) => `- ${key}: ${formatTypeError(error!)}`)
2757
+ .join("\n");
2758
+ return `Invalid object properties:\n${formattedErrors}`;
2759
+ }
2760
+ }
2761
+ });
2762
+
2763
+ /**
2764
+ * ObjectWithRecordType extends {@link Type} with additional `props` and `record`
2765
+ * properties for reflection.
2766
+ */
2767
+ export interface ObjectWithRecordType<
2768
+ Props extends Record<string, AnyType>,
2769
+ KeyName extends TypeName,
2770
+ KeyT extends string,
2771
+ KeyInput extends string,
2772
+ KeyError extends TypeError,
2773
+ KeyParent extends string,
2774
+ KeyParentError extends TypeError,
2775
+ Value extends AnyType,
2776
+ > extends Type<
2777
+ "ObjectWithRecord",
2778
+ Readonly<ObjectT<Props>> & Readonly<Record<KeyT, InferType<Value>>>,
2779
+ Readonly<ObjectInput<Props>> &
2780
+ Readonly<Record<KeyInput, InferInput<Value>>>,
2781
+ ObjectWithRecordError<
2782
+ { [K in keyof Props]: InferError<Props[K]> },
2783
+ KeyError,
2784
+ InferError<Value>
2785
+ >,
2786
+ Readonly<ObjectParent<Props>> &
2787
+ Readonly<Record<KeyParent, InferParent<Value>>>,
2788
+ ObjectWithRecordError<
2789
+ { [K in keyof Props]: InferParentError<Props[K]> },
2790
+ KeyParentError,
2791
+ InferParentError<Value>
2792
+ >
2793
+ > {
2794
+ readonly props: Props;
2795
+ readonly record: RecordType<
2796
+ KeyName,
2797
+ KeyT,
2798
+ KeyInput,
2799
+ KeyError,
2800
+ KeyParent,
2801
+ KeyParentError,
2802
+ Value
2803
+ >;
2804
+ }
2805
+
2806
+ export interface ObjectWithRecordError<
2807
+ PropsErrors extends Record<string, TypeError> = Record<string, TypeError>,
2808
+ KeyError extends TypeError = TypeError,
2809
+ ValueError extends TypeError = TypeError,
2810
+ > extends TypeErrorWithReason<
2811
+ "ObjectWithRecord",
2812
+ | { readonly kind: "NotObject" }
2813
+ | { readonly kind: "Props"; readonly errors: Partial<PropsErrors> }
2814
+ | {
2815
+ readonly kind: "IndexKey";
2816
+ readonly key: unknown;
2817
+ readonly error: KeyError;
2818
+ }
2819
+ | {
2820
+ readonly kind: "IndexValue";
2821
+ readonly key: string;
2822
+ readonly error: ValueError;
2823
+ }
2824
+ > {}
2825
+
2826
+ export const formatObjectWithRecordError = <Error extends TypeError>(
2827
+ formatTypeError: TypeErrorFormatter<Error>,
2828
+ ): TypeErrorFormatter<
2829
+ ObjectWithRecordError<Record<string, Error>, Error, Error>
2830
+ > =>
2831
+ createTypeErrorFormatter((error) => {
2832
+ switch (error.reason.kind) {
2833
+ case "NotObject":
2834
+ return `Expected an object, but received ${error.value}.`;
2835
+ case "Props":
2836
+ return formatObjectError(formatTypeError)({
2837
+ type: "Object",
2838
+ value: error.value,
2839
+ reason: { kind: "Props", errors: error.reason.errors },
2840
+ });
2841
+ case "IndexKey":
2842
+ return `Invalid index key ${error.reason.key}: ${formatTypeError(error.reason.error)}`;
2843
+ case "IndexValue":
2844
+ return `Invalid value at index key ${error.reason.key}: ${formatTypeError(error.reason.error)}`;
2845
+ }
2846
+ });
2847
+
2848
+ /**
2849
+ * Union {@link Type}.
2850
+ *
2851
+ * `UnionType` represents a union of multiple member Types. Accepts both
2852
+ * {@link Type} and literal values as arguments.
2853
+ *
2854
+ * Note that the `union` Type Factory delegates `fromParent` to `fromUnknown`.
2855
+ * That's because the union members can have different `Parent` types, and at
2856
+ * runtime, it is impossible to determine which member should process a given
2857
+ * `Parent` value.
2858
+ *
2859
+ * ### Example
2860
+ *
2861
+ * ```ts
2862
+ * const AorB = union("a", "b");
2863
+ * const result1 = AorB.from("a"); // ok("a")
2864
+ * const result2 = AorB.from("c"); // err
2865
+ *
2866
+ * const StringOrNumber = union(String, Number);
2867
+ * const result3 = StringOrNumber.from(42); // ok(42)
2868
+ * ```
2869
+ *
2870
+ * @category Base Factories
2871
+ */
2872
+ export function union<
2873
+ Members extends [AnyType, AnyType, ...ReadonlyArray<AnyType>],
2874
+ >(...members: Members): UnionType<Members>;
2875
+
2876
+ export function union<
2877
+ Literals extends [Literal, Literal, ...ReadonlyArray<Literal>],
2878
+ >(
2879
+ ...literals: Literals
2880
+ ): UnionType<{
2881
+ [K in keyof Literals]: LiteralType<Literals[K]>;
2882
+ }>;
2883
+
2884
+ export function union(...args: ReadonlyArray<any>): any {
2885
+ /**
2886
+ * Good First Issue:
2887
+ *
2888
+ * 1. Optimize for microbenchmarks:
2889
+ *
2890
+ * - Detect if all arguments are literals and generate a fast-check function.
2891
+ * - This could significantly improve performance for unions of literals.
2892
+ * 2. Enhance tagged union support:
2893
+ *
2894
+ * - Detect if all arguments are objects with the same property but different
2895
+ * literal values (tagged unions).
2896
+ * - Generate a specialized function to improve validation performance for such
2897
+ * cases.
2898
+ */
2899
+
2900
+ /* eslint-disable */
2901
+ const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
2902
+
2903
+ const fromUnknown = (value: unknown) => {
2904
+ const errors: UnionError<InferError<(typeof members)[number]>>["errors"] =
2905
+ [];
2906
+ for (const member of members) {
2907
+ const result = member.fromUnknown(value);
2908
+ if (result.ok) return result;
2909
+ errors.push(result.error);
2910
+ }
2911
+ return err<UnionError<InferError<(typeof members)[number]>>>({
2912
+ type: "Union",
2913
+ value,
2914
+ errors,
2915
+ });
2916
+ };
2917
+
2918
+ const to = (value: any) => {
2919
+ for (const member of members) {
2920
+ if (member.is(value)) return member.to(value);
2921
+ }
2922
+ assert(false, "No matching member found in Union Type `to` function");
2923
+ };
2924
+
2925
+ return {
2926
+ ...createType("Union", {
2927
+ fromUnknown,
2928
+ to,
2929
+ fromParent: fromUnknown,
2930
+ toParent: to,
2931
+ }),
2932
+ members,
2933
+ };
2934
+ /* eslint-enable */
2935
+ }
2936
+
2937
+ /**
2938
+ * UnionType extends {@link Type} with an additional `members` property for
2939
+ * reflection.
2940
+ */
2941
+ export interface UnionType<Members extends [AnyType, ...ReadonlyArray<AnyType>]>
2942
+ extends Type<
2943
+ "Union",
2944
+ InferType<Members[number]>,
2945
+ InferInput<Members[number]>,
2946
+ UnionError<InferErrors<Members[number]>>,
2947
+ InferInput<Members[number]>,
2948
+ never
2949
+ > {
2950
+ readonly members: Members;
2951
+ }
2952
+
2953
+ export interface UnionError<E extends TypeError = TypeError>
2954
+ extends TypeError<"Union"> {
2955
+ readonly errors: Array<E>;
2956
+ }
2957
+
2958
+ export const formatUnionError = <Error extends TypeError>(
2959
+ formatTypeError: TypeErrorFormatter<Error>,
2960
+ ): TypeErrorFormatter<UnionError<Error>> =>
2961
+ createTypeErrorFormatter((error) => {
2962
+ const formattedErrors = error.errors
2963
+ .map((e, i) => ` ${i + 1}. ${formatTypeError(e)}`)
2964
+ .join("\n");
2965
+
2966
+ return `Value ${error.value} does not match any member of the union.\nErrors:\n${formattedErrors}`;
2967
+ });
2968
+
2969
+ export const isUnionType = (
2970
+ t: AnyType,
2971
+ ): t is UnionType<[AnyType, ...ReadonlyArray<AnyType>]> =>
2972
+ t.name === "Union" && Array.isArray((t as { members?: unknown }).members);
2973
+
2974
+ /**
2975
+ * Recursive {@link Type}.
2976
+ *
2977
+ * Recursive types can't be inferred, so we must define them using an interface
2978
+ * and `recursive` Type Factory that returns a {@link Type}.
2979
+ *
2980
+ * ### Example
2981
+ *
2982
+ * ```ts
2983
+ * interface Category {
2984
+ * readonly name: string;
2985
+ * readonly subcategories: ReadonlyArray<Category>;
2986
+ * }
2987
+ *
2988
+ * interface CategoryInput {
2989
+ * readonly name: string;
2990
+ * readonly subcategories: ReadonlyArray<CategoryInput>;
2991
+ * }
2992
+ *
2993
+ * type CategoryError = ObjectError<{
2994
+ * readonly name: typeof String.Error;
2995
+ * readonly subcategories: ArrayError<CategoryError>;
2996
+ * }>;
2997
+ *
2998
+ * const Category = recursive(
2999
+ * (): Type<"Object", Category, CategoryInput, CategoryError> =>
3000
+ * object({
3001
+ * name: String,
3002
+ * subcategories: array(Category),
3003
+ * }),
3004
+ * );
3005
+ * ```
3006
+ *
3007
+ * @category Base Factories
3008
+ */
3009
+ export const recursive = <ParentType extends AnyType>(
3010
+ create: () => ParentType,
3011
+ ): RecursiveType<ParentType> => {
3012
+ let type: ParentType | undefined;
3013
+
3014
+ type T = InferType<ParentType>;
3015
+ type Input = InferInput<ParentType>;
3016
+ type Parent = InferParent<ParentType>;
3017
+
3018
+ return {
3019
+ name: "Recursive",
3020
+ from: (value: Input) => {
3021
+ type ??= create();
3022
+ return type.from(value);
3023
+ },
3024
+ fromUnknown: (value: unknown) => {
3025
+ type ??= create();
3026
+ return type.fromUnknown(value);
3027
+ },
3028
+ to: (value: T) => {
3029
+ type ??= create();
3030
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3031
+ return type.to(value);
3032
+ },
3033
+ fromParent: (value: Parent) => {
3034
+ type ??= create();
3035
+ return type.fromParent(value);
3036
+ },
3037
+ toParent: (value: T) => {
3038
+ type ??= create();
3039
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3040
+ return type.toParent(value);
3041
+ },
3042
+ is: (value: unknown): value is T => {
3043
+ type ??= create();
3044
+ return type.is(value);
3045
+ },
3046
+ [EvoluTypeSymbol]: true,
3047
+ getParentType: () => {
3048
+ type ??= create();
3049
+ return type;
3050
+ },
3051
+ } as RecursiveType<ParentType>;
3052
+ };
3053
+
3054
+ export interface RecursiveType<ParentType extends AnyType>
3055
+ extends Type<
3056
+ "Recursive",
3057
+ InferType<ParentType>,
3058
+ InferInput<ParentType>,
3059
+ InferError<ParentType>,
3060
+ InferParent<ParentType>,
3061
+ InferParentError<ParentType>
3062
+ > {
3063
+ getParentType(): ParentType;
3064
+ }
3065
+
3066
+ /**
3067
+ * `union(null, T)` {@link Type}.
3068
+ *
3069
+ * ### Example
3070
+ *
3071
+ * ```ts
3072
+ * const NullOrString = nullOr(String);
3073
+ * NullOrString.from("hello"); // ok("hello")
3074
+ * NullOrString.from(null); // ok(null)
3075
+ * NullOrString.from(42); // err(...)
3076
+ * ```
3077
+ *
3078
+ * @category Base Factories
3079
+ */
3080
+ export const nullOr = <T extends AnyType>(
3081
+ type: T,
3082
+ ): UnionType<[typeof Null, T]> => union(Null, type);
3083
+
3084
+ /**
3085
+ * `union(undefined, T)` {@link Type}.
3086
+ *
3087
+ * ### Example
3088
+ *
3089
+ * ```ts
3090
+ * const UndefinedOrString = undefinedOr(String);
3091
+ * UndefinedOrString.from("world"); // ok("world")
3092
+ * UndefinedOrString.from(undefined); // ok()
3093
+ * UndefinedOrString.from(42); // err(...)
3094
+ * ```
3095
+ *
3096
+ * @category Base Factories
3097
+ */
3098
+ export const undefinedOr = <T extends AnyType>(
3099
+ type: T,
3100
+ ): UnionType<[typeof Undefined, T]> => union(Undefined, type);
3101
+
3102
+ /**
3103
+ * `union(undefined, null, T)` {@link Type}.
3104
+ *
3105
+ * Learn more:
3106
+ * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing
3107
+ *
3108
+ * ### Example
3109
+ *
3110
+ * ```ts
3111
+ * const NullishOrString = nullishOr(String);
3112
+ * NullishOrString.from("test"); // ok("test")
3113
+ * NullishOrString.from(null); // ok(null)
3114
+ * NullishOrString.from(undefined); // ok()
3115
+ * NullishOrString.from(42); // err(...)
3116
+ * ```
3117
+ *
3118
+ * @category Base Factories
3119
+ */
3120
+ export const nullishOr = <T extends AnyType>(
3121
+ type: T,
3122
+ ): UnionType<[typeof Undefined, typeof Null, T]> =>
3123
+ union(Undefined, Null, type);
3124
+
3125
+ /**
3126
+ * Tuple {@link Type}.
3127
+ *
3128
+ * Represents a tuple of specific Types.
3129
+ *
3130
+ * ### Example
3131
+ *
3132
+ * ```ts
3133
+ * const NameAndAge = tuple(NonEmptyTrimmedString, PositiveNumber);
3134
+ *
3135
+ * const result = NameAndAge.from(["Alice", 25]); // ok(["Alice", 25])
3136
+ * const error = NameAndAge.from(["Alice", -10]); // err
3137
+ * ```
3138
+ *
3139
+ * @category Base Factories
3140
+ */
3141
+ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
3142
+ ...elements: Elements
3143
+ ): TupleType<Elements> => {
3144
+ const fromUnknown = (
3145
+ value: unknown,
3146
+ ): Result<
3147
+ { [K in keyof Elements]: InferType<Elements[K]> },
3148
+ TupleError<InferError<Elements[number]>>
3149
+ > => {
3150
+ if (!Array.isArray(value) || value.length !== elements.length) {
3151
+ return err({
3152
+ type: "Tuple",
3153
+ value,
3154
+ reason: { kind: "InvalidLength", expected: elements.length },
3155
+ });
3156
+ }
3157
+
3158
+ const result = [] as { [K in keyof Elements]: InferType<Elements[K]> };
3159
+
3160
+ for (let i = 0; i < elements.length; i++) {
3161
+ const elementResult = elements[i].fromUnknown(value[i]);
3162
+ if (!elementResult.ok) {
3163
+ return err({
3164
+ type: "Tuple",
3165
+ value,
3166
+ reason: {
3167
+ kind: "Element",
3168
+ index: i,
3169
+ error: elementResult.error as IntentionalNever,
3170
+ },
3171
+ });
3172
+ }
3173
+ result[i] = elementResult.value as InferType<Elements[typeof i]>;
3174
+ }
3175
+
3176
+ return ok(result);
3177
+ };
3178
+
3179
+ const to = (value: { [K in keyof Elements]: InferType<Elements[K]> }) =>
3180
+ value.map((val, index) => elements[index].to(val) as IntentionalNever);
3181
+
3182
+ const fromParent = (value: {
3183
+ [K in keyof Elements]: InferParent<Elements[K]>;
3184
+ }) => {
3185
+ if (!Array.isArray(value) || value.length !== elements.length) {
3186
+ return err<TupleError<InferParentError<Elements[number]>>>({
3187
+ type: "Tuple",
3188
+ value,
3189
+ reason: { kind: "InvalidLength", expected: elements.length },
3190
+ });
3191
+ }
3192
+
3193
+ const result: Array<unknown> = [];
3194
+ for (let i = 0; i < elements.length; i++) {
3195
+ const elementResult = elements[i].fromParent(value[i]);
3196
+ if (!elementResult.ok) {
3197
+ return err<TupleError<InferParentError<Elements[number]>>>({
3198
+ type: "Tuple",
3199
+ value,
3200
+ reason: {
3201
+ kind: "Element",
3202
+ index: i,
3203
+ error: elementResult.error as IntentionalNever,
3204
+ },
3205
+ });
3206
+ }
3207
+ result.push(elementResult.value);
3208
+ }
3209
+
3210
+ return ok(result as { [K in keyof Elements]: InferType<Elements[K]> });
3211
+ };
3212
+
3213
+ const toParent = (value: { [K in keyof Elements]: InferType<Elements[K]> }): {
3214
+ [K in keyof Elements]: InferParent<Elements[K]>;
3215
+ } =>
3216
+ value.map(
3217
+ (val, index) => elements[index].toParent(val) as IntentionalNever,
3218
+ ) as {
3219
+ [K in keyof Elements]: InferParent<Elements[K]>;
3220
+ };
3221
+
3222
+ return {
3223
+ ...createType("Tuple", {
3224
+ fromUnknown,
3225
+ to,
3226
+ fromParent,
3227
+ toParent,
3228
+ }),
3229
+ elements,
3230
+ } as IntentionalNever;
3231
+ };
3232
+
3233
+ /**
3234
+ * TupleType extends {@link Type} with an additional `elements` property for
3235
+ * reflection.
3236
+ */
3237
+ export interface TupleType<
3238
+ Elements extends readonly [AnyType, ...ReadonlyArray<AnyType>],
3239
+ > extends Type<
3240
+ "Tuple",
3241
+ readonly [...{ [K in keyof Elements]: InferType<Elements[K]> }],
3242
+ readonly [...{ [K in keyof Elements]: InferInput<Elements[K]> }],
3243
+ TupleError<{ [K in keyof Elements]: InferError<Elements[K]> }[number]>,
3244
+ readonly [...{ [K in keyof Elements]: InferParent<Elements[K]> }],
3245
+ TupleError<{ [K in keyof Elements]: InferParentError<Elements[K]> }[number]>
3246
+ > {
3247
+ readonly elements: Elements;
3248
+ }
3249
+
3250
+ export interface TupleError<ElementError extends TypeError = TypeError>
3251
+ extends TypeErrorWithReason<
3252
+ "Tuple",
3253
+ | { readonly kind: "InvalidLength"; readonly expected: number }
3254
+ | {
3255
+ readonly kind: "Element";
3256
+ readonly index: number;
3257
+ readonly error: ElementError;
3258
+ }
3259
+ > {}
3260
+
3261
+ export const formatTupleError = <Error extends TypeError>(
3262
+ formatTypeError: TypeErrorFormatter<Error>,
3263
+ ): TypeErrorFormatter<TupleError<Error>> =>
3264
+ createTypeErrorFormatter((error) => {
3265
+ switch (error.reason.kind) {
3266
+ case "InvalidLength":
3267
+ return `Expected a tuple of length ${error.reason.expected}, but received ${error.value}.`;
3268
+ case "Element":
3269
+ return `Invalid element at index ${error.reason.index}:\n ${formatTypeError(error.reason.error)}`;
3270
+ }
3271
+ });
3272
+
3273
+ /**
3274
+ * 64-bit signed integer.
3275
+ *
3276
+ * `Int64` represents a `BigInt` constrained to a 64-bit signed integer range,
3277
+ * which is useful for platforms that do not support the `bigint` type, such as
3278
+ * SQLite.
3279
+ *
3280
+ * Because SQLite lacks a dedicated `bigint` type, it may return `number` or
3281
+ * 'Int64` depending on the stored value or even a wrong value if a platform
3282
+ * wrapper does not support it. A workaround for SQLite is to insert 'Int64`
3283
+ * serialized as a string (SQLite will convert it to int) and manually cast the
3284
+ * result to a string in SQL query and then to `Int64` in JS.
3285
+ *
3286
+ * https://www.sqlite.org/c3ref/int64.html
3287
+ *
3288
+ * @category Number
3289
+ */
3290
+ export const Int64 = brand("Int64", BigInt, (value) =>
3291
+ value >= -9223372036854775808n && value <= 9223372036854775807n
3292
+ ? ok(value)
3293
+ : err<Int64Error>({ type: "Int64", value }),
3294
+ );
3295
+ export type Int64 = typeof Int64.Type;
3296
+ export interface Int64Error extends TypeError<"Int64"> {}
3297
+
3298
+ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
3299
+ (error) =>
3300
+ `The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
3301
+ );
3302
+
3303
+ export const BigIntFromString = transform(
3304
+ String,
3305
+ BigInt,
3306
+ (value) =>
3307
+ trySync(
3308
+ () => globalThis.BigInt(value),
3309
+ (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
3310
+ ),
3311
+ (value) => value.toString(),
3312
+ );
3313
+
3314
+ export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
3315
+
3316
+ export const formatBigIntFromStringError =
3317
+ createTypeErrorFormatter<BigIntFromStringError>(
3318
+ (error) => `The value ${error.value} could not be converted to a BigInt.`,
3319
+ );
3320
+
3321
+ /**
3322
+ * Stringified {@link Int64}.
3323
+ *
3324
+ * @category Number
3325
+ */
3326
+ export const Int64String = brand("Int64", String, (value) => {
3327
+ const bigint = BigIntFromString.fromParent(value);
3328
+ if (bigint.ok) {
3329
+ const int64 = Int64.fromParent(globalThis.BigInt(value));
3330
+ if (int64.ok) return ok(value);
3331
+ }
3332
+ return err<Int64StringError>({ type: "Int64String", value });
3333
+ });
3334
+
3335
+ export type Int64String = typeof Int64String.Type;
3336
+
3337
+ export interface Int64StringError extends TypeError<"Int64String"> {}
3338
+
3339
+ export const formatInt64StringError =
3340
+ createTypeErrorFormatter<Int64StringError>(
3341
+ (error) => `The value ${error.value} is not a valid Int64 string.`,
3342
+ );
3343
+
3344
+ export type JsonValue =
3345
+ | string
3346
+ | FiniteNumber
3347
+ | boolean
3348
+ | null
3349
+ | JsonArray
3350
+ | JsonObject;
3351
+
3352
+ export type JsonValueInput =
3353
+ | string
3354
+ | number
3355
+ | boolean
3356
+ | null
3357
+ | JsonArrayInput
3358
+ | JsonObjectInput;
3359
+
3360
+ export type JsonValueError = UnionError<
3361
+ | StringError
3362
+ | BooleanError
3363
+ | NullError
3364
+ | FiniteError
3365
+ | NumberError
3366
+ | ArrayError<JsonValueError>
3367
+ | RecordError<StringError, JsonValueError>
3368
+ >;
3369
+
3370
+ export interface JsonObject {
3371
+ readonly [key: string]: JsonValue;
3372
+ }
3373
+
3374
+ export interface JsonObjectInput {
3375
+ readonly [key: string]: JsonValueInput;
3376
+ }
3377
+
3378
+ export type JsonArray = ReadonlyArray<JsonValue>;
3379
+ export type JsonArrayInput = ReadonlyArray<JsonValueInput>;
3380
+
3381
+ /**
3382
+ * JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
3383
+ * {@link JsonArray}, or {@link JsonObject}.
3384
+ *
3385
+ * @category Base Types
3386
+ */
3387
+ export const JsonValue = recursive(
3388
+ (): UnionType<
3389
+ [
3390
+ typeof String,
3391
+ typeof FiniteNumber,
3392
+ typeof Boolean,
3393
+ typeof Null,
3394
+ ArrayType<
3395
+ Type<
3396
+ "Recursive",
3397
+ JsonValue,
3398
+ JsonValueInput,
3399
+ JsonValueError,
3400
+ JsonValueInput,
3401
+ JsonValueError
3402
+ >
3403
+ >,
3404
+ RecordType<
3405
+ "String",
3406
+ string,
3407
+ string,
3408
+ StringError,
3409
+ string,
3410
+ StringError,
3411
+ Type<
3412
+ "Recursive",
3413
+ JsonValue,
3414
+ JsonValueInput,
3415
+ JsonValueError,
3416
+ JsonValueInput,
3417
+ JsonValueError
3418
+ >
3419
+ >,
3420
+ ]
3421
+ > => union(String, FiniteNumber, Boolean, Null, JsonArray, JsonObject),
3422
+ );
3423
+
3424
+ /**
3425
+ * JSON-compatible array of {@link JsonValue} elements.
3426
+ *
3427
+ * @category Array
3428
+ */
3429
+ export const JsonArray = array(JsonValue);
3430
+
3431
+ /**
3432
+ * JSON-compatible object with string keys and {@link JsonValue} values.
3433
+ *
3434
+ * @category Object
3435
+ */
3436
+ export const JsonObject = record(String, JsonValue);
3437
+
3438
+ /**
3439
+ * Transform Type that parses a JSON into a {@link JsonValue} and serializes a
3440
+ * JsonValue back into a JSON string.
3441
+ *
3442
+ * ### Example
3443
+ *
3444
+ * ```ts
3445
+ * JsonValueFromString.from(`{"key":"value"}`); // -> ok({ key: "value" })
3446
+ * JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
3447
+ * ```
3448
+ *
3449
+ * @category String
3450
+ */
3451
+ export const JsonValueFromString = transform(
3452
+ String,
3453
+ JsonValue,
3454
+ (value) =>
3455
+ trySync(
3456
+ () => JSON.parse(value) as JsonValue,
3457
+ (error): JsonValueFromStringError => ({
3458
+ type: `JsonValueFromString`,
3459
+ value,
3460
+ message: globalThis.String(error),
3461
+ }),
3462
+ ),
3463
+ (value) => JSON.stringify(value),
3464
+ );
3465
+
3466
+ export interface JsonValueFromStringError
3467
+ extends TypeError<"JsonValueFromString"> {
3468
+ readonly message: string;
3469
+ }
3470
+
3471
+ export const formatJsonValueFromStringError =
3472
+ createTypeErrorFormatter<JsonValueFromStringError>(
3473
+ (error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`,
3474
+ );
3475
+
3476
+ /**
3477
+ * JSON-string {@link Type}.
3478
+ *
3479
+ * ### Example
3480
+ *
3481
+ * ```ts
3482
+ * const result = Json.from('{"key":"value"}'); // -> ok('{"key":"value"}')
3483
+ * const error = Json.from("invalid json"); // -> err({ type: "Json", value: "invalid json", message: "Unexpected token i in JSON at position 0" })
3484
+ * ```
3485
+ *
3486
+ * @category String
3487
+ */
3488
+ export const Json = brand("Json", String, (value) => {
3489
+ const result = JsonValueFromString.fromParent(value);
3490
+ if (!result.ok)
3491
+ return err<JsonError>({
3492
+ type: "Json",
3493
+ value,
3494
+ message: result.error.message,
3495
+ });
3496
+ return ok(value);
3497
+ });
3498
+
3499
+ export type Json = typeof Json.Type;
3500
+
3501
+ export interface JsonError extends TypeError<"Json"> {
3502
+ readonly message: string;
3503
+ }
3504
+
3505
+ export const formatJsonError = createTypeErrorFormatter<JsonError>(
3506
+ (error) => `Invalid JSON: ${error.value}. Error: ${error.message}`,
3507
+ );
3508
+
3509
+ /**
3510
+ * Optional {@link Type}.
3511
+ *
3512
+ * Marks a `Type` as **optional**, meaning:
3513
+ *
3514
+ * - If the value is **present**, it must match the given `Type`.
3515
+ * - If the value is **absent**, it is **not included** in the final object.
3516
+ *
3517
+ * This is different from {@link undefinedOr}, which allows explicit `undefined`
3518
+ * but **still requires the key to exist**.
3519
+ *
3520
+ * ### Example:
3521
+ *
3522
+ * ```ts
3523
+ * const Todo = object({
3524
+ * id: TodoId,
3525
+ * title: NonEmptyString1000,
3526
+ * isCompleted: optional(SqliteBoolean),
3527
+ * });
3528
+ * ```
3529
+ */
3530
+ export const optional = <T extends AnyType>(type: T): OptionalType<T> => ({
3531
+ ...createType("Optional", {
3532
+ fromUnknown: type.fromUnknown,
3533
+ to: type.to,
3534
+ fromParent: type.fromParent,
3535
+ toParent: type.toParent,
3536
+ }),
3537
+ parent: type,
3538
+ });
3539
+
3540
+ export interface OptionalType<T extends AnyType>
3541
+ extends Type<
3542
+ "Optional",
3543
+ InferType<T>,
3544
+ InferInput<T>,
3545
+ InferError<T>,
3546
+ InferParent<T>,
3547
+ InferParentError<T>
3548
+ > {
3549
+ readonly parent: T;
3550
+ }
3551
+
3552
+ /** Determines if a given type is an {@link OptionalType}. */
3553
+ export const isOptionalType = (x: unknown): x is OptionalType<any> =>
3554
+ typeof x === "object" && x != null && "name" in x && x.name === "Optional";
3555
+
3556
+ /**
3557
+ * Creates a partial object type where all properties are optional.
3558
+ *
3559
+ * This is useful when you want to validate an object in which none of the keys
3560
+ * are required, but if they are present they must conform to their
3561
+ * corresponding Types.
3562
+ *
3563
+ * ### Example
3564
+ *
3565
+ * ```ts
3566
+ * const PartialUser = partial({
3567
+ * name: NonEmptyString,
3568
+ * age: PositiveNumber,
3569
+ * });
3570
+ *
3571
+ * // Valid: an empty object is accepted
3572
+ * PartialUser.from({});
3573
+ *
3574
+ * // Valid: when provided, the properties must validate correctly
3575
+ * PartialUser.from({ name: "Alice" });
3576
+ *
3577
+ * // Invalid: if a property is present but fails validation it returns an error
3578
+ * PartialUser.from({ age: -5 });
3579
+ * ```
3580
+ *
3581
+ * @category Object
3582
+ */
3583
+ export const partial = <Props extends Record<string, AnyType>>(
3584
+ props: Props,
3585
+ ): ObjectType<{ [K in keyof Props]: OptionalType<Props[K]> }> => {
3586
+ const optionalProps = {} as { [K in keyof Props]: OptionalType<Props[K]> };
3587
+ for (const key in props) {
3588
+ if (Object.prototype.hasOwnProperty.call(props, key)) {
3589
+ optionalProps[key] = optional(props[key]);
3590
+ }
3591
+ }
3592
+ return object(optionalProps);
3593
+ };
3594
+
3595
+ /**
3596
+ * Converts each “nullable” property (a union that includes {@link Null}) into an
3597
+ * {@link optional} property. This means consumers can omit the property
3598
+ * entirely, or set it to `null`, or set it to the non-null member of the
3599
+ * union.
3600
+ *
3601
+ * @category Object
3602
+ */
3603
+ export const nullableToOptional = <Props extends Record<string, AnyType>>(
3604
+ props: Props,
3605
+ ): ObjectType<NullableToOptionalProps<Props>> => {
3606
+ const transformedProps: Record<string, AnyType> = {};
3607
+ for (const key in props) {
3608
+ const type = props[key];
3609
+ if (isUnionType(type)) {
3610
+ const hasNull = type.members.some((m) => m === Null);
3611
+ if (hasNull) {
3612
+ transformedProps[key] = optional(type);
3613
+ continue;
3614
+ }
3615
+ }
3616
+ transformedProps[key] = type;
3617
+ }
3618
+ return object(transformedProps) as ObjectType<NullableToOptionalProps<Props>>;
3619
+ };
3620
+
3621
+ export type NullableToOptionalProps<Props extends Record<string, AnyType>> = {
3622
+ [K in keyof Props]: TransformNullable<Props[K]>;
3623
+ };
3624
+
3625
+ export type TransformNullable<P extends AnyType> =
3626
+ IsUnionWithNull<P> extends true ? OptionalType<P> : P;
3627
+
3628
+ export type IsUnionWithNull<U extends AnyType> =
3629
+ U extends UnionType<infer Members>
3630
+ ? Members extends [AnyType, ...Array<AnyType>]
3631
+ ? NullTypeInMembers<Members>
3632
+ : false
3633
+ : false;
3634
+
3635
+ export type NullTypeInMembers<Members extends [AnyType, ...Array<AnyType>]> =
3636
+ Members extends [infer Head, ...infer Tail]
3637
+ ? Head extends typeof Null
3638
+ ? true
3639
+ : Tail extends [AnyType, ...Array<AnyType>]
3640
+ ? NullTypeInMembers<Tail>
3641
+ : false
3642
+ : false;
3643
+
3644
+ /**
3645
+ * Create a new `object` {@link Type} by omitting some keys.
3646
+ *
3647
+ * @category Object
3648
+ */
3649
+ export function omit<T extends ObjectType<any>, Keys extends keyof T["props"]>(
3650
+ objectType: T,
3651
+ ...keys: ReadonlyArray<Keys>
3652
+ ): ObjectType<Omit<T["props"], Keys>> {
3653
+ const newProps = {} as Omit<T["props"], Keys>;
3654
+
3655
+ for (const key in objectType.props) {
3656
+ if (!keys.includes(key as Keys)) {
3657
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
3658
+ newProps[key as keyof typeof newProps] = objectType.props[key];
3659
+ }
3660
+ }
3661
+ return object(newProps);
3662
+ }
3663
+
3664
+ /**
3665
+ * Creates a transform Type that serializes a given `Type` into a branded JSON
3666
+ * string. The transformation is reversible, ensuring that we can safely parse
3667
+ * it back.
3668
+ *
3669
+ * ### Example
3670
+ *
3671
+ * ```ts
3672
+ * const Person = object({
3673
+ * name: NonEmptyString50,
3674
+ * age: FiniteNumber,
3675
+ * });
3676
+ * type Person = typeof Person.Type;
3677
+ *
3678
+ * const PersonJson = json(Person, "PersonJson");
3679
+ * // string & Brand<"PersonJson">
3680
+ * type PersonJson = typeof PersonJson.Type;
3681
+ *
3682
+ * // Person -> string & Brand<"PersonJson">
3683
+ * const personJson = PersonJson.from({ name: "Alice", age: 30 });
3684
+ * expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
3685
+ *
3686
+ * // string & Brand<"PersonJson"> -> Person
3687
+ * const person = PersonJson.to(personJson);
3688
+ *
3689
+ * // serialize/parse any JSON value
3690
+ * const AnyJson = json(JsonValue, "AnyJson");
3691
+ * ```
3692
+ */
3693
+ export const json = <T extends AnyType, Name extends TypeName>(
3694
+ type: T,
3695
+ name: Name,
3696
+ ): TransformType<
3697
+ T,
3698
+ BrandType<
3699
+ typeof String,
3700
+ Name,
3701
+ JsonValueFromStringError | T["Errors"],
3702
+ StringError
3703
+ >
3704
+ > => {
3705
+ type E = JsonValueFromStringError | T["Errors"];
3706
+
3707
+ const BrandedJsonString: BrandType<typeof String, Name, E, StringError> =
3708
+ brand(name, String, (value) => {
3709
+ const jsonValue = JsonValueFromString.fromParent(value);
3710
+ if (!jsonValue.ok) return jsonValue as Err<E>;
3711
+
3712
+ const parsed = type.fromUnknown(jsonValue.value);
3713
+ if (!parsed.ok) return parsed as Err<E>;
3714
+
3715
+ return ok(value as InferType<typeof BrandedJsonString>);
3716
+ });
3717
+
3718
+ return transform(
3719
+ type,
3720
+ BrandedJsonString,
3721
+ (value) => ok(JSON.stringify(value) as InferType<typeof BrandedJsonString>),
3722
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3723
+ (value) => JSON.parse(value) as InferType<T>,
3724
+ );
3725
+ };
3726
+
3727
+ /**
3728
+ * Union of all `TypeError`s defined in the `Type.ts` file, including base type
3729
+ * errors (e.g., `StringError`, `NumberError`), composite type errors
3730
+ * (`ArrayError`, `ObjectError`), and optionally, user-defined extra errors.
3731
+ *
3732
+ * This type is **recursive**, meaning errors can be nested within composite
3733
+ * structures like arrays, objects, records, unions, and tuples.
3734
+ *
3735
+ * Used by {@link createFormatTypeError} to generate human-readable error
3736
+ * messages.
3737
+ *
3738
+ * @category Utilities
3739
+ */
3740
+ export type TypeErrors<ExtraErrors extends TypeError = never> =
3741
+ | StringError
3742
+ | NumberError
3743
+ | BigIntError
3744
+ | BooleanError
3745
+ | UndefinedError
3746
+ | NullError
3747
+ | FunctionError
3748
+ | Uint8ArrayError
3749
+ | InstanceOfError
3750
+ | EvoluTypeError
3751
+ | CurrencyCodeError
3752
+ | DateIsoStringError
3753
+ | TrimmedError
3754
+ | MinLengthError
3755
+ | MaxLengthError
3756
+ | LengthError
3757
+ | MnemonicError
3758
+ | RegexError
3759
+ | NanoIdError
3760
+ | SimplePasswordError
3761
+ | IdError
3762
+ | PositiveError
3763
+ | NegativeError
3764
+ | NonPositiveError
3765
+ | NonNegativeError
3766
+ | IntError
3767
+ | GreaterThanError
3768
+ | LessThanError
3769
+ | GreaterThanOrEqualToError
3770
+ | LessThanOrEqualToError
3771
+ | NonNaNError
3772
+ | FiniteError
3773
+ | MultipleOfError
3774
+ | BetweenError
3775
+ | LiteralError
3776
+ | Int64Error
3777
+ | BigIntFromStringError
3778
+ | Int64StringError
3779
+ | JsonValueFromStringError
3780
+ | JsonError
3781
+ | ExtraErrors
3782
+ // Composite errors
3783
+ | ArrayError<TypeErrors<ExtraErrors>>
3784
+ | RecordError<TypeErrors<ExtraErrors>, TypeErrors<ExtraErrors>>
3785
+ | ObjectError<Record<string, TypeErrors<ExtraErrors>>>
3786
+ | ObjectWithRecordError<
3787
+ Record<string, TypeErrors<ExtraErrors>>,
3788
+ TypeErrors<ExtraErrors>,
3789
+ TypeErrors<ExtraErrors>
3790
+ >
3791
+ | UnionError<TypeErrors<ExtraErrors>>
3792
+ | TupleError<TypeErrors<ExtraErrors>>;
3793
+
3794
+ /**
3795
+ * Creates a unified error formatter that handles both Evolu Type's built-in
3796
+ * {@link TypeErrors} and custom errors. It also lets us override the default
3797
+ * formatting for specific errors.
3798
+ *
3799
+ * If you prefer not to reuse any built-in error formatters, you can write your
3800
+ * own `formatTypeError` function from scratch.
3801
+ *
3802
+ * ### Examples
3803
+ *
3804
+ * ```ts
3805
+ * const formatError = createFormatTypeError();
3806
+ * console.log(formatError({ type: "String", value: 42 }));
3807
+ * // "A value 42 is not a string."
3808
+ * ```
3809
+ *
3810
+ * A custom `formatTypeError` function:
3811
+ *
3812
+ * ```ts
3813
+ * type AppErrors =
3814
+ * | ValidMutationSizeError
3815
+ * | StringError
3816
+ * | MinLengthError
3817
+ * | MaxLengthError
3818
+ * | NullError
3819
+ * | IdError
3820
+ * | TrimmedError
3821
+ * | MnemonicError
3822
+ * | LiteralError
3823
+ * // Composite errors
3824
+ * | ObjectError<Record<string, AppErrors>>
3825
+ * | UnionError<AppErrors>;
3826
+ *
3827
+ * const formatTypeError: TypeErrorFormatter<AppErrors> = (error) => {
3828
+ * // In the real code, we would use the createTypeErrorFormatter helper
3829
+ * // that safely stringifies error value.
3830
+ * switch (error.type) {
3831
+ * case "Id":
3832
+ * return `Invalid Id on table: ${error.table}.`;
3833
+ * case "MaxLength":
3834
+ * return `Max length is ${error.max}.`;
3835
+ * case "MinLength":
3836
+ * return `Min length is ${error.min}.`;
3837
+ * case "Mnemonic":
3838
+ * return `Invalid mnemonic: ${String(error.value)}`;
3839
+ * case "Null":
3840
+ * return `Not null`;
3841
+ * case "String":
3842
+ * // We can reuse existing formatter.
3843
+ * return formatStringError(error);
3844
+ * case "Trimmed":
3845
+ * return "Value is not trimmed.";
3846
+ * case "ValidMutationSize":
3847
+ * return "A developer made an error, this should not happen.";
3848
+ * case "Literal":
3849
+ * return formatLiteralError(error);
3850
+ * // Composite Types
3851
+ * case "Union":
3852
+ * return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
3853
+ * case "Object": {
3854
+ * if (
3855
+ * error.reason.kind === "ExtraKeys" ||
3856
+ * error.reason.kind === "NotObject"
3857
+ * )
3858
+ * return "A developer made an error, this should not happen.";
3859
+ * const firstError = Object.values(error.reason.errors).find(
3860
+ * (e) => e !== undefined,
3861
+ * )!;
3862
+ * return formatTypeError(firstError);
3863
+ * }
3864
+ * }
3865
+ * };
3866
+ * ```
3867
+ *
3868
+ * @category Utilities
3869
+ */
3870
+ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3871
+ extraFormatter?: TypeErrorFormatter<ExtraErrors>,
3872
+ ): TypeErrorFormatter<TypeErrors<ExtraErrors>> => {
3873
+ const formatTypeError: TypeErrorFormatter<TypeErrors<ExtraErrors>> = (
3874
+ error,
3875
+ ) => {
3876
+ const extraMessage = extraFormatter?.(error as ExtraErrors);
3877
+
3878
+ if (extraMessage != null) return extraMessage;
3879
+
3880
+ error = error as TypeErrors;
3881
+
3882
+ switch (error.type) {
3883
+ case "String":
3884
+ return formatStringError(error);
3885
+ case "Number":
3886
+ return formatNumberError(error);
3887
+ case "BigInt":
3888
+ return formatBigIntError(error);
3889
+ case "Boolean":
3890
+ return formatBooleanError(error);
3891
+ case "Undefined":
3892
+ return formatUndefinedError(error);
3893
+ case "Null":
3894
+ return formatNullError(error);
3895
+ case "Function":
3896
+ return formatFunctionError(error);
3897
+ case "Uint8Array":
3898
+ return formatUint8ArrayError(error);
3899
+ case "InstanceOf":
3900
+ return formatInstanceOfError(error);
3901
+ case "EvoluType":
3902
+ return formatIsTypeError(error);
3903
+ case "CurrencyCode":
3904
+ return formatCurrencyCodeError(error);
3905
+ case "DateIsoString":
3906
+ return formatDateIsoStringError(error);
3907
+ case "Trimmed":
3908
+ return formatTrimmedError(error);
3909
+ case "MinLength":
3910
+ return formatMinLengthError(error);
3911
+ case "MaxLength":
3912
+ return formatMaxLengthError(error);
3913
+ case "Length":
3914
+ return formatLengthError(error);
3915
+ case "Mnemonic":
3916
+ return formatMnemonicError(error);
3917
+ case "Regex":
3918
+ return formatRegexError(error);
3919
+ case "Id":
3920
+ return formatIdError(error);
3921
+ case "Positive":
3922
+ return formatPositiveError(error);
3923
+ case "Negative":
3924
+ return formatNegativeError(error);
3925
+ case "NonPositive":
3926
+ return formatNonPositiveError(error);
3927
+ case "NonNegative":
3928
+ return formatNonNegativeError(error);
3929
+ case "Int":
3930
+ return formatIntError(error);
3931
+ case "GreaterThan":
3932
+ return formatGreaterThanError(error);
3933
+ case "LessThan":
3934
+ return formatLessThanError(error);
3935
+ case "GreaterThanOrEqualTo":
3936
+ return formatGreaterThanOrEqualToError(error);
3937
+ case "LessThanOrEqualTo":
3938
+ return formatLessThanOrEqualToError(error);
3939
+ case "NonNaN":
3940
+ return formatNonNaNError(error);
3941
+ case "Finite":
3942
+ return formatFiniteError(error);
3943
+ case "MultipleOf":
3944
+ return formatMultipleOfError(error);
3945
+ case "Between":
3946
+ return formatBetweenError(error);
3947
+ case "Literal":
3948
+ return formatLiteralError(error);
3949
+ case "Int64":
3950
+ return formatInt64Error(error);
3951
+ case "BigIntFromString":
3952
+ return formatBigIntFromStringError(error);
3953
+ case "Int64String":
3954
+ return formatInt64StringError(error);
3955
+ case "JsonValueFromString":
3956
+ return formatJsonValueFromStringError(error);
3957
+ case "Json":
3958
+ return formatJsonError(error);
3959
+ // Composite Types
3960
+ case "SimplePassword":
3961
+ return formatSimplePasswordError(formatTypeError)(error);
3962
+ case "Array":
3963
+ return formatArrayError(formatTypeError)(error);
3964
+ case "Record":
3965
+ return formatRecordError(formatTypeError)(error);
3966
+ case "Object":
3967
+ return formatObjectError(formatTypeError)(error);
3968
+ case "ObjectWithRecord":
3969
+ return formatObjectWithRecordError(formatTypeError)(error);
3970
+ case "Union":
3971
+ return formatUnionError(formatTypeError)(error);
3972
+ case "Tuple":
3973
+ return formatTupleError(formatTypeError)(error);
3974
+ }
3975
+ };
3976
+
3977
+ return formatTypeError;
3978
+ };