@evolu/common 6.0.1-preview.8 → 7.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 (184) hide show
  1. package/README.md +2 -2
  2. package/dist/src/Array.d.ts +256 -12
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +215 -9
  5. package/dist/src/Assert.d.ts +0 -13
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +0 -15
  8. package/dist/src/Brand.d.ts +75 -0
  9. package/dist/src/Brand.d.ts.map +1 -0
  10. package/dist/src/Brand.js +1 -0
  11. package/dist/src/Buffer.d.ts +1 -1
  12. package/dist/src/Buffer.d.ts.map +1 -1
  13. package/dist/src/Buffer.js +8 -7
  14. package/dist/src/Cache.d.ts +44 -0
  15. package/dist/src/Cache.d.ts.map +1 -0
  16. package/dist/src/Cache.js +52 -0
  17. package/dist/src/Callbacks.d.ts +45 -12
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Callbacks.js +14 -7
  20. package/dist/src/Console.d.ts +31 -6
  21. package/dist/src/Console.d.ts.map +1 -1
  22. package/dist/src/Console.js +72 -9
  23. package/dist/src/Crypto.d.ts +56 -42
  24. package/dist/src/Crypto.d.ts.map +1 -1
  25. package/dist/src/Crypto.js +40 -53
  26. package/dist/src/Evolu/Db.d.ts +162 -74
  27. package/dist/src/Evolu/Db.d.ts.map +1 -1
  28. package/dist/src/Evolu/Db.js +284 -702
  29. package/dist/src/Evolu/Diff.d.ts +3 -3
  30. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  31. package/dist/src/Evolu/Diff.js +7 -5
  32. package/dist/src/Evolu/Evolu.d.ts +214 -134
  33. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  34. package/dist/src/Evolu/Evolu.js +189 -180
  35. package/dist/src/Evolu/Internal.d.ts +0 -2
  36. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  37. package/dist/src/Evolu/Internal.js +0 -2
  38. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  39. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  40. package/dist/src/Evolu/LocalAuth.js +174 -0
  41. package/dist/src/Evolu/Owner.d.ts +273 -120
  42. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  43. package/dist/src/Evolu/Owner.js +130 -104
  44. package/dist/src/Evolu/Platform.d.ts +9 -7
  45. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  46. package/dist/src/Evolu/Protocol.d.ts +268 -240
  47. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  48. package/dist/src/Evolu/Protocol.js +600 -454
  49. package/dist/src/Evolu/Public.d.ts +6 -8
  50. package/dist/src/Evolu/Public.d.ts.map +1 -1
  51. package/dist/src/Evolu/Public.js +2 -3
  52. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
  53. package/dist/src/Evolu/PublicKysely.js +3 -4
  54. package/dist/src/Evolu/Query.d.ts +2 -1
  55. package/dist/src/Evolu/Query.d.ts.map +1 -1
  56. package/dist/src/Evolu/Query.js +1 -1
  57. package/dist/src/Evolu/Relay.d.ts +91 -8
  58. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  59. package/dist/src/Evolu/Relay.js +214 -88
  60. package/dist/src/Evolu/Schema.d.ts +125 -47
  61. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  62. package/dist/src/Evolu/Schema.js +175 -31
  63. package/dist/src/Evolu/Storage.d.ts +249 -27
  64. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  65. package/dist/src/Evolu/Storage.js +198 -92
  66. package/dist/src/Evolu/Sync.d.ts +68 -13
  67. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  68. package/dist/src/Evolu/Sync.js +469 -20
  69. package/dist/src/Evolu/Timestamp.d.ts +83 -30
  70. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  71. package/dist/src/Evolu/Timestamp.js +79 -34
  72. package/dist/src/Identicon.d.ts +35 -0
  73. package/dist/src/Identicon.d.ts.map +1 -0
  74. package/dist/src/Identicon.js +143 -0
  75. package/dist/src/Instances.d.ts +34 -0
  76. package/dist/src/Instances.d.ts.map +1 -0
  77. package/dist/src/Instances.js +44 -0
  78. package/dist/src/Number.d.ts +4 -3
  79. package/dist/src/Number.d.ts.map +1 -1
  80. package/dist/src/Number.js +5 -4
  81. package/dist/src/Object.d.ts +10 -4
  82. package/dist/src/Object.d.ts.map +1 -1
  83. package/dist/src/Object.js +9 -3
  84. package/dist/src/Platform.d.ts +20 -0
  85. package/dist/src/Platform.d.ts.map +1 -0
  86. package/dist/src/Platform.js +22 -0
  87. package/dist/src/Random.d.ts +3 -2
  88. package/dist/src/Random.d.ts.map +1 -1
  89. package/dist/src/Relation.d.ts +79 -0
  90. package/dist/src/Relation.d.ts.map +1 -0
  91. package/dist/src/Relation.js +127 -0
  92. package/dist/src/Resources.d.ts +118 -0
  93. package/dist/src/Resources.d.ts.map +1 -0
  94. package/dist/src/Resources.js +197 -0
  95. package/dist/src/Result.d.ts +184 -52
  96. package/dist/src/Result.d.ts.map +1 -1
  97. package/dist/src/Result.js +30 -241
  98. package/dist/src/Skiplist.js +2 -1
  99. package/dist/src/Sqlite.d.ts +89 -5
  100. package/dist/src/Sqlite.d.ts.map +1 -1
  101. package/dist/src/Sqlite.js +136 -9
  102. package/dist/src/Task.d.ts +586 -0
  103. package/dist/src/Task.d.ts.map +1 -0
  104. package/dist/src/Task.js +469 -0
  105. package/dist/src/Time.d.ts +66 -1
  106. package/dist/src/Time.d.ts.map +1 -1
  107. package/dist/src/Time.js +99 -5
  108. package/dist/src/Type.d.ts +676 -343
  109. package/dist/src/Type.d.ts.map +1 -1
  110. package/dist/src/Type.js +718 -467
  111. package/dist/src/Types.d.ts +1 -75
  112. package/dist/src/Types.d.ts.map +1 -1
  113. package/dist/src/WebSocket.d.ts +5 -2
  114. package/dist/src/WebSocket.d.ts.map +1 -1
  115. package/dist/src/WebSocket.js +12 -18
  116. package/dist/src/Worker.d.ts +39 -11
  117. package/dist/src/Worker.d.ts.map +1 -1
  118. package/dist/src/Worker.js +22 -4
  119. package/dist/src/index.d.ts +8 -3
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +8 -3
  122. package/package.json +15 -14
  123. package/src/Array.ts +301 -19
  124. package/src/Assert.ts +0 -21
  125. package/src/Brand.ts +75 -0
  126. package/src/Buffer.ts +7 -7
  127. package/src/Cache.ts +85 -0
  128. package/src/Callbacks.ts +62 -22
  129. package/src/Console.ts +91 -11
  130. package/src/Crypto.ts +94 -90
  131. package/src/Evolu/Db.ts +519 -1026
  132. package/src/Evolu/Diff.ts +7 -5
  133. package/src/Evolu/Evolu.ts +477 -361
  134. package/src/Evolu/Internal.ts +0 -2
  135. package/src/Evolu/LocalAuth.ts +463 -0
  136. package/src/Evolu/Owner.ts +355 -228
  137. package/src/Evolu/Platform.ts +9 -9
  138. package/src/Evolu/Protocol.ts +842 -742
  139. package/src/Evolu/Public.ts +7 -14
  140. package/src/Evolu/PublicKysely.ts +4 -5
  141. package/src/Evolu/Query.ts +3 -2
  142. package/src/Evolu/Relay.ts +406 -103
  143. package/src/Evolu/Schema.ts +323 -91
  144. package/src/Evolu/Storage.ts +559 -137
  145. package/src/Evolu/Sync.ts +819 -36
  146. package/src/Evolu/Timestamp.ts +90 -58
  147. package/src/Identicon.ts +197 -0
  148. package/src/Instances.ts +90 -0
  149. package/src/Number.ts +6 -10
  150. package/src/Object.ts +13 -5
  151. package/src/Platform.ts +26 -0
  152. package/src/Random.ts +3 -2
  153. package/src/Relation.ts +234 -0
  154. package/src/Resources.ts +367 -0
  155. package/src/Result.ts +191 -54
  156. package/src/Skiplist.ts +1 -1
  157. package/src/Sqlite.ts +152 -17
  158. package/src/Task.ts +901 -0
  159. package/src/Time.ts +180 -5
  160. package/src/Type.ts +1135 -730
  161. package/src/Types.ts +1 -77
  162. package/src/WebSocket.ts +27 -25
  163. package/src/Worker.ts +72 -23
  164. package/src/index.ts +8 -3
  165. package/dist/src/Evolu/Config.d.ts +0 -69
  166. package/dist/src/Evolu/Config.d.ts.map +0 -1
  167. package/dist/src/Evolu/Config.js +0 -9
  168. package/dist/src/Evolu/Kysely.d.ts +0 -6
  169. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  170. package/dist/src/Evolu/Kysely.js +0 -21
  171. package/dist/src/ManyToManyMap.d.ts +0 -26
  172. package/dist/src/ManyToManyMap.d.ts.map +0 -1
  173. package/dist/src/ManyToManyMap.js +0 -92
  174. package/dist/src/NanoId.d.ts +0 -27
  175. package/dist/src/NanoId.d.ts.map +0 -1
  176. package/dist/src/NanoId.js +0 -6
  177. package/dist/src/Promise.d.ts +0 -180
  178. package/dist/src/Promise.d.ts.map +0 -1
  179. package/dist/src/Promise.js +0 -176
  180. package/src/Evolu/Config.ts +0 -83
  181. package/src/Evolu/Kysely.ts +0 -38
  182. package/src/ManyToManyMap.ts +0 -140
  183. package/src/NanoId.ts +0 -39
  184. package/src/Promise.ts +0 -295
package/src/Type.ts CHANGED
@@ -1,84 +1,219 @@
1
1
  /**
2
- * 🧩 Validation, Parsing, and Transformation
2
+ * 🧩 Type-safe runtime types
3
3
  *
4
- * ## Intro
4
+ * Evolu {@link Type} is like a type guard that returns typed errors (via
5
+ * {@link Result}) instead of throwing. We either get a safely typed value or a
6
+ * composable typed error telling us exactly why validation failed.
5
7
  *
6
- * You probably know [Zod](https://zod.dev). Evolu has {@link Type}.
8
+ * Why another validation library?
7
9
  *
8
- * Evolu Type exists because no existing validation/parsing/transformation
9
- * library fully met our needs:
10
+ * - **Result-based error handling** no exceptions for normal control flow.
11
+ * - **Typed errors with decoupled formatters** – validation logic ≠ user
12
+ * messages.
13
+ * - **Consistent constraints via {@link Brand}** – every constraint becomes part
14
+ * of the type.
15
+ * - **Skippable validation** – parent validations can be skipped when already
16
+ * proved by types.
17
+ * - **Simple, top-down implementation** – readable source code from top to
18
+ * bottom.
19
+ * - **No user-land chaining DSL** – prepared for TC39 Hack pipes.
10
20
  *
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.
21
+ * A distinctive feature of Evolu Type compared to other validation libraries is
22
+ * that it returns typed errors rather than string messages. This allows
23
+ * TypeScript to enforce that all validation errors are handled by type
24
+ * checking, significantly improving the developer experience.
22
25
  *
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.
26
+ * Evolu Type supports [Standard Schema](https://standardschema.dev/) for
27
+ * interoperability with 40+ validation-compatible tools and frameworks.
27
28
  *
28
- * - Evolu `Type` is:
29
+ * ### Base Types Quick Start
29
30
  *
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.
31
+ * ```ts
32
+ * // Validate unknown values
33
+ * const value: unknown = "hello";
34
+ * const stringResult = String.fromUnknown(value);
35
+ * if (!stringResult.ok) {
36
+ * // console.error(formatStringError(stringResult.error));
37
+ * return stringResult; // inside a function returning Result<string, _>
38
+ * }
39
+ * // Safe branch: value is now string
40
+ * const upper = stringResult.value.toUpperCase();
41
+ *
42
+ * // Type guard style
43
+ * if (String.is(value)) {
44
+ * // narrowed to string
45
+ * }
46
+ *
47
+ * // Composing: arrays & objects
48
+ * const Numbers = array(Number); // ReadonlyArray<number>
49
+ * const Point = object({ x: Number, y: Number });
50
+ *
51
+ * Numbers.from([1, 2, 3]); // ok
52
+ * Point.from({ x: 1, y: 2 }); // ok
53
+ * Point.from({ x: 1, y: "2" }); // err -> nested Number error
54
+ * ```
55
+ *
56
+ * ### Branding Basics
57
+ *
58
+ * Branding adds semantic meaning & constraints while preserving the runtime
59
+ * shape:
60
+ *
61
+ * ```ts
62
+ * const CurrencyCode = brand("CurrencyCode", String, (value) =>
63
+ * /^[A-Z]{3}$/.test(value)
64
+ * ? ok(value)
65
+ * : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
66
+ * );
67
+ * type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
68
+ *
69
+ * interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
70
+ *
71
+ * const formatCurrencyCodeError =
72
+ * createTypeErrorFormatter<CurrencyCodeError>(
73
+ * (error) => `Invalid currency code: ${error.value}`,
74
+ * );
75
+ *
76
+ * const r = CurrencyCode.from("USD"); // ok("USD")
77
+ * const e = CurrencyCode.from("usd"); // err(...)
78
+ * ```
79
+ *
80
+ * See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
81
+ * `positive`, `between`, etc.
82
+ *
83
+ * ### Objects & Optional Fields
84
+ *
85
+ * ```ts
86
+ * const User = object({
87
+ * name: NonEmptyTrimmedString100,
88
+ * age: optional(PositiveInt),
89
+ * });
90
+ * type User = typeof User.Type;
91
+ *
92
+ * User.from({ name: "Alice" }); // ok
93
+ * User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
94
+ * ```
95
+ *
96
+ * ### Deriving JSON String Types
97
+ *
98
+ * ```ts
99
+ * const Person = object({
100
+ * name: NonEmptyString50,
101
+ * // Did you know that JSON.stringify converts NaN (a number) into null?
102
+ * // To prevent this, use FiniteNumber.
103
+ * age: FiniteNumber,
104
+ * });
105
+ * type Person = typeof Person.Type;
106
+ *
107
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
108
+ * Person,
109
+ * "PersonJson",
110
+ * );
111
+ * // string & Brand<"PersonJson">
112
+ * type PersonJson = typeof PersonJson.Type;
113
+ *
114
+ * const person = Person.orThrow({
115
+ * name: "Alice",
116
+ * age: 30,
117
+ * });
118
+ *
119
+ * const personJson = personToPersonJson(person);
120
+ * expect(personJsonToPerson(personJson)).toEqual(person);
121
+ * ```
122
+ *
123
+ * ### Error Formatting
124
+ *
125
+ * Evolu separates validation logic from human-readable messages. There are two
126
+ * layers:
127
+ *
128
+ * 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
129
+ * used earlier in the quick start example.
130
+ * 2. A unified formatter via `createFormatTypeError` – composes all built-in and
131
+ * custom errors (including nested composite types) and lets us override
132
+ * selected messages.
133
+ *
134
+ * #### 1. Per-Type Formatter (recap)
34
135
  *
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:
136
+ * ```ts
137
+ * const r = String.fromUnknown(42);
138
+ * if (!r.ok) console.error(formatStringError(r.error));
139
+ * ```
39
140
  *
40
- * `Unknown` -> `String` -> `TrimmedString` -> `NonEmptyTrimmedString100`
141
+ * #### 2. Unified Formatter with Overrides
41
142
  *
42
- * For `NonEmptyTrimmedString100`, the parent Type is `TrimmedString`. For
43
- * `TrimmedString`, the parent Type is `String`.
143
+ * ```ts
144
+ * // Override only what we care about; fall back to built-ins for the rest.
145
+ * const formatTypeError = createFormatTypeError((error) => {
146
+ * if (error.type === "MinLength") return `Min length is ${error.min}`;
147
+ * });
44
148
  *
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.
149
+ * const User = object({ name: NonEmptyTrimmedString100 });
150
+ * const resultUser = User.from({ name: "" });
151
+ * if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
48
152
  *
49
- * Speaking of `fromParent` and `toParent`, those functions exist to bypass
50
- * parent Types when we can rely on TypeScript types.
153
+ * const badPoint = object({ x: Number, y: Number }).from({
154
+ * x: 1,
155
+ * y: "foo",
156
+ * });
157
+ * if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
158
+ * ```
51
159
  *
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")`.
160
+ * The unified formatter walks nested structures (object / array / record /
161
+ * tuple / union) and applies overrides only where specified, greatly reducing
162
+ * boilerplate when formatting complex validation errors.
57
163
  *
58
164
  * ### Tip
59
165
  *
60
166
  * If necessary, write `globalThis.String` instead of `String` to avoid naming
61
- * clashes with Base Types.
167
+ * clashes with native types.
168
+ *
169
+ * ### Design Decision: No Bidirectional Transformations
62
170
  *
63
- * ### Design Decision:
171
+ * Evolu Type intentionally does not support bidirectional transformations. It
172
+ * previously did, but supporting that while keeping typed error fidelity added
173
+ * complexity that hurt readability & reliability. Most persistence pipelines
174
+ * (e.g. SQLite) already require explicit mapping of query results, so implicit
175
+ * reverse transforms would not buy much. We may revisit this if we can design a
176
+ * minimal, 100% safe API that preserves simplicity.
64
177
  *
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.
178
+ * ### Prepared for TC39 Hack Pipes
179
+ *
180
+ * Take a look how `SimplePassword` is defined:
181
+ *
182
+ * ```ts
183
+ * export const SimplePassword = brand(
184
+ * "SimplePassword",
185
+ * minLength(8)(maxLength(64)(TrimmedString)),
186
+ * );
187
+ * ```
188
+ *
189
+ * Nested functions are often OK (if not, make a helper) and read well, but with
190
+ * TC39 Hack pipes it would be clearer:
191
+ *
192
+ * ```ts
193
+ * // TrimmedString
194
+ * // |> minLength(8)(%)
195
+ * // |> maxLength(64)(%)
196
+ * // |> brand("SimplePassword", %)
197
+ * ```
198
+ *
199
+ * Note `minLength` and `maxLength` are curried because they are factories.
69
200
  *
70
201
  * @module
71
202
  */
72
203
 
204
+ import { utf8ToBytes } from "@noble/ciphers/utils.js";
205
+ import { sha256 } from "@noble/hashes/sha2.js";
73
206
  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";
207
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
208
+ import { pack } from "msgpackr";
209
+ import type { Brand } from "./Brand.js";
210
+ import { type RandomBytesDep } from "./Crypto.js";
78
211
  import { isPlainObject } from "./Object.js";
79
- import { Err, err, Ok, ok, Result, trySync } from "./Result.js";
212
+ import { hasNodeBuffer } from "./Platform.js";
213
+ import { err, getOrNull, getOrThrow, ok, Result, trySync } from "./Result.js";
80
214
  import { safelyStringifyUnknownValue } from "./String.js";
81
- import type { Brand, Literal, Simplify, WidenLiteral } from "./Types.js";
215
+ import type { TimeDep } from "./Time.js";
216
+ import type { Literal, Simplify, WidenLiteral } from "./Types.js";
82
217
  import { IntentionalNever } from "./Types.js";
83
218
 
84
219
  export interface Type<
@@ -93,7 +228,7 @@ export interface Type<
93
228
  Parent = T,
94
229
  /** The parent's error. */
95
230
  ParentError extends TypeError = Error,
96
- > {
231
+ > extends StandardSchemaV1<Input, T> {
97
232
  readonly name: Name;
98
233
 
99
234
  /**
@@ -106,41 +241,98 @@ export interface Type<
106
241
  readonly from: (value: Input) => Result<T, ParentError | Error>;
107
242
 
108
243
  /**
109
- * Creates `T` from an unknown value.
244
+ * Creates `T` from an `Input` value, throwing an error if validation fails.
110
245
  *
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`.
246
+ * Throws an Error with the Type validation error in its `cause` property,
247
+ * making it debuggable while avoiding the need for custom error messages.
248
+ *
249
+ * This is a convenience method that combines `from` with `getOrThrow`.
250
+ *
251
+ * **When to use:**
117
252
  *
118
- * This is useful to transform `T` back to its `Input` representation.
253
+ * - Configuration values that are guaranteed to be valid (e.g., hardcoded
254
+ * constants)
255
+ * - Application startup where failure should crash the program
256
+ * - As an alternative to assertions when the Type error in the thrown Error's
257
+ * `cause` provides sufficient debugging information
258
+ * - Test code with known valid inputs (when error message clarity is not
259
+ * critical; for better test error messages, use Vitest `schemaMatching` +
260
+ * `assert` with `.is()`)
119
261
  *
120
- * For `refine`, it only removes the brand. For `transform`, it changes value.
262
+ * ### Example
263
+ *
264
+ * ```ts
265
+ * // ✅ Good: Known valid constant
266
+ * const maxRetries = PositiveInt.orThrow(3);
267
+ *
268
+ * // ✅ Good: App configuration that should crash on invalid values
269
+ * const appName = SimpleName.orThrow("MyApp");
270
+ *
271
+ * // ✅ Good: Instead of assert when Type error is clear enough
272
+ * // Context makes it obvious: count increments from non-negative value
273
+ * const currentCount = counts.get(id) ?? 0;
274
+ * const newCount = PositiveInt.orThrow(currentCount + 1);
275
+ *
276
+ * // ✅ Good: Test setup with known valid values
277
+ * const testUser = User.orThrow({ name: "Alice", age: 30 });
278
+ *
279
+ * // ❌ Avoid: User input (use `from` instead)
280
+ * const userAge = PositiveInt.orThrow(userInput); // Could crash!
281
+ *
282
+ * // ✅ Better: Handle user input gracefully
283
+ * const ageResult = PositiveInt.from(userInput);
284
+ * if (!ageResult.ok) {
285
+ * // Handle validation error
286
+ * }
287
+ * ```
121
288
  */
122
- readonly to: (value: T) => Input;
289
+ readonly orThrow: (value: Input) => T;
123
290
 
124
291
  /**
125
- * Creates `T` from `Parent` type.
292
+ * Creates `T` from an `Input` value, returning `null` if validation fails.
126
293
  *
127
- * This function skips parent Types validations/transformations when we have
128
- * already partially validated/transformed value.
294
+ * This is a convenience method that combines `from` with `getOrNull`.
129
295
  *
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`.
296
+ * **When to use:**
297
+ *
298
+ * - When you need to convert a validation result to a nullable value
299
+ * - When the error is not important and you just want the value or nothing
132
300
  *
133
301
  * ### Example
134
302
  *
135
303
  * ```ts
136
- * // string & Brand<"Trimmed">
137
- * const value = TrimString.fromParent("a ").value; // as efficient as foo.trim()
304
+ * // Good: Optional user input
305
+ * const age = PositiveInt.orNull(userInput);
306
+ * if (age != null) {
307
+ * console.log("Valid age:", age);
308
+ * }
309
+ *
310
+ * // ✅ Good: Default fallback
311
+ * const maxRetries = PositiveInt.orNull(config.retries) ?? 3;
312
+ *
313
+ * // ❌ Avoid: When you need to know why validation failed (use `from` instead)
314
+ * const result = PositiveInt.from(userInput);
315
+ * if (!result.ok) {
316
+ * console.error(formatPositiveError(result.error));
317
+ * }
138
318
  * ```
139
319
  */
140
- readonly fromParent: (value: Parent) => Result<T, Error>;
320
+ readonly orNull: (value: Input) => T | null;
141
321
 
142
- /** The opposite of `fromParent`. */
143
- readonly toParent: (value: T) => Parent;
322
+ /**
323
+ * Creates `T` from an unknown value.
324
+ *
325
+ * This is useful when a value is unknown.
326
+ */
327
+ readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
328
+
329
+ /**
330
+ * Creates `T` from `Parent` type.
331
+ *
332
+ * This function skips parent Types validations when we have already partially
333
+ * validated value.
334
+ */
335
+ readonly fromParent: (value: Parent) => Result<T, Error>;
144
336
 
145
337
  /**
146
338
  * A **type guard** that checks whether an unknown value satisfies the
@@ -221,8 +413,6 @@ export interface Type<
221
413
  readonly ParentError: ParentError;
222
414
 
223
415
  /**
224
- * Error | ParentError
225
- *
226
416
  * ### Example
227
417
  *
228
418
  * ```ts
@@ -261,26 +451,61 @@ export interface TypeErrorWithReason<
261
451
 
262
452
  export type AnyType = Type<any, any, any, any, any, any>;
263
453
 
454
+ /**
455
+ * Extracts the name from a {@link Type}.
456
+ *
457
+ * @category Utilities
458
+ */
264
459
  export type InferName<A extends AnyType> =
265
460
  A extends Type<infer Name, any, any, any, any, any> ? Name : never;
266
461
 
462
+ /**
463
+ * Extracts the type from a {@link Type}.
464
+ *
465
+ * @category Utilities
466
+ */
267
467
  export type InferType<A extends AnyType> =
268
468
  A extends Type<any, infer T, any, any, any, any> ? T : never;
269
469
 
470
+ /**
471
+ * Extracts the input type from a {@link Type}.
472
+ *
473
+ * @category Utilities
474
+ */
270
475
  export type InferInput<A extends AnyType> =
271
476
  A extends Type<any, any, infer Input, any, any, any> ? Input : never;
272
477
 
478
+ /**
479
+ * Extracts the specific error type from a {@link Type}.
480
+ *
481
+ * @category Utilities
482
+ */
273
483
  export type InferError<A extends AnyType> =
274
484
  A extends Type<any, any, any, infer Error, any, any> ? Error : never;
275
485
 
486
+ /**
487
+ * Extracts the parent type from a {@link Type}.
488
+ *
489
+ * @category Utilities
490
+ */
276
491
  export type InferParent<A extends AnyType> =
277
492
  A extends Type<any, any, any, any, infer Parent, any> ? Parent : never;
278
493
 
494
+ /**
495
+ * Extracts the parent error type from a {@link Type}.
496
+ *
497
+ * @category Utilities
498
+ */
279
499
  export type InferParentError<A extends AnyType> =
280
500
  A extends Type<any, any, any, any, any, infer ParentError>
281
501
  ? ParentError
282
502
  : never;
283
503
 
504
+ /**
505
+ * Extracts all error types from a {@link Type}.
506
+ *
507
+ * @category Utilities
508
+ */
284
509
  export type InferErrors<T extends AnyType> =
285
510
  T extends Type<any, any, any, infer Error, any, infer ParentError>
286
511
  ? Error | ParentError
@@ -310,6 +535,8 @@ const createType = <
310
535
  | "name"
311
536
  | "is"
312
537
  | "from"
538
+ | "orThrow"
539
+ | "orNull"
313
540
  | typeof EvoluTypeSymbol
314
541
  | "Type"
315
542
  | "Input"
@@ -317,12 +544,15 @@ const createType = <
317
544
  | "Parent"
318
545
  | "ParentError"
319
546
  | "Errors"
547
+ | "~standard"
320
548
  >,
321
549
  ): Type<Name, T, Input, Error, Parent, ParentError> => ({
322
550
  ...definition,
323
551
  name,
324
552
  is: (value: unknown): value is T => definition.fromUnknown(value).ok,
325
553
  from: definition.fromUnknown,
554
+ orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
555
+ orNull: (value) => getOrNull(definition.fromUnknown(value)),
326
556
  [EvoluTypeSymbol]: true,
327
557
  Type: undefined as unknown as T,
328
558
  Input: undefined as unknown as Input,
@@ -330,6 +560,27 @@ const createType = <
330
560
  Parent: undefined as unknown as Parent,
331
561
  ParentError: undefined as unknown as ParentError,
332
562
  Errors: undefined as unknown as Error | ParentError,
563
+ "~standard": {
564
+ version: 1,
565
+ vendor: "evolu",
566
+ validate: (value: unknown): StandardSchemaV1.Result<T> => {
567
+ const result = definition.fromUnknown(value);
568
+ if (result.ok) {
569
+ return { value: result.value };
570
+ }
571
+ cachedStandardSchemaFormatTypeError ??= createFormatTypeError();
572
+ return {
573
+ issues: typeErrorToStandardSchemaIssues(
574
+ result.error as TypeErrors<Error>,
575
+ cachedStandardSchemaFormatTypeError,
576
+ ),
577
+ };
578
+ },
579
+ types: {
580
+ input: undefined as unknown as Input,
581
+ output: undefined as unknown as T,
582
+ },
583
+ },
333
584
  });
334
585
 
335
586
  /**
@@ -365,12 +616,6 @@ export type TypeErrorFormatter<Error extends TypeError> = (
365
616
  * Base {@link Type}.
366
617
  *
367
618
  * 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
619
  *
375
620
  * ### Example
376
621
  *
@@ -396,9 +641,7 @@ export const base = <Name extends TypeName, T, Error extends TypeError>(
396
641
  ): Type<Name, T, T, Error> =>
397
642
  createType(name, {
398
643
  fromUnknown,
399
- to: identity,
400
644
  fromParent: ok<T>, // `fromParent` relies on types, so it can't fail for the Base Type
401
- toParent: identity,
402
645
  });
403
646
 
404
647
  /**
@@ -564,7 +807,7 @@ export interface InstanceOfType<T extends abstract new (...args: any) => any>
564
807
  }
565
808
 
566
809
  export const formatInstanceOfError = createTypeErrorFormatter<InstanceOfError>(
567
- (error) => `Value ${error.value} is not an instance of ${error.ctor}`,
810
+ (error) => `The value ${error.value} is not an instance of ${error.ctor}.`,
568
811
  );
569
812
 
570
813
  /**
@@ -607,7 +850,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
607
850
  * The `brand` Type Factory takes the name of a new {@link Brand}, a parent Type
608
851
  * to be branded, and the optional `refine` function for additional constraint.
609
852
  *
610
- * If the `refine` function is omited, TODO:
853
+ * The `refine` function can be omitted if we only want to add a brand.
611
854
  *
612
855
  * ### Examples
613
856
  *
@@ -686,7 +929,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
686
929
  * confirmPassword: SimplePassword,
687
930
  * });
688
931
  *
689
- * const ValidForm = brand("Valid", Form, (value) => {
932
+ * const ValidForm = brand("ValidForm", Form, (value) => {
690
933
  * if (value.password !== value.confirmPassword)
691
934
  * return err<ValidFormError>({
692
935
  * type: "ValidForm",
@@ -780,9 +1023,7 @@ export function brand<
780
1023
  return {
781
1024
  ...createType("Brand", {
782
1025
  fromUnknown,
783
- to: identity,
784
1026
  fromParent: (refine ?? ok) as IntentionalNever,
785
- toParent: identity,
786
1027
  }),
787
1028
  brand: name,
788
1029
  parentType: parent,
@@ -830,7 +1071,7 @@ export interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
830
1071
 
831
1072
  export const formatCurrencyCodeError =
832
1073
  createTypeErrorFormatter<CurrencyCodeError>(
833
- (error) => `Invalid currency code: ${error.value}`,
1074
+ (error) => `Invalid currency code: ${error.value}.`,
834
1075
  );
835
1076
 
836
1077
  /**
@@ -848,30 +1089,41 @@ export const formatCurrencyCodeError =
848
1089
  * ### Example
849
1090
  *
850
1091
  * ```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
1092
+ * const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
1093
+ * const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
853
1094
  * ```
854
1095
  *
855
1096
  * @category String
856
1097
  */
857
- export const DateIsoString = brand("DateIso", String, (value) => {
1098
+ export const DateIso = brand("DateIso", String, (value) => {
858
1099
  if (value.length !== 24) {
859
- return err<DateIsoStringError>({ type: "DateIsoString", value });
1100
+ return err<DateIsoError>({ type: "DateIso", value });
1101
+ }
1102
+ const parsed = globalThis.Date.parse(value);
1103
+ if (isNaN(parsed)) {
1104
+ return err<DateIsoError>({ type: "DateIso", value });
860
1105
  }
861
- if (isNaN(globalThis.Date.parse(value))) {
862
- return err<DateIsoStringError>({ type: "DateIsoString", value });
1106
+ // Round-trip test: ensure the string is actually a proper ISO format
1107
+ const roundTrip = new globalThis.Date(parsed).toISOString();
1108
+ if (roundTrip !== value) {
1109
+ return err<DateIsoError>({ type: "DateIso", value });
863
1110
  }
864
1111
  return ok(value);
865
1112
  });
866
1113
 
867
- export type DateIsoString = typeof DateIsoString.Type;
1114
+ export type DateIso = typeof DateIso.Type;
868
1115
 
869
- export interface DateIsoStringError extends TypeError<"DateIsoString"> {}
1116
+ export interface DateIsoError extends TypeError<"DateIso"> {}
870
1117
 
871
- export const formatDateIsoStringError =
872
- createTypeErrorFormatter<DateIsoStringError>(
873
- (error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
874
- );
1118
+ export const formatDateIsoError = createTypeErrorFormatter<DateIsoError>(
1119
+ (error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
1120
+ );
1121
+
1122
+ export const dateToDateIso = (value: Date): Result<DateIso, DateIsoError> =>
1123
+ DateIso.fromParent(value.toISOString());
1124
+
1125
+ export const dateIsoToDate = (value: DateIso): Date =>
1126
+ new globalThis.Date(value);
875
1127
 
876
1128
  /**
877
1129
  * Helper type for Type Factory that creates a branded Type.
@@ -914,18 +1166,12 @@ export type BrandFactory<
914
1166
  /**
915
1167
  * Trimmed string.
916
1168
  *
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.
1169
+ * This Type Factory validates whether a string has no leading or trailing
1170
+ * whitespaces.
920
1171
  *
921
- * ### Examples
1172
+ * ### Example
922
1173
  *
923
1174
  * ```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
1175
  * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
930
1176
  * // string & Brand<"MinLength1"> & Brand<"Trimmed">
931
1177
  * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
@@ -945,67 +1191,9 @@ export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
945
1191
  export interface TrimmedError extends TypeError<"Trimmed"> {}
946
1192
 
947
1193
  export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
948
- (error) => `A value ${error.value} is not trimmed`,
1194
+ (error) => `The value ${error.value} must be trimmed.`,
949
1195
  );
950
1196
 
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
1197
  /**
1010
1198
  * Trimmed string
1011
1199
  *
@@ -1017,6 +1205,9 @@ export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
1017
1205
  export const TrimmedString = trimmed(String);
1018
1206
  export type TrimmedString = typeof TrimmedString.Type;
1019
1207
 
1208
+ export const trim = (value: string): TrimmedString =>
1209
+ value.trim() as TrimmedString;
1210
+
1020
1211
  /**
1021
1212
  * Minimum length.
1022
1213
  *
@@ -1045,7 +1236,7 @@ export interface MinLengthError<Min extends number = number>
1045
1236
 
1046
1237
  export const formatMinLengthError = createTypeErrorFormatter<MinLengthError>(
1047
1238
  (error) =>
1048
- `Value ${error.value} does not meet the minimum length of ${error.min}.`,
1239
+ `The value ${error.value} does not meet the minimum length of ${error.min}.`,
1049
1240
  );
1050
1241
 
1051
1242
  /**
@@ -1075,7 +1266,8 @@ export interface MaxLengthError<Max extends number = number>
1075
1266
  }
1076
1267
 
1077
1268
  export const formatMaxLengthError = createTypeErrorFormatter<MaxLengthError>(
1078
- (error) => `Value ${error.value} exceeds the maximum length of ${error.max}.`,
1269
+ (error) =>
1270
+ `The value ${error.value} exceeds the maximum length of ${error.max}.`,
1079
1271
  );
1080
1272
 
1081
1273
  /**
@@ -1108,7 +1300,7 @@ export interface LengthError<Exact extends number = number>
1108
1300
 
1109
1301
  export const formatLengthError = createTypeErrorFormatter<LengthError>(
1110
1302
  (error) =>
1111
- `Value ${error.value} does not have the required length of ${error.exact}.`,
1303
+ `The value ${error.value} does not have the required length of ${error.exact}.`,
1112
1304
  );
1113
1305
 
1114
1306
  /** @category String */
@@ -1170,7 +1362,7 @@ export type Mnemonic = typeof Mnemonic.Type;
1170
1362
  export interface MnemonicError extends TypeError<"Mnemonic"> {}
1171
1363
 
1172
1364
  export const formatMnemonicError = createTypeErrorFormatter<MnemonicError>(
1173
- (error) => `Invalid BIP39 mnemonic: ${error.value}`,
1365
+ (error) => `Invalid BIP39 mnemonic: ${error.value}.`,
1174
1366
  );
1175
1367
 
1176
1368
  /**
@@ -1209,13 +1401,13 @@ export interface RegexError<Name extends TypeName = TypeName>
1209
1401
 
1210
1402
  export const formatRegexError = createTypeErrorFormatter<RegexError>(
1211
1403
  (error) =>
1212
- `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`,
1404
+ `The value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}.`,
1213
1405
  );
1214
1406
 
1215
1407
  /**
1216
- * URL-safe Base64 string.
1408
+ * URL-safe string.
1217
1409
  *
1218
- * A `Base64Url` string uses a limited alphabet that is URL-safe:
1410
+ * A `UrlSafeString` uses a limited alphabet that is safe for URLs:
1219
1411
  *
1220
1412
  * - Uppercase letters (`A-Z`)
1221
1413
  * - Lowercase letters (`a-z`)
@@ -1223,37 +1415,117 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
1223
1415
  * - Dash (`-`)
1224
1416
  * - Underscore (`_`)
1225
1417
  *
1418
+ * This is the same character set used by Base64Url encoding, but this type does
1419
+ * not validate that the string is actually Base64Url-encoded data.
1420
+ *
1226
1421
  * ### Example
1227
1422
  *
1228
1423
  * ```ts
1229
- * const result = Base64Url.from("abc123_-");
1424
+ * const result = UrlSafeString.from("abc123_-");
1230
1425
  * if (result.ok) {
1231
- * console.log("Valid Base64Url string:", result.value);
1426
+ * console.log("Valid URL-safe string:", result.value);
1232
1427
  * } else {
1233
- * console.error("Invalid Base64Url string:", result.error);
1428
+ * console.error("Invalid URL-safe string:", result.error);
1234
1429
  * }
1235
1430
  * ```
1236
1431
  *
1237
1432
  * @category String
1238
1433
  */
1239
- export const Base64Url = regex(
1434
+ export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
1435
+ export type UrlSafeString = typeof UrlSafeString.Type;
1436
+ export type UrlSafeStringError = typeof UrlSafeString.Error;
1437
+
1438
+ /**
1439
+ * Base64Url without padding.
1440
+ *
1441
+ * Encode with {@link uint8ArrayToBase64Url}, decode with
1442
+ * {@link base64UrlToUint8Array}.
1443
+ *
1444
+ * @category String
1445
+ */
1446
+ export const Base64Url = brand(
1240
1447
  "Base64Url",
1241
- /^[A-Za-z0-9_-]+$/, // URL-safe Base64 alphabet
1242
- )(String);
1448
+ String,
1449
+ (value: string): Result<string, Base64UrlError> => {
1450
+ // Round-trip validation ensures consistency across different base64url
1451
+ // implementations (Node.js Buffer, native browser API, manual fallback).
1452
+ // Only strings that decode and encode identically are accepted.
1453
+ let roundTrip;
1454
+ try {
1455
+ roundTrip = uint8ArrayToBase64Url(
1456
+ base64UrlToUint8Array(value as Base64Url),
1457
+ );
1458
+ } catch {
1459
+ //
1460
+ }
1461
+ return roundTrip === value
1462
+ ? ok(value)
1463
+ : err<Base64UrlError>({ type: "Base64Url", value });
1464
+ },
1465
+ );
1243
1466
  export type Base64Url = typeof Base64Url.Type;
1244
- export type Base64UrlError = typeof Base64Url.Error;
1467
+ export interface Base64UrlError extends TypeError<"Base64Url"> {}
1468
+
1469
+ export const formatBase64UrlError = createTypeErrorFormatter<Base64UrlError>(
1470
+ (error) => `The value ${error.value} is not a valid Base64Url string.`,
1471
+ );
1472
+
1473
+ const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
1474
+
1475
+ /** Encodes a Uint8Array to a {@link Base64Url} string. */
1476
+ export const uint8ArrayToBase64Url: (bytes: Uint8Array) => Base64Url =
1477
+ hasNodeBuffer
1478
+ ? (bytes: Uint8Array) =>
1479
+ globalThis.Buffer.from(bytes).toString("base64url") as Base64Url
1480
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1481
+ typeof (globalThis.Uint8Array.prototype as any)?.toBase64 !== "undefined"
1482
+ ? (bytes: Uint8Array) =>
1483
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
1484
+ (bytes as any).toBase64(base64UrlOptions) as Base64Url
1485
+ : (bytes: Uint8Array) => {
1486
+ const binaryString = Array.from(bytes, (byte) =>
1487
+ globalThis.String.fromCodePoint(byte),
1488
+ ).join("");
1489
+ const base64 = globalThis.btoa(binaryString);
1490
+ return base64
1491
+ .replace(/\+/g, "-")
1492
+ .replace(/\//g, "_")
1493
+ .replace(/=/g, "") as Base64Url;
1494
+ };
1495
+
1496
+ /** Decodes a {@link Base64Url} string to a Uint8Array. */
1497
+ export const base64UrlToUint8Array: (str: Base64Url) => Uint8Array =
1498
+ hasNodeBuffer
1499
+ ? (str: Base64Url) => {
1500
+ const nodeBuffer = globalThis.Buffer.from(str, "base64url");
1501
+ return new globalThis.Uint8Array(nodeBuffer);
1502
+ }
1503
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1504
+ typeof (globalThis.Uint8Array as any)?.fromBase64 !== "undefined"
1505
+ ? (str: Base64Url) =>
1506
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
1507
+ (globalThis.Uint8Array as any).fromBase64(
1508
+ str,
1509
+ base64UrlOptions,
1510
+ ) as Uint8Array
1511
+ : (str: Base64Url) => {
1512
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
1513
+ while (base64.length % 4 !== 0) {
1514
+ base64 += "=";
1515
+ }
1516
+ const binaryString = globalThis.atob(base64);
1517
+ return globalThis.Uint8Array.from(binaryString, (c) =>
1518
+ c.charCodeAt(0),
1519
+ );
1520
+ };
1245
1521
 
1246
1522
  /**
1247
- * Simple alphanumeric string for naming.
1523
+ * Simple alphanumeric string for naming in file systems, URLs, and identifiers.
1248
1524
  *
1249
- * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
1525
+ * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
1526
+ * `_`). See `UrlSafeString` for details.
1250
1527
  *
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.
1528
+ * The string must be between 1 and 64 characters.
1257
1529
  *
1258
1530
  * ### Example
1259
1531
  *
@@ -1268,22 +1540,36 @@ export type Base64UrlError = typeof Base64Url.Error;
1268
1540
  *
1269
1541
  * @category String
1270
1542
  */
1271
- export const SimpleName = regex("SimpleName", /^[a-z0-9-]{1,42}$/i)(String);
1543
+ export const SimpleName = brand("SimpleName", UrlSafeString, (value) =>
1544
+ value.length >= 1 && value.length <= 64
1545
+ ? ok(value)
1546
+ : err<SimpleNameError>({ type: "SimpleName", value }),
1547
+ );
1272
1548
  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;
1549
+ export interface SimpleNameError extends TypeError<"SimpleName"> {}
1283
1550
 
1284
1551
  /**
1285
1552
  * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
1286
1553
  *
1554
+ * Take a look how `SimplePassword` is defined:
1555
+ *
1556
+ * ```ts
1557
+ * export const SimplePassword = brand(
1558
+ * "SimplePassword",
1559
+ * minLength(8)(maxLength(64)(TrimmedString)),
1560
+ * );
1561
+ * ```
1562
+ *
1563
+ * Nested functions are often OK (if not, make a helper), but with TC39 Hack
1564
+ * pipes it would be clearer:
1565
+ *
1566
+ * ```ts
1567
+ * // TrimmedString
1568
+ * // |> minLength(8)(%)
1569
+ * // |> maxLength(64)(%)
1570
+ * // |> brand("SimplePassword", %)
1571
+ * ```
1572
+ *
1287
1573
  * @category String
1288
1574
  */
1289
1575
  export const SimplePassword = brand(
@@ -1304,33 +1590,146 @@ export const formatSimplePasswordError = (
1304
1590
  );
1305
1591
 
1306
1592
  /**
1307
- * `Id` {@link Type}.
1593
+ * Evolu Id: 16 bytes encoded as a 22‑character Base64Url string.
1308
1594
  *
1309
- * Represents a unique identifier with exactly 21 characters, using NanoID's
1310
- * standard format (`A-Za-z0-9_-`).
1595
+ * There are three ways to create an Evolu Id:
1596
+ *
1597
+ * - {@link createId} – default cryptographically secure random bytes
1598
+ * (privacy‑preserving)
1599
+ * - {@link createIdFromString} – deterministic: first 16 bytes of SHA‑256 of a
1600
+ * string
1601
+ * - {@link createIdAsUuidv7} – optional: embeds timestamp bits (UUID v7 layout)
1602
+ *
1603
+ * Privacy: the default random Id does not leak creation time and is safe to
1604
+ * share or log. The UUID v7 variant leaks creation time anywhere the Id is
1605
+ * copied (logs, URLs, exports); only use it when you explicitly want insertion
1606
+ * locality for very large write‑heavy tables and accept timestamp exposure.
1607
+ *
1608
+ * ### Future
1609
+ *
1610
+ * A possible hybrid masked‑time approach (`timestamp ^ H(cluster_id, timestamp
1611
+ *
1612
+ * > > N)`) could provide locality without exposing raw creation time. See
1613
+ * > > https://brooker.co.za/blog/2025/10/22/uuidv7.html
1311
1614
  *
1312
1615
  * @category String
1313
1616
  */
1314
- export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
1617
+ export const Id = brand("Id", String, (value) =>
1618
+ value.length === 22 && Base64Url.fromParent(value).ok
1619
+ ? ok(value)
1620
+ : err<IdError>({ type: "Id", value }),
1621
+ );
1315
1622
  export type Id = typeof Id.Type;
1316
1623
 
1317
- export const idTypeValueLength = 21;
1624
+ export interface IdError extends TypeError<"Id"> {}
1625
+
1626
+ export const formatIdError = createTypeErrorFormatter<IdError>(
1627
+ (error) => `The value ${error.value} is not a valid Id.`,
1628
+ );
1318
1629
 
1319
1630
  /**
1320
- * Creates an {@link Id}.
1631
+ * Creates a random {@link Id}. This is the recommended default.
1632
+ *
1633
+ * Use {@link createIdFromString} for deterministic mapping of external IDs or
1634
+ * {@link createIdAsUuidv7} when you accept timestamp leakage for index
1635
+ * locality.
1321
1636
  *
1322
1637
  * ### Example
1323
1638
  *
1324
1639
  * ```ts
1325
- * // string & Brand<"Id">
1326
1640
  * const id = createId(deps);
1641
+ * const todoId = createId<"Todo">(deps);
1642
+ * ```
1643
+ */
1644
+ export const createId = <B extends string = never>(
1645
+ deps: RandomBytesDep,
1646
+ ): [B] extends [never] ? Id : Id & Brand<B> => {
1647
+ const id = uint8ArrayToBase64Url(deps.randomBytes.create(16));
1648
+ return id as unknown as [B] extends [never] ? Id : Id & Brand<B>;
1649
+ };
1650
+
1651
+ /**
1652
+ * Creates an {@link Id} from a string using SHA-256.
1653
+ *
1654
+ * When integrating with external systems that use different ID formats, use
1655
+ * this function to convert external IDs into valid Evolu IDs.
1656
+ *
1657
+ * In Evolu's CRDT, the ID serves as the unique identifier for conflict
1658
+ * resolution across distributed clients. When multiple clients create records
1659
+ * with the same external identifier, they must resolve to the same Evolu ID to
1660
+ * ensure data consistency.
1661
+ *
1662
+ * ### Example
1663
+ *
1664
+ * ```ts
1665
+ * // Both clients will generate the same ID
1666
+ * const id1 = createIdFromString("user-api-123");
1667
+ * const id2 = createIdFromString("user-api-123");
1668
+ * console.log(id1 === id2); // true
1669
+ *
1670
+ * upsert("todo", {
1671
+ * id: createIdFromString("external-todo-456"),
1672
+ * title: "Synced from external system",
1673
+ * });
1327
1674
  * ```
1675
+ *
1676
+ * **Important**: This transformation uses the first 16 bytes of SHA-256 hash of
1677
+ * the string bytes, therefore it's not possible to recover the original
1678
+ * external string from the generated {@link Id}. If you need to preserve the
1679
+ * original external ID, store it in a separate column.
1680
+ *
1681
+ * @category String
1328
1682
  */
1329
- export const createId = (deps: NanoIdLibDep): Id =>
1330
- deps.nanoIdLib.nanoid() as Id;
1683
+ export const createIdFromString = <B extends string = never>(
1684
+ value: string,
1685
+ ): [B] extends [never] ? Id : Id & Brand<B> => {
1686
+ const hash = sha256(utf8ToBytes(value));
1687
+ // Take first 16 bytes of hash and convert to Id
1688
+ const id = idBytesToId(hash.slice(0, 16) as IdBytes);
1689
+
1690
+ return id as [B] extends [never] ? Id : Id & Brand<B>;
1691
+ };
1692
+
1693
+ /**
1694
+ * Creates an {@link Id} embedding timestamp bits (UUID v7 layout) before
1695
+ * Base64Url encoding.
1696
+ *
1697
+ * Tradeoff: better insertion locality / index performance for huge datasets vs
1698
+ * leaking creation time everywhere the Id appears. Evolu uses {@link createId}
1699
+ * by default to avoid activity leakage; choose this only if you explicitly
1700
+ * accept timestamp exposure.
1701
+ *
1702
+ * ### Example
1703
+ *
1704
+ * ```ts
1705
+ * const id = createIdAsUuidv7({ randomBytes, time });
1706
+ * const todoId = createIdAsUuidv7<"Todo">({ randomBytes, time });
1707
+ * ```
1708
+ */
1709
+ export const createIdAsUuidv7 = <B extends string = never>(
1710
+ deps: RandomBytesDep & TimeDep,
1711
+ ): [B] extends [never] ? Id : Id & Brand<B> => {
1712
+ const id = deps.randomBytes.create(16);
1713
+
1714
+ const timestamp = globalThis.BigInt(deps.time.now());
1715
+
1716
+ id[0] = globalThis.Number((timestamp >> 40n) & 0xffn);
1717
+ id[1] = globalThis.Number((timestamp >> 32n) & 0xffn);
1718
+ id[2] = globalThis.Number((timestamp >> 24n) & 0xffn);
1719
+ id[3] = globalThis.Number((timestamp >> 16n) & 0xffn);
1720
+ id[4] = globalThis.Number((timestamp >> 8n) & 0xffn);
1721
+ id[5] = globalThis.Number(timestamp & 0xffn);
1722
+
1723
+ id[6] = (id[6] & 0x0f) | 0x70;
1724
+ id[8] = (id[8] & 0x3f) | 0x80;
1725
+
1726
+ return id as unknown as [B] extends [never] ? Id : Id & Brand<B>;
1727
+ };
1331
1728
 
1332
1729
  /**
1333
- * Type Factory to create branded {@link Id} Type for a specific table.
1730
+ * Creates a branded {@link Id} Type for a table's primary key.
1731
+ *
1732
+ * The table name becomes an additional brand for type safety.
1334
1733
  *
1335
1734
  * ### Example
1336
1735
  *
@@ -1342,55 +1741,64 @@ export const createId = (deps: NanoIdLibDep): Id =>
1342
1741
  *
1343
1742
  * @category String
1344
1743
  */
1345
- export const id = <Table extends TypeName>(table: Table): IdType<Table> => {
1744
+ export const id = <Table extends TypeName>(table: Table): TableId<Table> => {
1745
+ const fromUnknown = (value: unknown) => {
1746
+ const parentResult = String.fromUnknown(value);
1747
+ if (!parentResult.ok) return parentResult;
1748
+ return fromParent(parentResult.value);
1749
+ };
1750
+
1346
1751
  const fromParent = (value: string) => {
1347
1752
  const idResult = Id.fromParent(value);
1348
1753
  if (!idResult.ok) {
1349
- return err<IdError<Table>>({ type: "Id", value, table });
1754
+ return err<TableIdError<Table>>({ type: "TableId", value, table });
1350
1755
  }
1351
1756
  return ok(idResult.value as Id & Brand<Table>);
1352
1757
  };
1353
1758
 
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
1759
  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
- }),
1760
+ ...createType("Id", { fromUnknown, fromParent }),
1367
1761
  table,
1368
1762
  };
1369
1763
  };
1370
1764
 
1371
- export interface IdType<Table extends TypeName>
1765
+ export interface TableId<Table extends TypeName>
1372
1766
  extends Type<
1373
1767
  "Id",
1374
1768
  string & Brand<"Id"> & Brand<Table>,
1375
1769
  string,
1376
- IdError<Table>,
1770
+ TableIdError<Table>,
1377
1771
  string,
1378
1772
  StringError
1379
1773
  > {
1380
1774
  table: Table;
1381
1775
  }
1382
1776
 
1383
- export interface IdError<Table extends TypeName = TypeName>
1384
- extends TypeError<"Id"> {
1777
+ export interface TableIdError<Table extends TypeName = TypeName>
1778
+ extends TypeError<"TableId"> {
1385
1779
  readonly table: Table;
1386
1780
  }
1387
1781
 
1388
- export const formatIdError = createTypeErrorFormatter<IdError>(
1389
- (error) => `Invalid ${error.type} table Id: ${error.value}`,
1782
+ export const formatTableIdError = createTypeErrorFormatter<TableIdError>(
1783
+ (error) => `Invalid Id for table ${error.table}: ${error.value}.`,
1390
1784
  );
1391
1785
 
1786
+ /** Binary representation of an {@link Id}. */
1787
+ export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
1788
+ export type IdBytes = typeof IdBytes.Type;
1789
+
1790
+ export const idBytesTypeValueLength = 16 as NonNegativeInt;
1791
+
1792
+ export const idToIdBytes = (id: Id): IdBytes =>
1793
+ // Id is Base64Url (validated by Id.from), cast is safe
1794
+ base64UrlToUint8Array(id as unknown as Base64Url) as IdBytes;
1795
+
1796
+ export const idBytesToId = (idBytes: IdBytes): Id =>
1797
+ // Base64Url encoding of 16 bytes always produces valid Id (22 chars)
1798
+ uint8ArrayToBase64Url(idBytes) as unknown as Id;
1799
+
1392
1800
  /**
1393
- * Positive number.
1801
+ * Positive number (> 0).
1394
1802
  *
1395
1803
  * ### Example
1396
1804
  *
@@ -1413,11 +1821,11 @@ export const positive: BrandFactory<"Positive", number, PositiveError> = (
1413
1821
  export interface PositiveError extends TypeError<"Positive"> {}
1414
1822
 
1415
1823
  export const formatPositiveError = createTypeErrorFormatter<PositiveError>(
1416
- (error) => `The value ${error.value} is not positive.`,
1824
+ (error) => `The value ${error.value} must be positive (> 0).`,
1417
1825
  );
1418
1826
 
1419
1827
  /**
1420
- * Negative number.
1828
+ * Negative number (< 0).
1421
1829
  *
1422
1830
  * ### Example
1423
1831
  *
@@ -1437,11 +1845,11 @@ export const negative: BrandFactory<"Negative", number, NegativeError> = (
1437
1845
  export interface NegativeError extends TypeError<"Negative"> {}
1438
1846
 
1439
1847
  export const formatNegativeError = createTypeErrorFormatter<NegativeError>(
1440
- (error) => `The value ${error.value} is not negative.`,
1848
+ (error) => `The value ${error.value} must be negative (< 0).`,
1441
1849
  );
1442
1850
 
1443
1851
  /**
1444
- * Non-positive number.
1852
+ * Non-positive number (≤ 0).
1445
1853
  *
1446
1854
  * ### Example
1447
1855
  *
@@ -1466,11 +1874,11 @@ export interface NonPositiveError extends TypeError<"NonPositive"> {}
1466
1874
 
1467
1875
  export const formatNonPositiveError =
1468
1876
  createTypeErrorFormatter<NonPositiveError>(
1469
- (error) => `The value ${error.value} is not non-positive.`,
1877
+ (error) => `The value ${error.value} must be non-positive (≤ 0).`,
1470
1878
  );
1471
1879
 
1472
1880
  /**
1473
- * Non-negative number.
1881
+ * Non-negative number (≥ 0).
1474
1882
  *
1475
1883
  * ### Example
1476
1884
  *
@@ -1495,22 +1903,38 @@ export interface NonNegativeError extends TypeError<"NonNegative"> {}
1495
1903
 
1496
1904
  export const formatNonNegativeError =
1497
1905
  createTypeErrorFormatter<NonNegativeError>(
1498
- (error) => `The value ${error.value} is not non-negative.`,
1906
+ (error) => `The value ${error.value} must be non-negative (≥ 0).`,
1499
1907
  );
1500
1908
 
1501
- /** @category Number */
1909
+ /**
1910
+ * Non-negative number (≥ 0).
1911
+ *
1912
+ * @category Number
1913
+ */
1502
1914
  export const NonNegativeNumber = nonNegative(Number);
1503
1915
  export type NonNegativeNumber = typeof NonNegativeNumber.Type;
1504
1916
 
1505
- /** @category Number */
1917
+ /**
1918
+ * Positive number (> 0).
1919
+ *
1920
+ * @category Number
1921
+ */
1506
1922
  export const PositiveNumber = positive(NonNegativeNumber);
1507
1923
  export type PositiveNumber = typeof PositiveNumber.Type;
1508
1924
 
1509
- /** @category Number */
1925
+ /**
1926
+ * Non-positive number (≤ 0).
1927
+ *
1928
+ * @category Number
1929
+ */
1510
1930
  export const NonPositiveNumber = nonPositive(Number);
1511
1931
  export type NonPositiveNumber = typeof NonPositiveNumber.Type;
1512
1932
 
1513
- /** @category Number */
1933
+ /**
1934
+ * Negative number (< 0).
1935
+ *
1936
+ * @category Number
1937
+ */
1514
1938
  export const NegativeNumber = negative(NonPositiveNumber);
1515
1939
  export type NegativeNumber = typeof NegativeNumber.Type;
1516
1940
 
@@ -1535,7 +1959,7 @@ export const int: BrandFactory<"Int", number, IntError> = (parent) =>
1535
1959
  export interface IntError extends TypeError<"Int"> {}
1536
1960
 
1537
1961
  export const formatIntError = createTypeErrorFormatter<IntError>(
1538
- (error) => `The value ${error.value} is not an integer.`,
1962
+ (error) => `The value ${error.value} must be an integer.`,
1539
1963
  );
1540
1964
 
1541
1965
  /**
@@ -1546,19 +1970,40 @@ export const formatIntError = createTypeErrorFormatter<IntError>(
1546
1970
  export const Int = int(Number);
1547
1971
  export type Int = typeof Int.Type;
1548
1972
 
1549
- /** @category Number */
1973
+ /**
1974
+ * Non-negative integer (≥ 0).
1975
+ *
1976
+ * @category Number
1977
+ */
1550
1978
  export const NonNegativeInt = nonNegative(Int);
1551
1979
  export type NonNegativeInt = typeof NonNegativeInt.Type;
1552
1980
 
1553
- /** @category Number */
1981
+ /**
1982
+ * Positive integer (> 0).
1983
+ *
1984
+ * @category Number
1985
+ */
1554
1986
  export const PositiveInt = positive(NonNegativeInt);
1555
1987
  export type PositiveInt = typeof PositiveInt.Type;
1556
1988
 
1557
- /** @category Number */
1989
+ /** Maximum safe positive integer value for practically infinite operations. */
1990
+ export const maxPositiveInt = PositiveInt.orThrow(
1991
+ globalThis.Number.MAX_SAFE_INTEGER,
1992
+ );
1993
+
1994
+ /**
1995
+ * Non-positive integer (≤ 0).
1996
+ *
1997
+ * @category Number
1998
+ */
1558
1999
  export const NonPositiveInt = nonPositive(Int);
1559
2000
  export type NonPositiveInt = typeof NonPositiveInt.Type;
1560
2001
 
1561
- /** @category Number */
2002
+ /**
2003
+ * Negative integer (< 0).
2004
+ *
2005
+ * @category Number
2006
+ */
1562
2007
  export const NegativeInt = negative(NonPositiveInt);
1563
2008
  export type NegativeInt = typeof NegativeInt.Type;
1564
2009
 
@@ -1676,7 +2121,7 @@ export const nonNaN: BrandFactory<"NonNaN", number, NonNaNError> = (parent) =>
1676
2121
  export interface NonNaNError extends TypeError<"NonNaN"> {}
1677
2122
 
1678
2123
  export const formatNonNaNError = createTypeErrorFormatter<NonNaNError>(
1679
- (error) => `The value ${error.value} is NaN (not a number).`,
2124
+ () => `The value must not be NaN.`,
1680
2125
  );
1681
2126
 
1682
2127
  /** @category Number */
@@ -1698,7 +2143,7 @@ export const finite: BrandFactory<"Finite", number, FiniteError> = (parent) =>
1698
2143
  export interface FiniteError extends TypeError<"Finite"> {}
1699
2144
 
1700
2145
  export const formatFiniteError = createTypeErrorFormatter<FiniteError>(
1701
- (error) => `The value ${error.value} is not finite.`,
2146
+ (error) => `The value ${error.value} must be finite.`,
1702
2147
  );
1703
2148
 
1704
2149
  /**
@@ -1778,195 +2223,54 @@ export const formatBetweenError = createTypeErrorFormatter<BetweenError>(
1778
2223
  `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
1779
2224
  );
1780
2225
 
1781
- /** @category Number */
1782
- export const Between1And10 = between(1, 10)(Number);
1783
- export type Between1And10 = typeof Between1And10.Type;
1784
-
1785
2226
  /**
1786
2227
  * Literal {@link Type}.
1787
2228
  *
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
- *
2229
+ * https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
2230
+ *
1943
2231
  * ### Example
1944
2232
  *
1945
2233
  * ```ts
1946
- * NumberFromString.from("42"); // ok(42)
1947
- * NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
2234
+ * const LiteralHello = literal("Hello");
2235
+ * const result = LiteralHello.from("Hello"); // ok("Hello")
2236
+ * const errorResult = LiteralHello.from("World"); // err
1948
2237
  * ```
1949
2238
  *
1950
- * @category Number
2239
+ * TODO: Add JsonValue
2240
+ *
2241
+ * @category Base Factories
1951
2242
  */
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
- );
2243
+ export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
2244
+ const fromUnknown = (value: unknown): Result<T, LiteralError<T>> =>
2245
+ value === expected
2246
+ ? ok(expected)
2247
+ : err<LiteralError<T>>({ type: "Literal", value, expected });
1963
2248
 
1964
- export interface NumberFromStringError extends TypeError<"NumberFromString"> {}
2249
+ return {
2250
+ ...createType("Literal", {
2251
+ fromUnknown,
2252
+ fromParent: ok,
2253
+ }),
2254
+ expected,
2255
+ } as LiteralType<T>;
2256
+ };
1965
2257
 
1966
- export const formatNumberFromStringError =
1967
- createTypeErrorFormatter<NumberFromStringError>((error) => {
1968
- return `The value ${error.value} could not be converted to a finite number.`;
1969
- });
2258
+ export interface LiteralType<T extends Literal>
2259
+ extends Type<"Literal", T, WidenLiteral<T>, LiteralError<T>> {
2260
+ expected: T;
2261
+ }
2262
+
2263
+ export interface LiteralError<T extends Literal = Literal>
2264
+ extends TypeError<"Literal"> {
2265
+ readonly expected: T;
2266
+ }
2267
+
2268
+ export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
2269
+ (error) =>
2270
+ `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(
2271
+ error.expected,
2272
+ )}.`,
2273
+ );
1970
2274
 
1971
2275
  /**
1972
2276
  * Array of a specific {@link Type}.
@@ -2020,9 +2324,6 @@ export const array = <ElementType extends AnyType>(
2020
2324
  return ok(result);
2021
2325
  };
2022
2326
 
2023
- const to = (value: ReadonlyArray<InferType<ElementType>>) =>
2024
- value.map(element.to) as ReadonlyArray<InferInput<ElementType>>;
2025
-
2026
2327
  const fromParent = (
2027
2328
  value: ReadonlyArray<InferParent<ElementType>>,
2028
2329
  ): Result<
@@ -2048,18 +2349,10 @@ export const array = <ElementType extends AnyType>(
2048
2349
  return ok(result);
2049
2350
  };
2050
2351
 
2051
- const toParent = (values: ReadonlyArray<InferType<ElementType>>) =>
2052
- values.map(element.toParent) as ReadonlyArray<InferParent<ElementType>>;
2053
-
2054
2352
  return {
2055
- ...createType("Array", {
2056
- fromUnknown,
2057
- to,
2058
- fromParent,
2059
- toParent,
2060
- }),
2353
+ ...createType("Array", { fromUnknown, fromParent }),
2061
2354
  element,
2062
- } as ArrayType<ElementType>;
2355
+ };
2063
2356
  };
2064
2357
 
2065
2358
  /** ArrayType extends Type with an additional `element` property for reflection. */
@@ -2192,16 +2485,6 @@ export const record = <
2192
2485
  return ok(result);
2193
2486
  };
2194
2487
 
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
2488
  const fromParent = (
2206
2489
  value: Readonly<Record<KeyParent, InferParent<Value>>>,
2207
2490
  ): Result<
@@ -2246,22 +2529,10 @@ export const record = <
2246
2529
  return ok(result);
2247
2530
  };
2248
2531
 
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
2532
  return {
2260
2533
  ...createType("Record", {
2261
2534
  fromUnknown,
2262
- to,
2263
2535
  fromParent,
2264
- toParent,
2265
2536
  }),
2266
2537
  key: keyType,
2267
2538
  value: valueType,
@@ -2561,25 +2832,6 @@ export function object(
2561
2832
  return ok(result);
2562
2833
  };
2563
2834
 
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
2835
  const fromParent = (value: any): Result<any, any> => {
2584
2836
  const errors: Record<string, any> = {};
2585
2837
  const result: Record<string, any> = {};
@@ -2631,31 +2883,10 @@ export function object(
2631
2883
  return ok(result);
2632
2884
  };
2633
2885
 
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
2886
  return {
2654
2887
  ...createType(record ? "ObjectWithRecord" : "Object", {
2655
2888
  fromUnknown,
2656
- to,
2657
2889
  fromParent,
2658
- toParent,
2659
2890
  }),
2660
2891
  props,
2661
2892
  ...(record ? { record } : {}),
@@ -2897,7 +3128,6 @@ export function union(...args: ReadonlyArray<any>): any {
2897
3128
  * cases.
2898
3129
  */
2899
3130
 
2900
- /* eslint-disable */
2901
3131
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
2902
3132
 
2903
3133
  const fromUnknown = (value: unknown) => {
@@ -2915,23 +3145,13 @@ export function union(...args: ReadonlyArray<any>): any {
2915
3145
  });
2916
3146
  };
2917
3147
 
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
3148
  return {
2926
3149
  ...createType("Union", {
2927
3150
  fromUnknown,
2928
- to,
2929
3151
  fromParent: fromUnknown,
2930
- toParent: to,
2931
3152
  }),
2932
3153
  members,
2933
3154
  };
2934
- /* eslint-enable */
2935
3155
  }
2936
3156
 
2937
3157
  /**
@@ -3025,20 +3245,10 @@ export const recursive = <ParentType extends AnyType>(
3025
3245
  type ??= create();
3026
3246
  return type.fromUnknown(value);
3027
3247
  },
3028
- to: (value: T) => {
3029
- type ??= create();
3030
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3031
- return type.to(value);
3032
- },
3033
3248
  fromParent: (value: Parent) => {
3034
3249
  type ??= create();
3035
3250
  return type.fromParent(value);
3036
3251
  },
3037
- toParent: (value: T) => {
3038
- type ??= create();
3039
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3040
- return type.toParent(value);
3041
- },
3042
3252
  is: (value: unknown): value is T => {
3043
3253
  type ??= create();
3044
3254
  return type.is(value);
@@ -3176,9 +3386,6 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
3176
3386
  return ok(result);
3177
3387
  };
3178
3388
 
3179
- const to = (value: { [K in keyof Elements]: InferType<Elements[K]> }) =>
3180
- value.map((val, index) => elements[index].to(val) as IntentionalNever);
3181
-
3182
3389
  const fromParent = (value: {
3183
3390
  [K in keyof Elements]: InferParent<Elements[K]>;
3184
3391
  }) => {
@@ -3210,21 +3417,10 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
3210
3417
  return ok(result as { [K in keyof Elements]: InferType<Elements[K]> });
3211
3418
  };
3212
3419
 
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
3420
  return {
3223
3421
  ...createType("Tuple", {
3224
3422
  fromUnknown,
3225
- to,
3226
3423
  fromParent,
3227
- toParent,
3228
3424
  }),
3229
3425
  elements,
3230
3426
  } as IntentionalNever;
@@ -3300,37 +3496,21 @@ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
3300
3496
  `The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
3301
3497
  );
3302
3498
 
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
3499
  /**
3322
3500
  * Stringified {@link Int64}.
3323
3501
  *
3324
- * @category Number
3502
+ * @category String
3325
3503
  */
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
- });
3504
+ export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) =>
3505
+ trySync(
3506
+ () => {
3507
+ const maybeInt = globalThis.BigInt(value);
3508
+ Int64.orThrow(maybeInt);
3509
+ return value;
3510
+ },
3511
+ (): Int64StringError => ({ type: "Int64String", value }),
3512
+ ),
3513
+ );
3334
3514
 
3335
3515
  export type Int64String = typeof Int64String.Type;
3336
3516
 
@@ -3435,42 +3615,14 @@ export const JsonArray = array(JsonValue);
3435
3615
  */
3436
3616
  export const JsonObject = record(String, JsonValue);
3437
3617
 
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}`,
3618
+ export const parseJson = (value: string): Result<JsonValue, JsonError> =>
3619
+ trySync(
3620
+ () => JSON.parse(value) as JsonValue,
3621
+ (error): JsonError => ({
3622
+ type: `Json`,
3623
+ value,
3624
+ message: globalThis.String(error),
3625
+ }),
3474
3626
  );
3475
3627
 
3476
3628
  /**
@@ -3479,20 +3631,15 @@ export const formatJsonValueFromStringError =
3479
3631
  * ### Example
3480
3632
  *
3481
3633
  * ```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" })
3634
+ * const result = Json.from('{"key":"value"}'); // ok
3635
+ * const error = Json.from("invalid json"); // err
3484
3636
  * ```
3485
3637
  *
3486
3638
  * @category String
3487
3639
  */
3488
3640
  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
- });
3641
+ const result = parseJson(value);
3642
+ if (!result.ok) return result;
3496
3643
  return ok(value);
3497
3644
  });
3498
3645
 
@@ -3506,6 +3653,80 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
3506
3653
  (error) => `Invalid JSON: ${error.value}. Error: ${error.message}`,
3507
3654
  );
3508
3655
 
3656
+ export const jsonValueToJson = (value: JsonValue): Json =>
3657
+ JSON.stringify(value) as Json;
3658
+
3659
+ export const jsonToJsonValue = (value: Json): JsonValue =>
3660
+ JSON.parse(value) as JsonValue;
3661
+
3662
+ /**
3663
+ * Creates a branded JSON string {@link Type} and type-safe conversion functions
3664
+ * for a given Type.
3665
+ *
3666
+ * This factory creates:
3667
+ *
3668
+ * 1. A branded string Type that validates JSON parsing and structural conformity
3669
+ * 2. A serialization function (Type → branded JSON string)
3670
+ * 3. A parsing function (branded JSON string → Type, skipping validation)
3671
+ *
3672
+ * Optimized for Evolu's SQLite workflow where we store typed JSON strings and
3673
+ * need type-safe conversions without double parsing.
3674
+ *
3675
+ * ### Example
3676
+ *
3677
+ * ```ts
3678
+ * const Person = object({
3679
+ * name: NonEmptyString100,
3680
+ * age: FiniteNumber,
3681
+ * });
3682
+ * type Person = typeof Person.Type;
3683
+ *
3684
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
3685
+ * Person,
3686
+ * "PersonJson",
3687
+ * );
3688
+ * // string & Brand<"PersonJson">
3689
+ * type PersonJson = typeof PersonJson.Type;
3690
+ *
3691
+ * // Usage:
3692
+ * const person: Person = { name: "Alice", age: 30 };
3693
+ * const jsonString = personToPersonJson(person); // PersonJson
3694
+ * const backToPerson = personJsonToPerson(jsonString); // Person
3695
+ * ```
3696
+ */
3697
+ export const json = <T extends AnyType, Name extends TypeName>(
3698
+ type: T,
3699
+ name: Name,
3700
+ ): [
3701
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>,
3702
+ (
3703
+ value: InferType<T>,
3704
+ ) => InferType<
3705
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
3706
+ >,
3707
+ (
3708
+ value: InferType<
3709
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
3710
+ >,
3711
+ ) => InferType<T>,
3712
+ ] => {
3713
+ const BrandedJsonType = brand(name, String, (value) => {
3714
+ const parseResult = parseJson(value);
3715
+ if (!parseResult.ok) return parseResult;
3716
+
3717
+ const validationResult = type.fromUnknown(parseResult.value);
3718
+ if (!validationResult.ok) return validationResult;
3719
+
3720
+ return ok(value);
3721
+ }) as BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>;
3722
+
3723
+ return [
3724
+ BrandedJsonType,
3725
+ jsonValueToJson as IntentionalNever,
3726
+ jsonToJsonValue as IntentionalNever,
3727
+ ];
3728
+ };
3729
+
3509
3730
  /**
3510
3731
  * Optional {@link Type}.
3511
3732
  *
@@ -3530,9 +3751,7 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
3530
3751
  export const optional = <T extends AnyType>(type: T): OptionalType<T> => ({
3531
3752
  ...createType("Optional", {
3532
3753
  fromUnknown: type.fromUnknown,
3533
- to: type.to,
3534
3754
  fromParent: type.fromParent,
3535
- toParent: type.toParent,
3536
3755
  }),
3537
3756
  parent: type,
3538
3757
  });
@@ -3556,7 +3775,7 @@ export const isOptionalType = (x: unknown): x is OptionalType<any> =>
3556
3775
  /**
3557
3776
  * Creates a partial object type where all properties are optional.
3558
3777
  *
3559
- * This is useful when you want to validate an object in which none of the keys
3778
+ * This is useful when we want to validate an object in which none of the keys
3560
3779
  * are required, but if they are present they must conform to their
3561
3780
  * corresponding Types.
3562
3781
  *
@@ -3661,68 +3880,39 @@ export function omit<T extends ObjectType<any>, Keys extends keyof T["props"]>(
3661
3880
  return object(newProps);
3662
3881
  }
3663
3882
 
3883
+ export const maxMutationSize = 655360;
3884
+
3664
3885
  /**
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
- * ```
3886
+ * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
3887
+ * the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
3888
+ * via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
3889
+ * optimizations.
3692
3890
  */
3693
- export const json = <T extends AnyType, Name extends TypeName>(
3891
+ export const validMutationSize = <T extends AnyType>(
3694
3892
  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>;
3893
+ ): BrandType<T, "ValidMutationSize", ValidMutationSizeError, InferErrors<T>> =>
3894
+ brand("ValidMutationSize", type, (value) =>
3895
+ pack(value).byteLength <= maxMutationSize
3896
+ ? ok(value)
3897
+ : err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
3898
+ );
3714
3899
 
3715
- return ok(value as InferType<typeof BrandedJsonString>);
3716
- });
3900
+ export interface ValidMutationSizeError
3901
+ extends TypeError<"ValidMutationSize"> {}
3717
3902
 
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>,
3903
+ export const formatValidMutationSizeError =
3904
+ createTypeErrorFormatter<ValidMutationSizeError>(
3905
+ (error) =>
3906
+ `The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
3724
3907
  );
3725
- };
3908
+
3909
+ export type ValidMutationSize<Props extends Record<string, AnyType>> =
3910
+ BrandType<
3911
+ ObjectType<Props>,
3912
+ "ValidMutationSize",
3913
+ ValidMutationSizeError,
3914
+ InferErrors<ObjectType<Props>>
3915
+ >;
3726
3916
 
3727
3917
  /**
3728
3918
  * Union of all `TypeError`s defined in the `Type.ts` file, including base type
@@ -3749,16 +3939,16 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3749
3939
  | InstanceOfError
3750
3940
  | EvoluTypeError
3751
3941
  | CurrencyCodeError
3752
- | DateIsoStringError
3942
+ | DateIsoError
3753
3943
  | TrimmedError
3754
3944
  | MinLengthError
3755
3945
  | MaxLengthError
3756
3946
  | LengthError
3757
3947
  | MnemonicError
3758
3948
  | RegexError
3759
- | NanoIdError
3760
3949
  | SimplePasswordError
3761
3950
  | IdError
3951
+ | TableIdError
3762
3952
  | PositiveError
3763
3953
  | NegativeError
3764
3954
  | NonPositiveError
@@ -3774,10 +3964,9 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3774
3964
  | BetweenError
3775
3965
  | LiteralError
3776
3966
  | Int64Error
3777
- | BigIntFromStringError
3778
3967
  | Int64StringError
3779
- | JsonValueFromStringError
3780
3968
  | JsonError
3969
+ | ValidMutationSizeError
3781
3970
  | ExtraErrors
3782
3971
  // Composite errors
3783
3972
  | ArrayError<TypeErrors<ExtraErrors>>
@@ -3792,70 +3981,74 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3792
3981
  | TupleError<TypeErrors<ExtraErrors>>;
3793
3982
 
3794
3983
  /**
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.
3984
+ * Formats Evolu Type errors into user-friendly messages.
3798
3985
  *
3799
- * If you prefer not to reuse any built-in error formatters, you can write your
3800
- * own `formatTypeError` function from scratch.
3986
+ * Evolu Type typed errors ensure every error type must have a formatter.
3987
+ * TypeScript enforces this at compile-time, preventing unhandled validation
3988
+ * errors from reaching users.
3801
3989
  *
3802
- * ### Examples
3990
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
3991
+ * and custom errors, and lets us override default formatting for specific
3992
+ * errors.
3993
+ *
3994
+ * ### Example
3803
3995
  *
3804
3996
  * ```ts
3805
- * const formatError = createFormatTypeError();
3806
- * console.log(formatError({ type: "String", value: 42 }));
3807
- * // "A value 42 is not a string."
3997
+ * const formatTypeError = createFormatTypeError<
3998
+ * MinLengthError | MaxLengthError
3999
+ * >((error): string => {
4000
+ * switch (error.type) {
4001
+ * case "MinLength":
4002
+ * return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
4003
+ * case "MaxLength":
4004
+ * return `Text is too long (maximum ${error.max} characters)`;
4005
+ * }
4006
+ * });
3808
4007
  * ```
3809
4008
  *
3810
- * A custom `formatTypeError` function:
4009
+ * Alternatively, write a custom formatter from scratch without using
4010
+ * `createFormatTypeError`. This gives us full control over error formatting:
3811
4011
  *
3812
4012
  * ```ts
3813
- * type AppErrors =
3814
- * | ValidMutationSizeError
4013
+ * const Person = object({
4014
+ * name: NonEmptyTrimmedString100,
4015
+ * age: optional(PositiveInt),
4016
+ * });
4017
+ *
4018
+ * // Define only the errors actually used by Person Type
4019
+ * type PersonErrors =
3815
4020
  * | StringError
3816
- * | MinLengthError
3817
4021
  * | MaxLengthError
3818
- * | NullError
3819
- * | IdError
4022
+ * | MinLengthError
3820
4023
  * | 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.
4024
+ * | PositiveError
4025
+ * | NonNegativeError
4026
+ * | IntError
4027
+ * | NumberError
4028
+ * | ObjectError<Record<string, PersonErrors>>;
4029
+ *
4030
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
3830
4031
  * 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
4032
  * case "String":
3842
- * // We can reuse existing formatter.
3843
4033
  * return formatStringError(error);
4034
+ * case "Number":
4035
+ * return "Must be a number";
4036
+ * case "MinLength":
4037
+ * return `Must be at least ${error.min} characters`;
4038
+ * case "MaxLength":
4039
+ * return `Cannot exceed ${error.max} characters`;
3844
4040
  * 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(", ")}`;
4041
+ * return "Cannot have leading or trailing spaces";
4042
+ * case "Positive":
4043
+ * return "Must be a positive number";
4044
+ * case "NonNegative":
4045
+ * return "Must be zero or positive";
4046
+ * case "Int":
4047
+ * return "Must be an integer";
3853
4048
  * 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.";
4049
+ * if (error.reason.kind === "NotObject") return "Must be an object";
4050
+ * if (error.reason.kind === "ExtraKeys")
4051
+ * return "Contains unexpected fields";
3859
4052
  * const firstError = Object.values(error.reason.errors).find(
3860
4053
  * (e) => e !== undefined,
3861
4054
  * )!;
@@ -3902,8 +4095,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3902
4095
  return formatIsTypeError(error);
3903
4096
  case "CurrencyCode":
3904
4097
  return formatCurrencyCodeError(error);
3905
- case "DateIsoString":
3906
- return formatDateIsoStringError(error);
4098
+ case "DateIso":
4099
+ return formatDateIsoError(error);
3907
4100
  case "Trimmed":
3908
4101
  return formatTrimmedError(error);
3909
4102
  case "MinLength":
@@ -3918,6 +4111,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3918
4111
  return formatRegexError(error);
3919
4112
  case "Id":
3920
4113
  return formatIdError(error);
4114
+ case "TableId":
4115
+ return formatTableIdError(error);
3921
4116
  case "Positive":
3922
4117
  return formatPositiveError(error);
3923
4118
  case "Negative":
@@ -3948,14 +4143,12 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3948
4143
  return formatLiteralError(error);
3949
4144
  case "Int64":
3950
4145
  return formatInt64Error(error);
3951
- case "BigIntFromString":
3952
- return formatBigIntFromStringError(error);
3953
4146
  case "Int64String":
3954
4147
  return formatInt64StringError(error);
3955
- case "JsonValueFromString":
3956
- return formatJsonValueFromStringError(error);
3957
4148
  case "Json":
3958
4149
  return formatJsonError(error);
4150
+ case "ValidMutationSize":
4151
+ return formatValidMutationSizeError(error);
3959
4152
  // Composite Types
3960
4153
  case "SimplePassword":
3961
4154
  return formatSimplePasswordError(formatTypeError)(error);
@@ -3971,8 +4164,220 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3971
4164
  return formatUnionError(formatTypeError)(error);
3972
4165
  case "Tuple":
3973
4166
  return formatTupleError(formatTypeError)(error);
4167
+ default: {
4168
+ // Fallback for unknown error types
4169
+ const unknownError = error as TypeError;
4170
+ return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
4171
+ }
3974
4172
  }
3975
4173
  };
3976
4174
 
3977
4175
  return formatTypeError;
3978
4176
  };
4177
+
4178
+ /**
4179
+ * Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
4180
+ *
4181
+ * This function recursively converts Evolu's typed errors into the Standard
4182
+ * Schema issue format with proper path tracking for nested structures.
4183
+ *
4184
+ * @category Utilities
4185
+ */
4186
+ export const typeErrorToStandardSchemaIssues = <
4187
+ ExtraErrors extends TypeError = never,
4188
+ >(
4189
+ error: TypeErrors<ExtraErrors>,
4190
+ formatTypeError: TypeErrorFormatter<TypeErrors<ExtraErrors>>,
4191
+ path: ReadonlyArray<PropertyKey> = [],
4192
+ ): ReadonlyArray<StandardSchemaV1.Issue> => {
4193
+ if (error.type === "Array") {
4194
+ const arrayError = error as ArrayError;
4195
+ if (arrayError.reason.kind === "NotArray") {
4196
+ return [{ message: formatTypeError(error), path }];
4197
+ }
4198
+ return typeErrorToStandardSchemaIssues(
4199
+ arrayError.reason.error as TypeErrors<ExtraErrors>,
4200
+ formatTypeError,
4201
+ [...path, arrayError.reason.index],
4202
+ );
4203
+ }
4204
+
4205
+ if (error.type === "Object") {
4206
+ const objectError = error as ObjectError;
4207
+ if (
4208
+ objectError.reason.kind === "NotObject" ||
4209
+ objectError.reason.kind === "ExtraKeys"
4210
+ ) {
4211
+ return [{ message: formatTypeError(error), path }];
4212
+ }
4213
+ const issues: Array<StandardSchemaV1.Issue> = [];
4214
+ for (const [key, propError] of Object.entries(objectError.reason.errors)) {
4215
+ issues.push(
4216
+ ...typeErrorToStandardSchemaIssues(
4217
+ propError as TypeErrors<ExtraErrors>,
4218
+ formatTypeError,
4219
+ [...path, key],
4220
+ ),
4221
+ );
4222
+ }
4223
+ return issues;
4224
+ }
4225
+
4226
+ if (error.type === "ObjectWithRecord") {
4227
+ const objectWithRecordError = error as ObjectWithRecordError;
4228
+ if (objectWithRecordError.reason.kind === "NotObject") {
4229
+ return [{ message: formatTypeError(error), path }];
4230
+ }
4231
+ if (
4232
+ objectWithRecordError.reason.kind === "IndexKey" ||
4233
+ objectWithRecordError.reason.kind === "IndexValue"
4234
+ ) {
4235
+ return typeErrorToStandardSchemaIssues(
4236
+ objectWithRecordError.reason.error as TypeErrors<ExtraErrors>,
4237
+ formatTypeError,
4238
+ [...path, objectWithRecordError.reason.key as PropertyKey],
4239
+ );
4240
+ }
4241
+ const issues: Array<StandardSchemaV1.Issue> = [];
4242
+ for (const [key, propError] of Object.entries(
4243
+ objectWithRecordError.reason.errors,
4244
+ )) {
4245
+ issues.push(
4246
+ ...typeErrorToStandardSchemaIssues(
4247
+ propError as TypeErrors<ExtraErrors>,
4248
+ formatTypeError,
4249
+ [...path, key],
4250
+ ),
4251
+ );
4252
+ }
4253
+ return issues;
4254
+ }
4255
+
4256
+ if (error.type === "Record") {
4257
+ const recordError = error as RecordError;
4258
+ if (recordError.reason.kind === "NotRecord") {
4259
+ return [{ message: formatTypeError(error), path }];
4260
+ }
4261
+ return typeErrorToStandardSchemaIssues(
4262
+ recordError.reason.error as TypeErrors<ExtraErrors>,
4263
+ formatTypeError,
4264
+ [...path, recordError.reason.key as PropertyKey],
4265
+ );
4266
+ }
4267
+
4268
+ if (error.type === "Tuple") {
4269
+ const tupleError = error as TupleError;
4270
+ if (tupleError.reason.kind === "InvalidLength") {
4271
+ return [{ message: formatTypeError(error), path }];
4272
+ }
4273
+ return typeErrorToStandardSchemaIssues(
4274
+ tupleError.reason.error as TypeErrors<ExtraErrors>,
4275
+ formatTypeError,
4276
+ [...path, tupleError.reason.index],
4277
+ );
4278
+ }
4279
+
4280
+ if (error.type === "Union") {
4281
+ const unionError = error as UnionError;
4282
+ return unionError.errors.flatMap((err) =>
4283
+ typeErrorToStandardSchemaIssues(
4284
+ err as TypeErrors<ExtraErrors>,
4285
+ formatTypeError,
4286
+ path,
4287
+ ),
4288
+ );
4289
+ }
4290
+
4291
+ if (error.type === "Brand") {
4292
+ const brandError = error as BrandWithoutRefineError<TypeName, TypeError>;
4293
+ if ("parentError" in brandError) {
4294
+ return typeErrorToStandardSchemaIssues(
4295
+ brandError.parentError as TypeErrors<ExtraErrors>,
4296
+ formatTypeError,
4297
+ path,
4298
+ );
4299
+ }
4300
+ return [{ message: formatTypeError(error), path }];
4301
+ }
4302
+
4303
+ return [{ message: formatTypeError(error), path }];
4304
+ };
4305
+
4306
+ /** The Standard Schema interface. */
4307
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
4308
+ /** The Standard Schema properties. */
4309
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
4310
+ }
4311
+
4312
+ // eslint-disable-next-line @typescript-eslint/no-namespace
4313
+ export declare namespace StandardSchemaV1 {
4314
+ /** The Standard Schema properties interface. */
4315
+ export interface Props<Input = unknown, Output = Input> {
4316
+ /** The version number of the standard. */
4317
+ readonly version: 1;
4318
+ /** The vendor name of the schema library. */
4319
+ readonly vendor: string;
4320
+ /** Validates unknown input values. */
4321
+ readonly validate: (
4322
+ value: unknown,
4323
+ ) => Result<Output> | Promise<Result<Output>>;
4324
+ /** Inferred types associated with the schema. */
4325
+ readonly types?: Types<Input, Output> | undefined;
4326
+ }
4327
+
4328
+ /** The result interface of the validate function. */
4329
+ export type Result<Output> = SuccessResult<Output> | FailureResult;
4330
+
4331
+ /** The result interface if validation succeeds. */
4332
+ export interface SuccessResult<Output> {
4333
+ /** The typed output value. */
4334
+ readonly value: Output;
4335
+ /** The non-existent issues. */
4336
+ readonly issues?: undefined;
4337
+ }
4338
+
4339
+ /** The result interface if validation fails. */
4340
+ export interface FailureResult {
4341
+ /** The issues of failed validation. */
4342
+ readonly issues: ReadonlyArray<Issue>;
4343
+ }
4344
+
4345
+ /** The issue interface of the failure output. */
4346
+ export interface Issue {
4347
+ /** The error message of the issue. */
4348
+ readonly message: string;
4349
+ /** The path of the issue, if any. */
4350
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
4351
+ }
4352
+
4353
+ /** The path segment interface of the issue. */
4354
+ export interface PathSegment {
4355
+ /** The key representing a path segment. */
4356
+ readonly key: PropertyKey;
4357
+ }
4358
+
4359
+ /** The Standard Schema types interface. */
4360
+ export interface Types<Input = unknown, Output = Input> {
4361
+ /** The input type of the schema. */
4362
+ readonly input: Input;
4363
+ /** The output type of the schema. */
4364
+ readonly output: Output;
4365
+ }
4366
+
4367
+ /** Infers the input type of a Standard Schema. */
4368
+ export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
4369
+ Schema["~standard"]["types"]
4370
+ >["input"];
4371
+
4372
+ /** Infers the output type of a Standard Schema. */
4373
+ export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
4374
+ Schema["~standard"]["types"]
4375
+ >["output"];
4376
+ }
4377
+
4378
+ /**
4379
+ * Shared formatter cache for Standard Schema integration - avoids circular
4380
+ * dependency by lazily creating the formatter on first use rather than during
4381
+ * module initialization.
4382
+ */
4383
+ let cachedStandardSchemaFormatTypeError: TypeErrorFormatter<any> | undefined;