@evolu/common 6.0.1-preview.3 → 6.0.1-preview.30

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 (173) hide show
  1. package/dist/src/Array.d.ts +58 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +53 -5
  4. package/dist/src/Assert.d.ts +6 -16
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +6 -18
  7. package/dist/src/Brand.d.ts +75 -0
  8. package/dist/src/Brand.d.ts.map +1 -0
  9. package/dist/src/Brand.js +1 -0
  10. package/dist/src/Buffer.d.ts +1 -1
  11. package/dist/src/Buffer.d.ts.map +1 -1
  12. package/dist/src/Buffer.js +8 -7
  13. package/dist/src/Cache.d.ts +44 -0
  14. package/dist/src/Cache.d.ts.map +1 -0
  15. package/dist/src/Cache.js +52 -0
  16. package/dist/src/Callbacks.d.ts +45 -12
  17. package/dist/src/Callbacks.d.ts.map +1 -1
  18. package/dist/src/Callbacks.js +14 -7
  19. package/dist/src/Console.d.ts +31 -6
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +72 -9
  22. package/dist/src/Crypto.d.ts +61 -34
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +32 -45
  25. package/dist/src/Evolu/Db.d.ts +158 -65
  26. package/dist/src/Evolu/Db.d.ts.map +1 -1
  27. package/dist/src/Evolu/Db.js +286 -694
  28. package/dist/src/Evolu/Diff.d.ts +3 -3
  29. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  30. package/dist/src/Evolu/Diff.js +7 -5
  31. package/dist/src/Evolu/Evolu.d.ts +208 -133
  32. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  33. package/dist/src/Evolu/Evolu.js +188 -183
  34. package/dist/src/Evolu/Internal.d.ts +0 -2
  35. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  36. package/dist/src/Evolu/Internal.js +0 -2
  37. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  38. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  39. package/dist/src/Evolu/LocalAuth.js +174 -0
  40. package/dist/src/Evolu/Owner.d.ts +264 -120
  41. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  42. package/dist/src/Evolu/Owner.js +130 -104
  43. package/dist/src/Evolu/Platform.d.ts +9 -7
  44. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  45. package/dist/src/Evolu/Protocol.d.ts +277 -232
  46. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  47. package/dist/src/Evolu/Protocol.js +603 -378
  48. package/dist/src/Evolu/Public.d.ts +6 -8
  49. package/dist/src/Evolu/Public.d.ts.map +1 -1
  50. package/dist/src/Evolu/Public.js +2 -3
  51. package/dist/src/Evolu/PublicKysely.js +3 -3
  52. package/dist/src/Evolu/Query.d.ts +2 -1
  53. package/dist/src/Evolu/Query.d.ts.map +1 -1
  54. package/dist/src/Evolu/Relay.d.ts +92 -7
  55. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  56. package/dist/src/Evolu/Relay.js +243 -76
  57. package/dist/src/Evolu/Schema.d.ts +129 -73
  58. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  59. package/dist/src/Evolu/Schema.js +169 -89
  60. package/dist/src/Evolu/Storage.d.ts +212 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +137 -79
  63. package/dist/src/Evolu/Sync.d.ts +68 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +422 -20
  66. package/dist/src/Evolu/Timestamp.d.ts +85 -27
  67. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  68. package/dist/src/Evolu/Timestamp.js +77 -18
  69. package/dist/src/Identicon.d.ts +35 -0
  70. package/dist/src/Identicon.d.ts.map +1 -0
  71. package/dist/src/Identicon.js +143 -0
  72. package/dist/src/Instances.d.ts +34 -0
  73. package/dist/src/Instances.d.ts.map +1 -0
  74. package/dist/src/Instances.js +44 -0
  75. package/dist/src/ManyToManyMap.d.ts +71 -10
  76. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  77. package/dist/src/ManyToManyMap.js +41 -6
  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/Platform.d.ts +20 -0
  82. package/dist/src/Platform.d.ts.map +1 -0
  83. package/dist/src/Platform.js +22 -0
  84. package/dist/src/Random.d.ts +3 -2
  85. package/dist/src/Random.d.ts.map +1 -1
  86. package/dist/src/Resources.d.ts +118 -0
  87. package/dist/src/Resources.d.ts.map +1 -0
  88. package/dist/src/Resources.js +197 -0
  89. package/dist/src/Result.d.ts +184 -52
  90. package/dist/src/Result.d.ts.map +1 -1
  91. package/dist/src/Result.js +30 -241
  92. package/dist/src/Skiplist.js +2 -1
  93. package/dist/src/Sqlite.d.ts +63 -5
  94. package/dist/src/Sqlite.d.ts.map +1 -1
  95. package/dist/src/Sqlite.js +110 -9
  96. package/dist/src/Task.d.ts +586 -0
  97. package/dist/src/Task.d.ts.map +1 -0
  98. package/dist/src/Task.js +469 -0
  99. package/dist/src/Time.d.ts +66 -1
  100. package/dist/src/Time.d.ts.map +1 -1
  101. package/dist/src/Time.js +99 -5
  102. package/dist/src/Type.d.ts +621 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +665 -464
  105. package/dist/src/Types.d.ts +1 -75
  106. package/dist/src/Types.d.ts.map +1 -1
  107. package/dist/src/WebSocket.d.ts +5 -2
  108. package/dist/src/WebSocket.d.ts.map +1 -1
  109. package/dist/src/WebSocket.js +12 -18
  110. package/dist/src/Worker.d.ts +39 -11
  111. package/dist/src/Worker.d.ts.map +1 -1
  112. package/dist/src/Worker.js +22 -4
  113. package/dist/src/index.d.ts +7 -2
  114. package/dist/src/index.d.ts.map +1 -1
  115. package/dist/src/index.js +7 -2
  116. package/package.json +14 -13
  117. package/src/Array.ts +76 -11
  118. package/src/Assert.ts +6 -24
  119. package/src/Brand.ts +75 -0
  120. package/src/Buffer.ts +7 -7
  121. package/src/Cache.ts +85 -0
  122. package/src/Callbacks.ts +62 -22
  123. package/src/Console.ts +91 -11
  124. package/src/Crypto.ts +97 -82
  125. package/src/Evolu/Db.ts +514 -1020
  126. package/src/Evolu/Diff.ts +7 -5
  127. package/src/Evolu/Evolu.ts +464 -355
  128. package/src/Evolu/Internal.ts +0 -2
  129. package/src/Evolu/LocalAuth.ts +463 -0
  130. package/src/Evolu/Owner.ts +369 -228
  131. package/src/Evolu/Platform.ts +9 -9
  132. package/src/Evolu/Protocol.ts +859 -676
  133. package/src/Evolu/Public.ts +7 -14
  134. package/src/Evolu/PublicKysely.ts +3 -3
  135. package/src/Evolu/Query.ts +2 -1
  136. package/src/Evolu/Relay.ts +420 -92
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +451 -118
  139. package/src/Evolu/Sync.ts +720 -37
  140. package/src/Evolu/Timestamp.ts +88 -35
  141. package/src/Identicon.ts +197 -0
  142. package/src/Instances.ts +90 -0
  143. package/src/ManyToManyMap.ts +124 -24
  144. package/src/Number.ts +6 -10
  145. package/src/Platform.ts +26 -0
  146. package/src/Random.ts +3 -2
  147. package/src/Resources.ts +367 -0
  148. package/src/Result.ts +191 -54
  149. package/src/Skiplist.ts +1 -1
  150. package/src/Sqlite.ts +122 -17
  151. package/src/Task.ts +901 -0
  152. package/src/Time.ts +180 -5
  153. package/src/Type.ts +1083 -727
  154. package/src/Types.ts +1 -77
  155. package/src/WebSocket.ts +27 -25
  156. package/src/Worker.ts +72 -23
  157. package/src/index.ts +7 -2
  158. package/dist/src/Evolu/Config.d.ts +0 -69
  159. package/dist/src/Evolu/Config.d.ts.map +0 -1
  160. package/dist/src/Evolu/Config.js +0 -9
  161. package/dist/src/Evolu/Kysely.d.ts +0 -6
  162. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  163. package/dist/src/Evolu/Kysely.js +0 -21
  164. package/dist/src/NanoId.d.ts +0 -27
  165. package/dist/src/NanoId.d.ts.map +0 -1
  166. package/dist/src/NanoId.js +0 -6
  167. package/dist/src/Promise.d.ts +0 -180
  168. package/dist/src/Promise.d.ts.map +0 -1
  169. package/dist/src/Promise.js +0 -176
  170. package/src/Evolu/Config.ts +0 -83
  171. package/src/Evolu/Kysely.ts +0 -38
  172. package/src/NanoId.ts +0 -39
  173. package/src/Promise.ts +0 -295
package/src/Type.ts CHANGED
@@ -1,84 +1,191 @@
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
+ * precise, composable error value telling us exactly why validation failed.
5
7
  *
6
- * You probably know [Zod](https://zod.dev). Evolu has {@link Type}.
8
+ * Evolu Type supports [Standard Schema](https://standardschema.dev/) for
9
+ * interoperability with 40+ validation-compatible tools and frameworks.
7
10
  *
8
- * Evolu Type exists because no existing validation/parsing/transformation
9
- * library fully met our needs:
11
+ * Why another validation library?
10
12
  *
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.
13
+ * - **Result-based error handling** no exceptions for normal control flow.
14
+ * - **Typed errors with decoupled formatters** – validation logic ≠ user
15
+ * messages.
16
+ * - **Consistent constraints via {@link Brand}** every constraint becomes part
17
+ * of the type.
18
+ * - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
19
+ * in mind.
20
+ * - **Selective validation** parent validations are skipped when already proved
21
+ * by typing.
22
+ * - **Simple, top-down implementation** – readable source code from top to bottom
23
+ * with no hidden magic; just plain functions and composition.
22
24
  *
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.
25
+ * ### Base Types Quick Start
27
26
  *
28
- * - Evolu `Type` is:
27
+ * ```ts
28
+ * // Validate unknown values
29
+ * const value: unknown = "hello";
30
+ * const stringResult = String.fromUnknown(value);
31
+ * if (!stringResult.ok) {
32
+ * // console.error(formatStringError(stringResult.error));
33
+ * return stringResult; // inside a function returning Result<string, _>
34
+ * }
35
+ * // Safe branch: value is now string
36
+ * const upper = stringResult.value.toUpperCase();
37
+ *
38
+ * // Type guard style
39
+ * if (String.is(value)) {
40
+ * // narrowed to string
41
+ * }
42
+ *
43
+ * // Composing: arrays & objects
44
+ * const Numbers = array(Number); // ReadonlyArray<number>
45
+ * const Point = object({ x: Number, y: Number });
46
+ *
47
+ * Numbers.from([1, 2, 3]); // ok
48
+ * Point.from({ x: 1, y: 2 }); // ok
49
+ * Point.from({ x: 1, y: "2" }); // err -> nested Number error
50
+ * ```
51
+ *
52
+ * ### Branding Basics
53
+ *
54
+ * Branding adds semantic meaning & constraints while preserving the runtime
55
+ * shape:
56
+ *
57
+ * ```ts
58
+ * const CurrencyCode = brand("CurrencyCode", String, (value) =>
59
+ * /^[A-Z]{3}$/.test(value)
60
+ * ? ok(value)
61
+ * : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
62
+ * );
63
+ * type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
64
+ *
65
+ * interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
66
+ *
67
+ * const formatCurrencyCodeError =
68
+ * createTypeErrorFormatter<CurrencyCodeError>(
69
+ * (error) => `Invalid currency code: ${error.value}`,
70
+ * );
71
+ *
72
+ * const r = CurrencyCode.from("USD"); // ok("USD")
73
+ * const e = CurrencyCode.from("usd"); // err(...)
74
+ * ```
75
+ *
76
+ * See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
77
+ * `positive`, `between`, etc.
78
+ *
79
+ * ### Objects & Optional Fields
80
+ *
81
+ * ```ts
82
+ * const User = object({
83
+ * name: NonEmptyTrimmedString100,
84
+ * age: optional(PositiveInt),
85
+ * });
86
+ * type User = typeof User.Type;
87
+ *
88
+ * User.from({ name: "Alice" }); // ok
89
+ * User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
90
+ * ```
91
+ *
92
+ * ### Deriving JSON String Types
93
+ *
94
+ * ```ts
95
+ * const Person = object({
96
+ * name: NonEmptyString50,
97
+ * // Did you know that JSON.stringify converts NaN (a number) into null?
98
+ * // To prevent this, use FiniteNumber.
99
+ * age: FiniteNumber,
100
+ * });
101
+ * type Person = typeof Person.Type;
102
+ *
103
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
104
+ * Person,
105
+ * "PersonJson",
106
+ * );
107
+ * // string & Brand<"PersonJson">
108
+ * type PersonJson = typeof PersonJson.Type;
109
+ *
110
+ * const person = Person.orThrow({
111
+ * name: "Alice",
112
+ * age: 30,
113
+ * });
114
+ *
115
+ * const personJson = personToPersonJson(person);
116
+ * expect(personJsonToPerson(personJson)).toEqual(person);
117
+ * ```
29
118
  *
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.
119
+ * ### Error Formatting
34
120
  *
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:
121
+ * Evolu separates validation logic from human-readable messages. There are two
122
+ * layers:
39
123
  *
40
- * `Unknown` -> `String` -> `TrimmedString` -> `NonEmptyTrimmedString100`
124
+ * 1. Per-type formatters (e.g. `formatStringError`) simple, focused, already
125
+ * used earlier in the quick start example.
126
+ * 2. A unified formatter via `createFormatTypeError` – composes all built-in and
127
+ * custom errors (including nested composite types) and lets us override
128
+ * selected messages.
41
129
  *
42
- * For `NonEmptyTrimmedString100`, the parent Type is `TrimmedString`. For
43
- * `TrimmedString`, the parent Type is `String`.
130
+ * #### 1. Per-Type Formatter (recap)
44
131
  *
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.
132
+ * ```ts
133
+ * const r = String.fromUnknown(42);
134
+ * if (!r.ok) console.error(formatStringError(r.error));
135
+ * ```
48
136
  *
49
- * Speaking of `fromParent` and `toParent`, those functions exist to bypass
50
- * parent Types when we can rely on TypeScript types.
137
+ * #### 2. Unified Formatter with Overrides
51
138
  *
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")`.
139
+ * ```ts
140
+ * // Override only what we care about; fall back to built-ins for the rest.
141
+ * const formatTypeError = createFormatTypeError((error) => {
142
+ * if (error.type === "MinLength") return `Min length is ${error.min}`;
143
+ * });
144
+ *
145
+ * const User = object({ name: NonEmptyTrimmedString100 });
146
+ * const resultUser = User.from({ name: "" });
147
+ * if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
148
+ *
149
+ * const badPoint = object({ x: Number, y: Number }).from({
150
+ * x: 1,
151
+ * y: "foo",
152
+ * });
153
+ * if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
154
+ * ```
155
+ *
156
+ * The unified formatter walks nested structures (object / array / record /
157
+ * tuple / union) and applies overrides only where specified, greatly reducing
158
+ * boilerplate when formatting complex validation errors.
57
159
  *
58
160
  * ### Tip
59
161
  *
60
162
  * If necessary, write `globalThis.String` instead of `String` to avoid naming
61
- * clashes with Base Types.
163
+ * clashes with native types.
62
164
  *
63
- * ### Design Decision:
165
+ * ### Design Decision: No Bidirectional Transformations
64
166
  *
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.
167
+ * Evolu Type intentionally does not support bidirectional transformations. It
168
+ * previously did, but supporting that while keeping typed error fidelity added
169
+ * complexity that hurt readability & reliability. Most persistence pipelines
170
+ * (e.g. SQLite) already require explicit mapping of query results, so implicit
171
+ * reverse transforms would not buy much. We may revisit this if we can design a
172
+ * minimal, 100% safe API that preserves simplicity.
69
173
  *
70
174
  * @module
71
175
  */
72
176
 
177
+ import { utf8ToBytes } from "@noble/ciphers/utils.js";
178
+ import { sha256 } from "@noble/hashes/sha2.js";
73
179
  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";
180
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
181
+ import { pack } from "msgpackr";
182
+ import type { Brand } from "./Brand.js";
183
+ import { type RandomBytesDep } from "./Crypto.js";
78
184
  import { isPlainObject } from "./Object.js";
79
- import { Err, err, Ok, ok, Result, trySync } from "./Result.js";
185
+ import { hasNodeBuffer } from "./Platform.js";
186
+ import { err, getOrNull, getOrThrow, ok, Result, trySync } from "./Result.js";
80
187
  import { safelyStringifyUnknownValue } from "./String.js";
81
- import type { Brand, Literal, Simplify, WidenLiteral } from "./Types.js";
188
+ import type { Literal, Simplify, WidenLiteral } from "./Types.js";
82
189
  import { IntentionalNever } from "./Types.js";
83
190
 
84
191
  export interface Type<
@@ -93,7 +200,7 @@ export interface Type<
93
200
  Parent = T,
94
201
  /** The parent's error. */
95
202
  ParentError extends TypeError = Error,
96
- > {
203
+ > extends StandardSchemaV1<Input, T> {
97
204
  readonly name: Name;
98
205
 
99
206
  /**
@@ -106,41 +213,98 @@ export interface Type<
106
213
  readonly from: (value: Input) => Result<T, ParentError | Error>;
107
214
 
108
215
  /**
109
- * Creates `T` from an unknown value.
216
+ * Creates `T` from an `Input` value, throwing an error if validation fails.
110
217
  *
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`.
218
+ * Throws an Error with the Type validation error in its `cause` property,
219
+ * making it debuggable while avoiding the need for custom error messages.
220
+ *
221
+ * This is a convenience method that combines `from` with `getOrThrow`.
222
+ *
223
+ * **When to use:**
224
+ *
225
+ * - Configuration values that are guaranteed to be valid (e.g., hardcoded
226
+ * constants)
227
+ * - Application startup where failure should crash the program
228
+ * - As an alternative to assertions when the Type error in the thrown Error's
229
+ * `cause` provides sufficient debugging information
230
+ * - Test code with known valid inputs (when error message clarity is not
231
+ * critical; for better test error messages, use Vitest `schemaMatching` +
232
+ * `assert` with `.is()`)
233
+ *
234
+ * ### Example
235
+ *
236
+ * ```ts
237
+ * // ✅ Good: Known valid constant
238
+ * const maxRetries = PositiveInt.orThrow(3);
239
+ *
240
+ * // ✅ Good: App configuration that should crash on invalid values
241
+ * const appName = SimpleName.orThrow("MyApp");
242
+ *
243
+ * // ✅ Good: Instead of assert when Type error is clear enough
244
+ * // Context makes it obvious: count increments from non-negative value
245
+ * const currentCount = counts.get(id) ?? 0;
246
+ * const newCount = PositiveInt.orThrow(currentCount + 1);
117
247
  *
118
- * This is useful to transform `T` back to its `Input` representation.
248
+ * // Good: Test setup with known valid values
249
+ * const testUser = User.orThrow({ name: "Alice", age: 30 });
119
250
  *
120
- * For `refine`, it only removes the brand. For `transform`, it changes value.
251
+ * // Avoid: User input (use `from` instead)
252
+ * const userAge = PositiveInt.orThrow(userInput); // Could crash!
253
+ *
254
+ * // ✅ Better: Handle user input gracefully
255
+ * const ageResult = PositiveInt.from(userInput);
256
+ * if (!ageResult.ok) {
257
+ * // Handle validation error
258
+ * }
259
+ * ```
121
260
  */
122
- readonly to: (value: T) => Input;
261
+ readonly orThrow: (value: Input) => T;
123
262
 
124
263
  /**
125
- * Creates `T` from `Parent` type.
264
+ * Creates `T` from an `Input` value, returning `null` if validation fails.
265
+ *
266
+ * This is a convenience method that combines `from` with `getOrNull`.
126
267
  *
127
- * This function skips parent Types validations/transformations when we have
128
- * already partially validated/transformed value.
268
+ * **When to use:**
129
269
  *
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`.
270
+ * - When you need to convert a validation result to a nullable value
271
+ * - When the error is not important and you just want the value or nothing
132
272
  *
133
273
  * ### Example
134
274
  *
135
275
  * ```ts
136
- * // string & Brand<"Trimmed">
137
- * const value = TrimString.fromParent("a ").value; // as efficient as foo.trim()
276
+ * // Good: Optional user input
277
+ * const age = PositiveInt.orNull(userInput);
278
+ * if (age != null) {
279
+ * console.log("Valid age:", age);
280
+ * }
281
+ *
282
+ * // ✅ Good: Default fallback
283
+ * const maxRetries = PositiveInt.orNull(config.retries) ?? 3;
284
+ *
285
+ * // ❌ Avoid: When you need to know why validation failed (use `from` instead)
286
+ * const result = PositiveInt.from(userInput);
287
+ * if (!result.ok) {
288
+ * console.error(formatPositiveError(result.error));
289
+ * }
138
290
  * ```
139
291
  */
140
- readonly fromParent: (value: Parent) => Result<T, Error>;
292
+ readonly orNull: (value: Input) => T | null;
293
+
294
+ /**
295
+ * Creates `T` from an unknown value.
296
+ *
297
+ * This is useful when a value is unknown.
298
+ */
299
+ readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
141
300
 
142
- /** The opposite of `fromParent`. */
143
- readonly toParent: (value: T) => Parent;
301
+ /**
302
+ * Creates `T` from `Parent` type.
303
+ *
304
+ * This function skips parent Types validations when we have already partially
305
+ * validated value.
306
+ */
307
+ readonly fromParent: (value: Parent) => Result<T, Error>;
144
308
 
145
309
  /**
146
310
  * A **type guard** that checks whether an unknown value satisfies the
@@ -221,8 +385,6 @@ export interface Type<
221
385
  readonly ParentError: ParentError;
222
386
 
223
387
  /**
224
- * Error | ParentError
225
- *
226
388
  * ### Example
227
389
  *
228
390
  * ```ts
@@ -261,26 +423,61 @@ export interface TypeErrorWithReason<
261
423
 
262
424
  export type AnyType = Type<any, any, any, any, any, any>;
263
425
 
426
+ /**
427
+ * Extracts the name from a {@link Type}.
428
+ *
429
+ * @category Utilities
430
+ */
264
431
  export type InferName<A extends AnyType> =
265
432
  A extends Type<infer Name, any, any, any, any, any> ? Name : never;
266
433
 
434
+ /**
435
+ * Extracts the type from a {@link Type}.
436
+ *
437
+ * @category Utilities
438
+ */
267
439
  export type InferType<A extends AnyType> =
268
440
  A extends Type<any, infer T, any, any, any, any> ? T : never;
269
441
 
442
+ /**
443
+ * Extracts the input type from a {@link Type}.
444
+ *
445
+ * @category Utilities
446
+ */
270
447
  export type InferInput<A extends AnyType> =
271
448
  A extends Type<any, any, infer Input, any, any, any> ? Input : never;
272
449
 
450
+ /**
451
+ * Extracts the specific error type from a {@link Type}.
452
+ *
453
+ * @category Utilities
454
+ */
273
455
  export type InferError<A extends AnyType> =
274
456
  A extends Type<any, any, any, infer Error, any, any> ? Error : never;
275
457
 
458
+ /**
459
+ * Extracts the parent type from a {@link Type}.
460
+ *
461
+ * @category Utilities
462
+ */
276
463
  export type InferParent<A extends AnyType> =
277
464
  A extends Type<any, any, any, any, infer Parent, any> ? Parent : never;
278
465
 
466
+ /**
467
+ * Extracts the parent error type from a {@link Type}.
468
+ *
469
+ * @category Utilities
470
+ */
279
471
  export type InferParentError<A extends AnyType> =
280
472
  A extends Type<any, any, any, any, any, infer ParentError>
281
473
  ? ParentError
282
474
  : never;
283
475
 
476
+ /**
477
+ * Extracts all error types from a {@link Type}.
478
+ *
479
+ * @category Utilities
480
+ */
284
481
  export type InferErrors<T extends AnyType> =
285
482
  T extends Type<any, any, any, infer Error, any, infer ParentError>
286
483
  ? Error | ParentError
@@ -310,6 +507,8 @@ const createType = <
310
507
  | "name"
311
508
  | "is"
312
509
  | "from"
510
+ | "orThrow"
511
+ | "orNull"
313
512
  | typeof EvoluTypeSymbol
314
513
  | "Type"
315
514
  | "Input"
@@ -317,12 +516,15 @@ const createType = <
317
516
  | "Parent"
318
517
  | "ParentError"
319
518
  | "Errors"
519
+ | "~standard"
320
520
  >,
321
521
  ): Type<Name, T, Input, Error, Parent, ParentError> => ({
322
522
  ...definition,
323
523
  name,
324
524
  is: (value: unknown): value is T => definition.fromUnknown(value).ok,
325
525
  from: definition.fromUnknown,
526
+ orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
527
+ orNull: (value) => getOrNull(definition.fromUnknown(value)),
326
528
  [EvoluTypeSymbol]: true,
327
529
  Type: undefined as unknown as T,
328
530
  Input: undefined as unknown as Input,
@@ -330,6 +532,27 @@ const createType = <
330
532
  Parent: undefined as unknown as Parent,
331
533
  ParentError: undefined as unknown as ParentError,
332
534
  Errors: undefined as unknown as Error | ParentError,
535
+ "~standard": {
536
+ version: 1,
537
+ vendor: "evolu",
538
+ validate: (value: unknown): StandardSchemaV1.Result<T> => {
539
+ const result = definition.fromUnknown(value);
540
+ if (result.ok) {
541
+ return { value: result.value };
542
+ }
543
+ cachedStandardSchemaFormatTypeError ??= createFormatTypeError();
544
+ return {
545
+ issues: typeErrorToStandardSchemaIssues(
546
+ result.error as TypeErrors<Error>,
547
+ cachedStandardSchemaFormatTypeError,
548
+ ),
549
+ };
550
+ },
551
+ types: {
552
+ input: undefined as unknown as Input,
553
+ output: undefined as unknown as T,
554
+ },
555
+ },
333
556
  });
334
557
 
335
558
  /**
@@ -365,12 +588,6 @@ export type TypeErrorFormatter<Error extends TypeError> = (
365
588
  * Base {@link Type}.
366
589
  *
367
590
  * 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
591
  *
375
592
  * ### Example
376
593
  *
@@ -396,9 +613,7 @@ export const base = <Name extends TypeName, T, Error extends TypeError>(
396
613
  ): Type<Name, T, T, Error> =>
397
614
  createType(name, {
398
615
  fromUnknown,
399
- to: identity,
400
616
  fromParent: ok<T>, // `fromParent` relies on types, so it can't fail for the Base Type
401
- toParent: identity,
402
617
  });
403
618
 
404
619
  /**
@@ -564,7 +779,7 @@ export interface InstanceOfType<T extends abstract new (...args: any) => any>
564
779
  }
565
780
 
566
781
  export const formatInstanceOfError = createTypeErrorFormatter<InstanceOfError>(
567
- (error) => `Value ${error.value} is not an instance of ${error.ctor}`,
782
+ (error) => `The value ${error.value} is not an instance of ${error.ctor}.`,
568
783
  );
569
784
 
570
785
  /**
@@ -607,7 +822,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
607
822
  * The `brand` Type Factory takes the name of a new {@link Brand}, a parent Type
608
823
  * to be branded, and the optional `refine` function for additional constraint.
609
824
  *
610
- * If the `refine` function is omited, TODO:
825
+ * The `refine` function can be omitted if we only want to add a brand.
611
826
  *
612
827
  * ### Examples
613
828
  *
@@ -686,7 +901,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
686
901
  * confirmPassword: SimplePassword,
687
902
  * });
688
903
  *
689
- * const ValidForm = brand("Valid", Form, (value) => {
904
+ * const ValidForm = brand("ValidForm", Form, (value) => {
690
905
  * if (value.password !== value.confirmPassword)
691
906
  * return err<ValidFormError>({
692
907
  * type: "ValidForm",
@@ -780,9 +995,7 @@ export function brand<
780
995
  return {
781
996
  ...createType("Brand", {
782
997
  fromUnknown,
783
- to: identity,
784
998
  fromParent: (refine ?? ok) as IntentionalNever,
785
- toParent: identity,
786
999
  }),
787
1000
  brand: name,
788
1001
  parentType: parent,
@@ -830,7 +1043,7 @@ export interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
830
1043
 
831
1044
  export const formatCurrencyCodeError =
832
1045
  createTypeErrorFormatter<CurrencyCodeError>(
833
- (error) => `Invalid currency code: ${error.value}`,
1046
+ (error) => `Invalid currency code: ${error.value}.`,
834
1047
  );
835
1048
 
836
1049
  /**
@@ -848,30 +1061,41 @@ export const formatCurrencyCodeError =
848
1061
  * ### Example
849
1062
  *
850
1063
  * ```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
1064
+ * const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
1065
+ * const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
853
1066
  * ```
854
1067
  *
855
1068
  * @category String
856
1069
  */
857
- export const DateIsoString = brand("DateIso", String, (value) => {
1070
+ export const DateIso = brand("DateIso", String, (value) => {
858
1071
  if (value.length !== 24) {
859
- return err<DateIsoStringError>({ type: "DateIsoString", value });
1072
+ return err<DateIsoError>({ type: "DateIso", value });
1073
+ }
1074
+ const parsed = globalThis.Date.parse(value);
1075
+ if (isNaN(parsed)) {
1076
+ return err<DateIsoError>({ type: "DateIso", value });
860
1077
  }
861
- if (isNaN(globalThis.Date.parse(value))) {
862
- return err<DateIsoStringError>({ type: "DateIsoString", value });
1078
+ // Round-trip test: ensure the string is actually a proper ISO format
1079
+ const roundTrip = new globalThis.Date(parsed).toISOString();
1080
+ if (roundTrip !== value) {
1081
+ return err<DateIsoError>({ type: "DateIso", value });
863
1082
  }
864
1083
  return ok(value);
865
1084
  });
866
1085
 
867
- export type DateIsoString = typeof DateIsoString.Type;
1086
+ export type DateIso = typeof DateIso.Type;
868
1087
 
869
- export interface DateIsoStringError extends TypeError<"DateIsoString"> {}
1088
+ export interface DateIsoError extends TypeError<"DateIso"> {}
870
1089
 
871
- export const formatDateIsoStringError =
872
- createTypeErrorFormatter<DateIsoStringError>(
873
- (error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
874
- );
1090
+ export const formatDateIsoError = createTypeErrorFormatter<DateIsoError>(
1091
+ (error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
1092
+ );
1093
+
1094
+ export const dateToDateIso = (value: Date): Result<DateIso, DateIsoError> =>
1095
+ DateIso.fromParent(value.toISOString());
1096
+
1097
+ export const dateIsoToDate = (value: DateIso): Date =>
1098
+ new globalThis.Date(value);
875
1099
 
876
1100
  /**
877
1101
  * Helper type for Type Factory that creates a branded Type.
@@ -914,18 +1138,12 @@ export type BrandFactory<
914
1138
  /**
915
1139
  * Trimmed string.
916
1140
  *
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.
1141
+ * This Type Factory validates whether a string has no leading or trailing
1142
+ * whitespaces.
920
1143
  *
921
- * ### Examples
1144
+ * ### Example
922
1145
  *
923
1146
  * ```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
1147
  * const TrimmedNonEmptyString = trimmed(minLength(1)(String));
930
1148
  * // string & Brand<"MinLength1"> & Brand<"Trimmed">
931
1149
  * type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
@@ -945,67 +1163,9 @@ export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
945
1163
  export interface TrimmedError extends TypeError<"Trimmed"> {}
946
1164
 
947
1165
  export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
948
- (error) => `A value ${error.value} is not trimmed`,
1166
+ (error) => `The value ${error.value} must be trimmed.`,
949
1167
  );
950
1168
 
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
1169
  /**
1010
1170
  * Trimmed string
1011
1171
  *
@@ -1017,6 +1177,9 @@ export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
1017
1177
  export const TrimmedString = trimmed(String);
1018
1178
  export type TrimmedString = typeof TrimmedString.Type;
1019
1179
 
1180
+ export const trim = (value: string): TrimmedString =>
1181
+ value.trim() as TrimmedString;
1182
+
1020
1183
  /**
1021
1184
  * Minimum length.
1022
1185
  *
@@ -1045,7 +1208,7 @@ export interface MinLengthError<Min extends number = number>
1045
1208
 
1046
1209
  export const formatMinLengthError = createTypeErrorFormatter<MinLengthError>(
1047
1210
  (error) =>
1048
- `Value ${error.value} does not meet the minimum length of ${error.min}.`,
1211
+ `The value ${error.value} does not meet the minimum length of ${error.min}.`,
1049
1212
  );
1050
1213
 
1051
1214
  /**
@@ -1075,7 +1238,8 @@ export interface MaxLengthError<Max extends number = number>
1075
1238
  }
1076
1239
 
1077
1240
  export const formatMaxLengthError = createTypeErrorFormatter<MaxLengthError>(
1078
- (error) => `Value ${error.value} exceeds the maximum length of ${error.max}.`,
1241
+ (error) =>
1242
+ `The value ${error.value} exceeds the maximum length of ${error.max}.`,
1079
1243
  );
1080
1244
 
1081
1245
  /**
@@ -1108,7 +1272,7 @@ export interface LengthError<Exact extends number = number>
1108
1272
 
1109
1273
  export const formatLengthError = createTypeErrorFormatter<LengthError>(
1110
1274
  (error) =>
1111
- `Value ${error.value} does not have the required length of ${error.exact}.`,
1275
+ `The value ${error.value} does not have the required length of ${error.exact}.`,
1112
1276
  );
1113
1277
 
1114
1278
  /** @category String */
@@ -1170,7 +1334,7 @@ export type Mnemonic = typeof Mnemonic.Type;
1170
1334
  export interface MnemonicError extends TypeError<"Mnemonic"> {}
1171
1335
 
1172
1336
  export const formatMnemonicError = createTypeErrorFormatter<MnemonicError>(
1173
- (error) => `Invalid BIP39 mnemonic: ${error.value}`,
1337
+ (error) => `Invalid BIP39 mnemonic: ${error.value}.`,
1174
1338
  );
1175
1339
 
1176
1340
  /**
@@ -1209,13 +1373,13 @@ export interface RegexError<Name extends TypeName = TypeName>
1209
1373
 
1210
1374
  export const formatRegexError = createTypeErrorFormatter<RegexError>(
1211
1375
  (error) =>
1212
- `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`,
1376
+ `The value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}.`,
1213
1377
  );
1214
1378
 
1215
1379
  /**
1216
- * URL-safe Base64 string.
1380
+ * URL-safe string.
1217
1381
  *
1218
- * A `Base64Url` string uses a limited alphabet that is URL-safe:
1382
+ * A `UrlSafeString` uses a limited alphabet that is safe for URLs:
1219
1383
  *
1220
1384
  * - Uppercase letters (`A-Z`)
1221
1385
  * - Lowercase letters (`a-z`)
@@ -1223,35 +1387,115 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
1223
1387
  * - Dash (`-`)
1224
1388
  * - Underscore (`_`)
1225
1389
  *
1390
+ * This is the same character set used by Base64Url encoding, but this type does
1391
+ * not validate that the string is actually Base64Url-encoded data.
1392
+ *
1226
1393
  * ### Example
1227
1394
  *
1228
1395
  * ```ts
1229
- * const result = Base64Url.from("abc123_-");
1396
+ * const result = UrlSafeString.from("abc123_-");
1230
1397
  * if (result.ok) {
1231
- * console.log("Valid Base64Url string:", result.value);
1398
+ * console.log("Valid URL-safe string:", result.value);
1232
1399
  * } else {
1233
- * console.error("Invalid Base64Url string:", result.error);
1400
+ * console.error("Invalid URL-safe string:", result.error);
1234
1401
  * }
1235
1402
  * ```
1236
1403
  *
1237
1404
  * @category String
1238
1405
  */
1239
- export const Base64Url = regex(
1406
+ export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
1407
+ export type UrlSafeString = typeof UrlSafeString.Type;
1408
+ export type UrlSafeStringError = typeof UrlSafeString.Error;
1409
+
1410
+ /**
1411
+ * Base64Url without padding.
1412
+ *
1413
+ * Encode with {@link uint8ArrayToBase64Url}, decode with
1414
+ * {@link base64UrlToUint8Array}.
1415
+ *
1416
+ * @category String
1417
+ */
1418
+ export const Base64Url = brand(
1240
1419
  "Base64Url",
1241
- /^[A-Za-z0-9_-]+$/, // URL-safe Base64 alphabet
1242
- )(String);
1420
+ String,
1421
+ (value: string): Result<string, Base64UrlError> => {
1422
+ // Round-trip validation ensures consistency across different base64url
1423
+ // implementations (Node.js Buffer, native browser API, manual fallback).
1424
+ // Only strings that decode and encode identically are accepted.
1425
+ let roundTrip;
1426
+ try {
1427
+ roundTrip = uint8ArrayToBase64Url(
1428
+ base64UrlToUint8Array(value as Base64Url),
1429
+ );
1430
+ } catch {
1431
+ //
1432
+ }
1433
+ return roundTrip === value
1434
+ ? ok(value)
1435
+ : err<Base64UrlError>({ type: "Base64Url", value });
1436
+ },
1437
+ );
1243
1438
  export type Base64Url = typeof Base64Url.Type;
1244
- export type Base64UrlError = typeof Base64Url.Error;
1439
+ export interface Base64UrlError extends TypeError<"Base64Url"> {}
1440
+
1441
+ export const formatBase64UrlError = createTypeErrorFormatter<Base64UrlError>(
1442
+ (error) => `The value ${error.value} is not a valid Base64Url string.`,
1443
+ );
1444
+
1445
+ const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
1446
+
1447
+ /** Encodes a Uint8Array to a {@link Base64Url} string. */
1448
+ export const uint8ArrayToBase64Url: (bytes: Uint8Array) => Base64Url =
1449
+ hasNodeBuffer
1450
+ ? (bytes: Uint8Array) =>
1451
+ globalThis.Buffer.from(bytes).toString("base64url") as Base64Url
1452
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1453
+ typeof (globalThis.Uint8Array.prototype as any)?.toBase64 !== "undefined"
1454
+ ? (bytes: Uint8Array) =>
1455
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
1456
+ (bytes as any).toBase64(base64UrlOptions) as Base64Url
1457
+ : (bytes: Uint8Array) => {
1458
+ const binaryString = Array.from(bytes, (byte) =>
1459
+ globalThis.String.fromCodePoint(byte),
1460
+ ).join("");
1461
+ const base64 = globalThis.btoa(binaryString);
1462
+ return base64
1463
+ .replace(/\+/g, "-")
1464
+ .replace(/\//g, "_")
1465
+ .replace(/=/g, "") as Base64Url;
1466
+ };
1467
+
1468
+ /** Decodes a {@link Base64Url} string to a Uint8Array. */
1469
+ export const base64UrlToUint8Array: (str: Base64Url) => Uint8Array =
1470
+ hasNodeBuffer
1471
+ ? (str: Base64Url) => {
1472
+ const nodeBuffer = globalThis.Buffer.from(str, "base64url");
1473
+ return new globalThis.Uint8Array(nodeBuffer);
1474
+ }
1475
+ : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1476
+ typeof (globalThis.Uint8Array as any)?.fromBase64 !== "undefined"
1477
+ ? (str: Base64Url) =>
1478
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
1479
+ (globalThis.Uint8Array as any).fromBase64(
1480
+ str,
1481
+ base64UrlOptions,
1482
+ ) as Uint8Array
1483
+ : (str: Base64Url) => {
1484
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
1485
+ while (base64.length % 4 !== 0) {
1486
+ base64 += "=";
1487
+ }
1488
+ const binaryString = globalThis.atob(base64);
1489
+ return globalThis.Uint8Array.from(binaryString, (c) =>
1490
+ c.charCodeAt(0),
1491
+ );
1492
+ };
1245
1493
 
1246
1494
  /**
1247
- * Simple alphanumeric string for naming.
1248
- *
1249
- * A `SimpleName` string uses a limited, safe alphabet for naming purposes:
1495
+ * Simple alphanumeric string for naming in file systems, URLs, and identifiers.
1250
1496
  *
1251
- * - Uppercase letters (`A-Z`)
1252
- * - Lowercase letters (`a-z`)
1253
- * - Digits (`0-9`)
1254
- * - Dash (`-`)
1497
+ * Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
1498
+ * `_`). See `UrlSafeString` for details.
1255
1499
  *
1256
1500
  * The string must be between 1 and 42 characters.
1257
1501
  *
@@ -1268,18 +1512,13 @@ export type Base64UrlError = typeof Base64Url.Error;
1268
1512
  *
1269
1513
  * @category String
1270
1514
  */
1271
- export const SimpleName = regex("SimpleName", /^[a-z0-9-]{1,42}$/i)(String);
1515
+ export const SimpleName = brand("SimpleName", UrlSafeString, (value) =>
1516
+ value.length >= 1 && value.length <= 42
1517
+ ? ok(value)
1518
+ : err<SimpleNameError>({ type: "SimpleName", value }),
1519
+ );
1272
1520
  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;
1521
+ export interface SimpleNameError extends TypeError<"SimpleName"> {}
1283
1522
 
1284
1523
  /**
1285
1524
  * Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
@@ -1304,17 +1543,56 @@ export const formatSimplePasswordError = (
1304
1543
  );
1305
1544
 
1306
1545
  /**
1307
- * `Id` {@link Type}.
1308
- *
1309
- * Represents a unique identifier with exactly 21 characters, using NanoID's
1310
- * standard format (`A-Za-z0-9_-`).
1546
+ * Globally unique identifier.
1547
+ *
1548
+ * **Evolu Id** is 16 random bytes from a cryptographically secure random
1549
+ * generator, encoded as 22-character Base64Url string. This provides strong
1550
+ * collision resistance for distributed ID generation.
1551
+ *
1552
+ * ### Design Rationale
1553
+ *
1554
+ * Why Evolu Id over alternatives:
1555
+ *
1556
+ * - **NanoID**: No standard binary serialization format, and uses only ~126 bits
1557
+ * of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
1558
+ * 128 bits.
1559
+ * - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
1560
+ * Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
1561
+ * standard string representation is verbose.
1562
+ * - **UUID v7**: Includes timestamp in the ID, which leaks information about when
1563
+ * data was created. This is a privacy concern for local-first applications
1564
+ * where creation time must remain private.
1565
+ *
1566
+ * Evolu Id provides 128 bits of entropy, compact string representation (22
1567
+ * characters), standard and native string serialization (Base64Url), and no
1568
+ * privacy leaks.
1569
+ *
1570
+ * ### Future Consideration
1571
+ *
1572
+ * For database-heavy workloads where insert performance is critical, a hybrid
1573
+ * approach could be considered: `timestamp ^ H(cluster_id, timestamp >> N)`
1574
+ * where H is a keyed hash function and N is a configurable parameter. This
1575
+ * would maintain spatial locality for database caches (improving insert
1576
+ * performance by an order of magnitude) while adding entropy to prevent
1577
+ * timestamp leakage and correlation across systems. The parameter N would allow
1578
+ * trading off cache locality (larger N = better locality) versus entropy
1579
+ * distribution. See https://brooker.co.za/blog/2025/10/22/uuidv7.html for
1580
+ * details on this approach.
1311
1581
  *
1312
1582
  * @category String
1313
1583
  */
1314
- export const Id = regex("Id", /^[A-Za-z0-9_-]{21}$/)(String);
1584
+ export const Id = brand("Id", String, (value) =>
1585
+ value.length === 22 && Base64Url.fromParent(value).ok
1586
+ ? ok(value)
1587
+ : err<IdError>({ type: "Id", value }),
1588
+ );
1315
1589
  export type Id = typeof Id.Type;
1316
1590
 
1317
- export const idTypeValueLength = 21;
1591
+ export interface IdError extends TypeError<"Id"> {}
1592
+
1593
+ export const formatIdError = createTypeErrorFormatter<IdError>(
1594
+ (error) => `The value ${error.value} is not a valid Id.`,
1595
+ );
1318
1596
 
1319
1597
  /**
1320
1598
  * Creates an {@link Id}.
@@ -1324,13 +1602,65 @@ export const idTypeValueLength = 21;
1324
1602
  * ```ts
1325
1603
  * // string & Brand<"Id">
1326
1604
  * const id = createId(deps);
1605
+ *
1606
+ * // string & Brand<"Id"> & Brand<"Todo">
1607
+ * const todoId = createId<"Todo">(deps);
1327
1608
  * ```
1328
1609
  */
1329
- export const createId = (deps: NanoIdLibDep): Id =>
1330
- deps.nanoIdLib.nanoid() as Id;
1610
+ export const createId = <B extends string = never>(
1611
+ deps: RandomBytesDep,
1612
+ ): [B] extends [never] ? Id : Id & Brand<B> =>
1613
+ uint8ArrayToBase64Url(deps.randomBytes.create(16)) as unknown as [B] extends [
1614
+ never,
1615
+ ]
1616
+ ? Id
1617
+ : Id & Brand<B>;
1331
1618
 
1332
1619
  /**
1333
- * Type Factory to create branded {@link Id} Type for a specific table.
1620
+ * Creates an {@link Id} from a string using SHA-256.
1621
+ *
1622
+ * When integrating with external systems that use different ID formats, use
1623
+ * this function to convert external IDs into valid Evolu IDs.
1624
+ *
1625
+ * In Evolu's CRDT, the ID serves as the unique identifier for conflict
1626
+ * resolution across distributed clients. When multiple clients create records
1627
+ * with the same external identifier, they must resolve to the same Evolu ID to
1628
+ * ensure data consistency.
1629
+ *
1630
+ * ### Example
1631
+ *
1632
+ * ```ts
1633
+ * // Both clients will generate the same ID
1634
+ * const id1 = createIdFromString("user-api-123");
1635
+ * const id2 = createIdFromString("user-api-123");
1636
+ * console.log(id1 === id2); // true
1637
+ *
1638
+ * upsert("todo", {
1639
+ * id: createIdFromString("external-todo-456"),
1640
+ * title: "Synced from external system",
1641
+ * });
1642
+ * ```
1643
+ *
1644
+ * **Important**: This transformation is one-way. We cannot recover the original
1645
+ * external string from the generated {@link Id}. If we need to preserve the
1646
+ * original external ID, store it in a separate column.
1647
+ *
1648
+ * @category String
1649
+ */
1650
+ export const createIdFromString = <B extends string = never>(
1651
+ value: string,
1652
+ ): [B] extends [never] ? Id : Id & Brand<B> => {
1653
+ const hash = sha256(utf8ToBytes(value));
1654
+ // Take first 16 bytes of hash and convert to Id
1655
+ const id = idBytesToId(hash.slice(0, 16) as IdBytes);
1656
+
1657
+ return id as [B] extends [never] ? Id : Id & Brand<B>;
1658
+ };
1659
+
1660
+ /**
1661
+ * Creates a branded {@link Id} Type for a table's primary key.
1662
+ *
1663
+ * The table name becomes an additional brand for type safety.
1334
1664
  *
1335
1665
  * ### Example
1336
1666
  *
@@ -1342,55 +1672,64 @@ export const createId = (deps: NanoIdLibDep): Id =>
1342
1672
  *
1343
1673
  * @category String
1344
1674
  */
1345
- export const id = <Table extends TypeName>(table: Table): IdType<Table> => {
1675
+ export const id = <Table extends TypeName>(table: Table): TableId<Table> => {
1676
+ const fromUnknown = (value: unknown) => {
1677
+ const parentResult = String.fromUnknown(value);
1678
+ if (!parentResult.ok) return parentResult;
1679
+ return fromParent(parentResult.value);
1680
+ };
1681
+
1346
1682
  const fromParent = (value: string) => {
1347
1683
  const idResult = Id.fromParent(value);
1348
1684
  if (!idResult.ok) {
1349
- return err<IdError<Table>>({ type: "Id", value, table });
1685
+ return err<TableIdError<Table>>({ type: "TableId", value, table });
1350
1686
  }
1351
1687
  return ok(idResult.value as Id & Brand<Table>);
1352
1688
  };
1353
1689
 
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
1690
  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
- }),
1691
+ ...createType("Id", { fromUnknown, fromParent }),
1367
1692
  table,
1368
1693
  };
1369
1694
  };
1370
1695
 
1371
- export interface IdType<Table extends TypeName>
1696
+ export interface TableId<Table extends TypeName>
1372
1697
  extends Type<
1373
1698
  "Id",
1374
1699
  string & Brand<"Id"> & Brand<Table>,
1375
1700
  string,
1376
- IdError<Table>,
1701
+ TableIdError<Table>,
1377
1702
  string,
1378
1703
  StringError
1379
1704
  > {
1380
1705
  table: Table;
1381
1706
  }
1382
1707
 
1383
- export interface IdError<Table extends TypeName = TypeName>
1384
- extends TypeError<"Id"> {
1708
+ export interface TableIdError<Table extends TypeName = TypeName>
1709
+ extends TypeError<"TableId"> {
1385
1710
  readonly table: Table;
1386
1711
  }
1387
1712
 
1388
- export const formatIdError = createTypeErrorFormatter<IdError>(
1389
- (error) => `Invalid ${error.type} table Id: ${error.value}`,
1713
+ export const formatTableIdError = createTypeErrorFormatter<TableIdError>(
1714
+ (error) => `Invalid Id for table ${error.table}: ${error.value}.`,
1390
1715
  );
1391
1716
 
1717
+ /** Binary representation of an {@link Id}. */
1718
+ export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
1719
+ export type IdBytes = typeof IdBytes.Type;
1720
+
1721
+ export const idBytesTypeValueLength = 16 as NonNegativeInt;
1722
+
1723
+ export const idToIdBytes = (id: Id): IdBytes =>
1724
+ // Id is Base64Url (validated by Id.from), cast is safe
1725
+ base64UrlToUint8Array(id as unknown as Base64Url) as IdBytes;
1726
+
1727
+ export const idBytesToId = (idBytes: IdBytes): Id =>
1728
+ // Base64Url encoding of 16 bytes always produces valid Id (22 chars)
1729
+ uint8ArrayToBase64Url(idBytes) as unknown as Id;
1730
+
1392
1731
  /**
1393
- * Positive number.
1732
+ * Positive number (> 0).
1394
1733
  *
1395
1734
  * ### Example
1396
1735
  *
@@ -1413,11 +1752,11 @@ export const positive: BrandFactory<"Positive", number, PositiveError> = (
1413
1752
  export interface PositiveError extends TypeError<"Positive"> {}
1414
1753
 
1415
1754
  export const formatPositiveError = createTypeErrorFormatter<PositiveError>(
1416
- (error) => `The value ${error.value} is not positive.`,
1755
+ (error) => `The value ${error.value} must be positive (> 0).`,
1417
1756
  );
1418
1757
 
1419
1758
  /**
1420
- * Negative number.
1759
+ * Negative number (< 0).
1421
1760
  *
1422
1761
  * ### Example
1423
1762
  *
@@ -1437,11 +1776,11 @@ export const negative: BrandFactory<"Negative", number, NegativeError> = (
1437
1776
  export interface NegativeError extends TypeError<"Negative"> {}
1438
1777
 
1439
1778
  export const formatNegativeError = createTypeErrorFormatter<NegativeError>(
1440
- (error) => `The value ${error.value} is not negative.`,
1779
+ (error) => `The value ${error.value} must be negative (< 0).`,
1441
1780
  );
1442
1781
 
1443
1782
  /**
1444
- * Non-positive number.
1783
+ * Non-positive number (≤ 0).
1445
1784
  *
1446
1785
  * ### Example
1447
1786
  *
@@ -1466,11 +1805,11 @@ export interface NonPositiveError extends TypeError<"NonPositive"> {}
1466
1805
 
1467
1806
  export const formatNonPositiveError =
1468
1807
  createTypeErrorFormatter<NonPositiveError>(
1469
- (error) => `The value ${error.value} is not non-positive.`,
1808
+ (error) => `The value ${error.value} must be non-positive (≤ 0).`,
1470
1809
  );
1471
1810
 
1472
1811
  /**
1473
- * Non-negative number.
1812
+ * Non-negative number (≥ 0).
1474
1813
  *
1475
1814
  * ### Example
1476
1815
  *
@@ -1495,22 +1834,38 @@ export interface NonNegativeError extends TypeError<"NonNegative"> {}
1495
1834
 
1496
1835
  export const formatNonNegativeError =
1497
1836
  createTypeErrorFormatter<NonNegativeError>(
1498
- (error) => `The value ${error.value} is not non-negative.`,
1837
+ (error) => `The value ${error.value} must be non-negative (≥ 0).`,
1499
1838
  );
1500
1839
 
1501
- /** @category Number */
1840
+ /**
1841
+ * Non-negative number (≥ 0).
1842
+ *
1843
+ * @category Number
1844
+ */
1502
1845
  export const NonNegativeNumber = nonNegative(Number);
1503
1846
  export type NonNegativeNumber = typeof NonNegativeNumber.Type;
1504
1847
 
1505
- /** @category Number */
1848
+ /**
1849
+ * Positive number (> 0).
1850
+ *
1851
+ * @category Number
1852
+ */
1506
1853
  export const PositiveNumber = positive(NonNegativeNumber);
1507
1854
  export type PositiveNumber = typeof PositiveNumber.Type;
1508
1855
 
1509
- /** @category Number */
1856
+ /**
1857
+ * Non-positive number (≤ 0).
1858
+ *
1859
+ * @category Number
1860
+ */
1510
1861
  export const NonPositiveNumber = nonPositive(Number);
1511
1862
  export type NonPositiveNumber = typeof NonPositiveNumber.Type;
1512
1863
 
1513
- /** @category Number */
1864
+ /**
1865
+ * Negative number (< 0).
1866
+ *
1867
+ * @category Number
1868
+ */
1514
1869
  export const NegativeNumber = negative(NonPositiveNumber);
1515
1870
  export type NegativeNumber = typeof NegativeNumber.Type;
1516
1871
 
@@ -1535,7 +1890,7 @@ export const int: BrandFactory<"Int", number, IntError> = (parent) =>
1535
1890
  export interface IntError extends TypeError<"Int"> {}
1536
1891
 
1537
1892
  export const formatIntError = createTypeErrorFormatter<IntError>(
1538
- (error) => `The value ${error.value} is not an integer.`,
1893
+ (error) => `The value ${error.value} must be an integer.`,
1539
1894
  );
1540
1895
 
1541
1896
  /**
@@ -1546,19 +1901,40 @@ export const formatIntError = createTypeErrorFormatter<IntError>(
1546
1901
  export const Int = int(Number);
1547
1902
  export type Int = typeof Int.Type;
1548
1903
 
1549
- /** @category Number */
1904
+ /**
1905
+ * Non-negative integer (≥ 0).
1906
+ *
1907
+ * @category Number
1908
+ */
1550
1909
  export const NonNegativeInt = nonNegative(Int);
1551
1910
  export type NonNegativeInt = typeof NonNegativeInt.Type;
1552
1911
 
1553
- /** @category Number */
1912
+ /**
1913
+ * Positive integer (> 0).
1914
+ *
1915
+ * @category Number
1916
+ */
1554
1917
  export const PositiveInt = positive(NonNegativeInt);
1555
1918
  export type PositiveInt = typeof PositiveInt.Type;
1556
1919
 
1557
- /** @category Number */
1920
+ /** Maximum safe positive integer value for practically infinite operations. */
1921
+ export const maxPositiveInt = PositiveInt.orThrow(
1922
+ globalThis.Number.MAX_SAFE_INTEGER,
1923
+ );
1924
+
1925
+ /**
1926
+ * Non-positive integer (≤ 0).
1927
+ *
1928
+ * @category Number
1929
+ */
1558
1930
  export const NonPositiveInt = nonPositive(Int);
1559
1931
  export type NonPositiveInt = typeof NonPositiveInt.Type;
1560
1932
 
1561
- /** @category Number */
1933
+ /**
1934
+ * Negative integer (< 0).
1935
+ *
1936
+ * @category Number
1937
+ */
1562
1938
  export const NegativeInt = negative(NonPositiveInt);
1563
1939
  export type NegativeInt = typeof NegativeInt.Type;
1564
1940
 
@@ -1676,7 +2052,7 @@ export const nonNaN: BrandFactory<"NonNaN", number, NonNaNError> = (parent) =>
1676
2052
  export interface NonNaNError extends TypeError<"NonNaN"> {}
1677
2053
 
1678
2054
  export const formatNonNaNError = createTypeErrorFormatter<NonNaNError>(
1679
- (error) => `The value ${error.value} is NaN (not a number).`,
2055
+ () => `The value must not be NaN.`,
1680
2056
  );
1681
2057
 
1682
2058
  /** @category Number */
@@ -1698,7 +2074,7 @@ export const finite: BrandFactory<"Finite", number, FiniteError> = (parent) =>
1698
2074
  export interface FiniteError extends TypeError<"Finite"> {}
1699
2075
 
1700
2076
  export const formatFiniteError = createTypeErrorFormatter<FiniteError>(
1701
- (error) => `The value ${error.value} is not finite.`,
2077
+ (error) => `The value ${error.value} must be finite.`,
1702
2078
  );
1703
2079
 
1704
2080
  /**
@@ -1775,198 +2151,57 @@ export interface BetweenError<
1775
2151
 
1776
2152
  export const formatBetweenError = createTypeErrorFormatter<BetweenError>(
1777
2153
  (error) =>
1778
- `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
1779
- );
1780
-
1781
- /** @category Number */
1782
- export const Between1And10 = between(1, 10)(Number);
1783
- export type Between1And10 = typeof Between1And10.Type;
1784
-
1785
- /**
1786
- * Literal {@link Type}.
1787
- *
1788
- * https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
1789
- *
1790
- * ### Example
1791
- *
1792
- * ```ts
1793
- * const LiteralHello = literal("Hello");
1794
- * const result = LiteralHello.from("Hello"); // ok("Hello")
1795
- * const errorResult = LiteralHello.from("World"); // err
1796
- * ```
1797
- *
1798
- * TODO: Add JsonValue
1799
- *
1800
- * @category Base Factories
1801
- */
1802
- export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
1803
- const fromUnknown = (value: unknown): Result<T, LiteralError<T>> =>
1804
- value === expected
1805
- ? ok(expected)
1806
- : err<LiteralError<T>>({ type: "Literal", value, expected });
1807
-
1808
- return {
1809
- ...createType("Literal", {
1810
- fromUnknown,
1811
- to: identity as (value: T) => WidenLiteral<T>,
1812
- fromParent: ok,
1813
- toParent: identity,
1814
- }),
1815
- expected,
1816
- };
1817
- };
1818
-
1819
- export interface LiteralType<T extends Literal>
1820
- extends Type<"Literal", T, WidenLiteral<T>, LiteralError<T>> {
1821
- expected: T;
1822
- }
1823
-
1824
- export interface LiteralError<T extends Literal = Literal>
1825
- extends TypeError<"Literal"> {
1826
- readonly expected: T;
1827
- }
1828
-
1829
- export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
1830
- (error) =>
1831
- `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(
1832
- error.expected,
1833
- )}.`,
1834
- );
1835
-
1836
- /**
1837
- * {@link Type} that transforms values between `FromType` and `ToType`.
1838
- *
1839
- * - `fromParent`: Converts `FromType` to `ToType`, may fail.
1840
- * - `toParent`: Converts `ToType` back to `FromType`, must not fail.
1841
- *
1842
- * ### Example
1843
- *
1844
- * // TODO: Examples
1845
- *
1846
- * @category Base Factories
1847
- */
1848
- export const transform = <
1849
- FromType extends AnyType,
1850
- ToType extends AnyType,
1851
- TransformError extends TypeError = never,
1852
- >(
1853
- fromType: FromType,
1854
- toType: ToType,
1855
- fromParent: (
1856
- parentValue: InferType<FromType>,
1857
- ) => Result<InferType<ToType>, TransformError>,
1858
- toParent: (value: InferType<ToType>) => InferType<FromType>,
1859
- ): TransformType<FromType, ToType, TransformError> => {
1860
- const fromUnknown = (
1861
- value: unknown,
1862
- ): Result<InferType<ToType>, InferErrors<FromType> | TransformError> => {
1863
- const parentResult = fromType.fromUnknown(value);
1864
- if (!parentResult.ok) return parentResult;
1865
- return fromParent(parentResult.value as IntentionalNever);
1866
- };
1867
-
1868
- const to = (value: InferType<ToType>): InferInput<FromType> =>
1869
- fromType.to(toParent(value)) as IntentionalNever;
1870
-
1871
- return {
1872
- ...createType("Transform", {
1873
- fromUnknown,
1874
- to,
1875
- fromParent,
1876
- toParent,
1877
- }),
1878
- is: toType.is,
1879
- fromType,
1880
- toType,
1881
- } as TransformType<FromType, ToType, TransformError>;
1882
- };
1883
-
1884
- /**
1885
- * TransformType extends {@link Type} with additional `fromType` and `toType`
1886
- * properties for reflection.
1887
- */
1888
- export interface TransformType<
1889
- FromType extends AnyType,
1890
- ToType extends AnyType,
1891
- TransformError extends TypeError = never,
1892
- > extends Type<
1893
- "Transform",
1894
- InferType<ToType>,
1895
- InferInput<FromType>,
1896
- TransformError,
1897
- InferType<FromType>,
1898
- InferErrors<FromType>
1899
- > {
1900
- readonly fromType: FromType;
1901
- readonly toType: ToType;
1902
-
1903
- readonly fromParent: (
1904
- value: InferType<FromType>,
1905
- ) => [TransformError] extends [never]
1906
- ? Ok<InferType<ToType>>
1907
- : Result<InferType<ToType>, TransformError>;
1908
- }
1909
-
1910
- /**
1911
- * Trims leading and trailing whitespace from a string.
1912
- *
1913
- * ### Example
1914
- *
1915
- * ```ts
1916
- * expect(TrimString.from("a ")).toEqual(ok("a"));
1917
- * expect(TrimString.fromParent("a ").value).toEqual("a");
1918
- * ```
1919
- *
1920
- * @category String
1921
- */
1922
- export const TrimString = trim(String);
1923
-
1924
- /**
1925
- * Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
1926
- *
1927
- * ### Example
1928
- *
1929
- * TODO:
1930
- *
1931
- * @category String
1932
- */
1933
- export const DateIso = transform(
1934
- Date,
1935
- DateIsoString,
1936
- (value) => DateIsoString.fromParent(value.toISOString()),
1937
- (value) => new globalThis.Date(value),
2154
+ `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
1938
2155
  );
1939
2156
 
1940
2157
  /**
1941
- * Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
2158
+ * Literal {@link Type}.
2159
+ *
2160
+ * https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
1942
2161
  *
1943
2162
  * ### Example
1944
2163
  *
1945
2164
  * ```ts
1946
- * NumberFromString.from("42"); // ok(42)
1947
- * NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
2165
+ * const LiteralHello = literal("Hello");
2166
+ * const result = LiteralHello.from("Hello"); // ok("Hello")
2167
+ * const errorResult = LiteralHello.from("World"); // err
1948
2168
  * ```
1949
2169
  *
1950
- * @category Number
2170
+ * TODO: Add JsonValue
2171
+ *
2172
+ * @category Base Factories
1951
2173
  */
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
- );
2174
+ export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
2175
+ const fromUnknown = (value: unknown): Result<T, LiteralError<T>> =>
2176
+ value === expected
2177
+ ? ok(expected)
2178
+ : err<LiteralError<T>>({ type: "Literal", value, expected });
2179
+
2180
+ return {
2181
+ ...createType("Literal", {
2182
+ fromUnknown,
2183
+ fromParent: ok,
2184
+ }),
2185
+ expected,
2186
+ } as LiteralType<T>;
2187
+ };
1963
2188
 
1964
- export interface NumberFromStringError extends TypeError<"NumberFromString"> {}
2189
+ export interface LiteralType<T extends Literal>
2190
+ extends Type<"Literal", T, WidenLiteral<T>, LiteralError<T>> {
2191
+ expected: T;
2192
+ }
1965
2193
 
1966
- export const formatNumberFromStringError =
1967
- createTypeErrorFormatter<NumberFromStringError>((error) => {
1968
- return `The value ${error.value} could not be converted to a finite number.`;
1969
- });
2194
+ export interface LiteralError<T extends Literal = Literal>
2195
+ extends TypeError<"Literal"> {
2196
+ readonly expected: T;
2197
+ }
2198
+
2199
+ export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
2200
+ (error) =>
2201
+ `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(
2202
+ error.expected,
2203
+ )}.`,
2204
+ );
1970
2205
 
1971
2206
  /**
1972
2207
  * Array of a specific {@link Type}.
@@ -2020,9 +2255,6 @@ export const array = <ElementType extends AnyType>(
2020
2255
  return ok(result);
2021
2256
  };
2022
2257
 
2023
- const to = (value: ReadonlyArray<InferType<ElementType>>) =>
2024
- value.map(element.to) as ReadonlyArray<InferInput<ElementType>>;
2025
-
2026
2258
  const fromParent = (
2027
2259
  value: ReadonlyArray<InferParent<ElementType>>,
2028
2260
  ): Result<
@@ -2048,18 +2280,10 @@ export const array = <ElementType extends AnyType>(
2048
2280
  return ok(result);
2049
2281
  };
2050
2282
 
2051
- const toParent = (values: ReadonlyArray<InferType<ElementType>>) =>
2052
- values.map(element.toParent) as ReadonlyArray<InferParent<ElementType>>;
2053
-
2054
2283
  return {
2055
- ...createType("Array", {
2056
- fromUnknown,
2057
- to,
2058
- fromParent,
2059
- toParent,
2060
- }),
2284
+ ...createType("Array", { fromUnknown, fromParent }),
2061
2285
  element,
2062
- } as ArrayType<ElementType>;
2286
+ };
2063
2287
  };
2064
2288
 
2065
2289
  /** ArrayType extends Type with an additional `element` property for reflection. */
@@ -2192,16 +2416,6 @@ export const record = <
2192
2416
  return ok(result);
2193
2417
  };
2194
2418
 
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
2419
  const fromParent = (
2206
2420
  value: Readonly<Record<KeyParent, InferParent<Value>>>,
2207
2421
  ): Result<
@@ -2246,22 +2460,10 @@ export const record = <
2246
2460
  return ok(result);
2247
2461
  };
2248
2462
 
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
2463
  return {
2260
2464
  ...createType("Record", {
2261
2465
  fromUnknown,
2262
- to,
2263
2466
  fromParent,
2264
- toParent,
2265
2467
  }),
2266
2468
  key: keyType,
2267
2469
  value: valueType,
@@ -2561,25 +2763,6 @@ export function object(
2561
2763
  return ok(result);
2562
2764
  };
2563
2765
 
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
2766
  const fromParent = (value: any): Result<any, any> => {
2584
2767
  const errors: Record<string, any> = {};
2585
2768
  const result: Record<string, any> = {};
@@ -2631,31 +2814,10 @@ export function object(
2631
2814
  return ok(result);
2632
2815
  };
2633
2816
 
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
2817
  return {
2654
2818
  ...createType(record ? "ObjectWithRecord" : "Object", {
2655
2819
  fromUnknown,
2656
- to,
2657
2820
  fromParent,
2658
- toParent,
2659
2821
  }),
2660
2822
  props,
2661
2823
  ...(record ? { record } : {}),
@@ -2897,7 +3059,6 @@ export function union(...args: ReadonlyArray<any>): any {
2897
3059
  * cases.
2898
3060
  */
2899
3061
 
2900
- /* eslint-disable */
2901
3062
  const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
2902
3063
 
2903
3064
  const fromUnknown = (value: unknown) => {
@@ -2915,23 +3076,13 @@ export function union(...args: ReadonlyArray<any>): any {
2915
3076
  });
2916
3077
  };
2917
3078
 
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
3079
  return {
2926
3080
  ...createType("Union", {
2927
3081
  fromUnknown,
2928
- to,
2929
3082
  fromParent: fromUnknown,
2930
- toParent: to,
2931
3083
  }),
2932
3084
  members,
2933
3085
  };
2934
- /* eslint-enable */
2935
3086
  }
2936
3087
 
2937
3088
  /**
@@ -3025,20 +3176,10 @@ export const recursive = <ParentType extends AnyType>(
3025
3176
  type ??= create();
3026
3177
  return type.fromUnknown(value);
3027
3178
  },
3028
- to: (value: T) => {
3029
- type ??= create();
3030
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3031
- return type.to(value);
3032
- },
3033
3179
  fromParent: (value: Parent) => {
3034
3180
  type ??= create();
3035
3181
  return type.fromParent(value);
3036
3182
  },
3037
- toParent: (value: T) => {
3038
- type ??= create();
3039
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
3040
- return type.toParent(value);
3041
- },
3042
3183
  is: (value: unknown): value is T => {
3043
3184
  type ??= create();
3044
3185
  return type.is(value);
@@ -3176,9 +3317,6 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
3176
3317
  return ok(result);
3177
3318
  };
3178
3319
 
3179
- const to = (value: { [K in keyof Elements]: InferType<Elements[K]> }) =>
3180
- value.map((val, index) => elements[index].to(val) as IntentionalNever);
3181
-
3182
3320
  const fromParent = (value: {
3183
3321
  [K in keyof Elements]: InferParent<Elements[K]>;
3184
3322
  }) => {
@@ -3210,21 +3348,10 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
3210
3348
  return ok(result as { [K in keyof Elements]: InferType<Elements[K]> });
3211
3349
  };
3212
3350
 
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
3351
  return {
3223
3352
  ...createType("Tuple", {
3224
3353
  fromUnknown,
3225
- to,
3226
3354
  fromParent,
3227
- toParent,
3228
3355
  }),
3229
3356
  elements,
3230
3357
  } as IntentionalNever;
@@ -3300,37 +3427,41 @@ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
3300
3427
  `The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
3301
3428
  );
3302
3429
 
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
- );
3430
+ // // co s timhle? je to string, ze ktereho lze udelat bigint
3313
3431
 
3314
- export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
3432
+ // export const BigIntFromString = transform(
3433
+ // String,
3434
+ // BigInt,
3435
+ // (value) =>
3436
+ // trySync(
3437
+ // () => globalThis.BigInt(value),
3438
+ // (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
3439
+ // ),
3440
+ // (value) => value.toString(),
3441
+ // );
3315
3442
 
3316
- export const formatBigIntFromStringError =
3317
- createTypeErrorFormatter<BigIntFromStringError>(
3318
- (error) => `The value ${error.value} could not be converted to a BigInt.`,
3319
- );
3443
+ // export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
3444
+
3445
+ // export const formatBigIntFromStringError =
3446
+ // createTypeErrorFormatter<BigIntFromStringError>(
3447
+ // (error) => `The value ${error.value} could not be converted to a BigInt.`,
3448
+ // );
3320
3449
 
3321
3450
  /**
3322
3451
  * Stringified {@link Int64}.
3323
3452
  *
3324
- * @category Number
3453
+ * @category String
3325
3454
  */
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
- });
3455
+ export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) =>
3456
+ trySync(
3457
+ () => {
3458
+ const maybeInt = globalThis.BigInt(value);
3459
+ Int64.orThrow(maybeInt);
3460
+ return value;
3461
+ },
3462
+ (): Int64StringError => ({ type: "Int64String", value }),
3463
+ ),
3464
+ );
3334
3465
 
3335
3466
  export type Int64String = typeof Int64String.Type;
3336
3467
 
@@ -3435,42 +3566,14 @@ export const JsonArray = array(JsonValue);
3435
3566
  */
3436
3567
  export const JsonObject = record(String, JsonValue);
3437
3568
 
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}`,
3569
+ export const parseJson = (value: string): Result<JsonValue, JsonError> =>
3570
+ trySync(
3571
+ () => JSON.parse(value) as JsonValue,
3572
+ (error): JsonError => ({
3573
+ type: `Json`,
3574
+ value,
3575
+ message: globalThis.String(error),
3576
+ }),
3474
3577
  );
3475
3578
 
3476
3579
  /**
@@ -3479,20 +3582,15 @@ export const formatJsonValueFromStringError =
3479
3582
  * ### Example
3480
3583
  *
3481
3584
  * ```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" })
3585
+ * const result = Json.from('{"key":"value"}'); // ok
3586
+ * const error = Json.from("invalid json"); // err
3484
3587
  * ```
3485
3588
  *
3486
3589
  * @category String
3487
3590
  */
3488
3591
  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
- });
3592
+ const result = parseJson(value);
3593
+ if (!result.ok) return result;
3496
3594
  return ok(value);
3497
3595
  });
3498
3596
 
@@ -3506,6 +3604,80 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
3506
3604
  (error) => `Invalid JSON: ${error.value}. Error: ${error.message}`,
3507
3605
  );
3508
3606
 
3607
+ export const jsonValueToJson = (value: JsonValue): Json =>
3608
+ JSON.stringify(value) as Json;
3609
+
3610
+ export const jsonToJsonValue = (value: Json): JsonValue =>
3611
+ JSON.parse(value) as JsonValue;
3612
+
3613
+ /**
3614
+ * Creates a branded JSON string {@link Type} and type-safe conversion functions
3615
+ * for a given Type.
3616
+ *
3617
+ * This factory creates:
3618
+ *
3619
+ * 1. A branded string Type that validates JSON parsing and structural conformity
3620
+ * 2. A serialization function (Type → branded JSON string)
3621
+ * 3. A parsing function (branded JSON string → Type, skipping validation)
3622
+ *
3623
+ * Optimized for Evolu's SQLite workflow where we store typed JSON strings and
3624
+ * need type-safe conversions without double parsing.
3625
+ *
3626
+ * ### Example
3627
+ *
3628
+ * ```ts
3629
+ * const Person = object({
3630
+ * name: NonEmptyString100,
3631
+ * age: FiniteNumber,
3632
+ * });
3633
+ * type Person = typeof Person.Type;
3634
+ *
3635
+ * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
3636
+ * Person,
3637
+ * "PersonJson",
3638
+ * );
3639
+ * // string & Brand<"PersonJson">
3640
+ * type PersonJson = typeof PersonJson.Type;
3641
+ *
3642
+ * // Usage:
3643
+ * const person: Person = { name: "Alice", age: 30 };
3644
+ * const jsonString = personToPersonJson(person); // PersonJson
3645
+ * const backToPerson = personJsonToPerson(jsonString); // Person
3646
+ * ```
3647
+ */
3648
+ export const json = <T extends AnyType, Name extends TypeName>(
3649
+ type: T,
3650
+ name: Name,
3651
+ ): [
3652
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>,
3653
+ (
3654
+ value: InferType<T>,
3655
+ ) => InferType<
3656
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
3657
+ >,
3658
+ (
3659
+ value: InferType<
3660
+ BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
3661
+ >,
3662
+ ) => InferType<T>,
3663
+ ] => {
3664
+ const BrandedJsonType = brand(name, String, (value) => {
3665
+ const parseResult = parseJson(value);
3666
+ if (!parseResult.ok) return parseResult;
3667
+
3668
+ const validationResult = type.fromUnknown(parseResult.value);
3669
+ if (!validationResult.ok) return validationResult;
3670
+
3671
+ return ok(value);
3672
+ }) as BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>;
3673
+
3674
+ return [
3675
+ BrandedJsonType,
3676
+ jsonValueToJson as IntentionalNever,
3677
+ jsonToJsonValue as IntentionalNever,
3678
+ ];
3679
+ };
3680
+
3509
3681
  /**
3510
3682
  * Optional {@link Type}.
3511
3683
  *
@@ -3530,9 +3702,7 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
3530
3702
  export const optional = <T extends AnyType>(type: T): OptionalType<T> => ({
3531
3703
  ...createType("Optional", {
3532
3704
  fromUnknown: type.fromUnknown,
3533
- to: type.to,
3534
3705
  fromParent: type.fromParent,
3535
- toParent: type.toParent,
3536
3706
  }),
3537
3707
  parent: type,
3538
3708
  });
@@ -3556,7 +3726,7 @@ export const isOptionalType = (x: unknown): x is OptionalType<any> =>
3556
3726
  /**
3557
3727
  * Creates a partial object type where all properties are optional.
3558
3728
  *
3559
- * This is useful when you want to validate an object in which none of the keys
3729
+ * This is useful when we want to validate an object in which none of the keys
3560
3730
  * are required, but if they are present they must conform to their
3561
3731
  * corresponding Types.
3562
3732
  *
@@ -3661,68 +3831,39 @@ export function omit<T extends ObjectType<any>, Keys extends keyof T["props"]>(
3661
3831
  return object(newProps);
3662
3832
  }
3663
3833
 
3834
+ export const maxMutationSize = 655360;
3835
+
3664
3836
  /**
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
- * ```
3837
+ * Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
3838
+ * the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
3839
+ * via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
3840
+ * optimizations.
3692
3841
  */
3693
- export const json = <T extends AnyType, Name extends TypeName>(
3842
+ export const validMutationSize = <T extends AnyType>(
3694
3843
  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>;
3844
+ ): BrandType<T, "ValidMutationSize", ValidMutationSizeError, InferErrors<T>> =>
3845
+ brand("ValidMutationSize", type, (value) =>
3846
+ pack(value).byteLength <= maxMutationSize
3847
+ ? ok(value)
3848
+ : err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
3849
+ );
3714
3850
 
3715
- return ok(value as InferType<typeof BrandedJsonString>);
3716
- });
3851
+ export interface ValidMutationSizeError
3852
+ extends TypeError<"ValidMutationSize"> {}
3717
3853
 
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>,
3854
+ export const formatValidMutationSizeError =
3855
+ createTypeErrorFormatter<ValidMutationSizeError>(
3856
+ (error) =>
3857
+ `The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
3724
3858
  );
3725
- };
3859
+
3860
+ export type ValidMutationSize<Props extends Record<string, AnyType>> =
3861
+ BrandType<
3862
+ ObjectType<Props>,
3863
+ "ValidMutationSize",
3864
+ ValidMutationSizeError,
3865
+ InferErrors<ObjectType<Props>>
3866
+ >;
3726
3867
 
3727
3868
  /**
3728
3869
  * Union of all `TypeError`s defined in the `Type.ts` file, including base type
@@ -3749,16 +3890,16 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3749
3890
  | InstanceOfError
3750
3891
  | EvoluTypeError
3751
3892
  | CurrencyCodeError
3752
- | DateIsoStringError
3893
+ | DateIsoError
3753
3894
  | TrimmedError
3754
3895
  | MinLengthError
3755
3896
  | MaxLengthError
3756
3897
  | LengthError
3757
3898
  | MnemonicError
3758
3899
  | RegexError
3759
- | NanoIdError
3760
3900
  | SimplePasswordError
3761
3901
  | IdError
3902
+ | TableIdError
3762
3903
  | PositiveError
3763
3904
  | NegativeError
3764
3905
  | NonPositiveError
@@ -3774,10 +3915,9 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3774
3915
  | BetweenError
3775
3916
  | LiteralError
3776
3917
  | Int64Error
3777
- | BigIntFromStringError
3778
3918
  | Int64StringError
3779
- | JsonValueFromStringError
3780
3919
  | JsonError
3920
+ | ValidMutationSizeError
3781
3921
  | ExtraErrors
3782
3922
  // Composite errors
3783
3923
  | ArrayError<TypeErrors<ExtraErrors>>
@@ -3792,70 +3932,74 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
3792
3932
  | TupleError<TypeErrors<ExtraErrors>>;
3793
3933
 
3794
3934
  /**
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.
3935
+ * Formats Evolu Type errors into user-friendly messages.
3798
3936
  *
3799
- * If you prefer not to reuse any built-in error formatters, you can write your
3800
- * own `formatTypeError` function from scratch.
3937
+ * Evolu Type typed errors ensure every error type must have a formatter.
3938
+ * TypeScript enforces this at compile-time, preventing unhandled validation
3939
+ * errors from reaching users.
3801
3940
  *
3802
- * ### Examples
3941
+ * The `createFormatTypeError` function handles both built-in {@link TypeErrors}
3942
+ * and custom errors, and lets us override default formatting for specific
3943
+ * errors.
3944
+ *
3945
+ * ### Example
3803
3946
  *
3804
3947
  * ```ts
3805
- * const formatError = createFormatTypeError();
3806
- * console.log(formatError({ type: "String", value: 42 }));
3807
- * // "A value 42 is not a string."
3948
+ * const formatTypeError = createFormatTypeError<
3949
+ * MinLengthError | MaxLengthError
3950
+ * >((error): string => {
3951
+ * switch (error.type) {
3952
+ * case "MinLength":
3953
+ * return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
3954
+ * case "MaxLength":
3955
+ * return `Text is too long (maximum ${error.max} characters)`;
3956
+ * }
3957
+ * });
3808
3958
  * ```
3809
3959
  *
3810
- * A custom `formatTypeError` function:
3960
+ * Alternatively, write a custom formatter from scratch without using
3961
+ * `createFormatTypeError`. This gives us full control over error formatting:
3811
3962
  *
3812
3963
  * ```ts
3813
- * type AppErrors =
3814
- * | ValidMutationSizeError
3964
+ * const Person = object({
3965
+ * name: NonEmptyTrimmedString100,
3966
+ * age: optional(PositiveInt),
3967
+ * });
3968
+ *
3969
+ * // Define only the errors actually used by Person Type
3970
+ * type PersonErrors =
3815
3971
  * | StringError
3816
- * | MinLengthError
3817
3972
  * | MaxLengthError
3818
- * | NullError
3819
- * | IdError
3973
+ * | MinLengthError
3820
3974
  * | 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.
3975
+ * | PositiveError
3976
+ * | NonNegativeError
3977
+ * | IntError
3978
+ * | NumberError
3979
+ * | ObjectError<Record<string, PersonErrors>>;
3980
+ *
3981
+ * const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
3830
3982
  * 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
3983
  * case "String":
3842
- * // We can reuse existing formatter.
3843
3984
  * return formatStringError(error);
3985
+ * case "Number":
3986
+ * return "Must be a number";
3987
+ * case "MinLength":
3988
+ * return `Must be at least ${error.min} characters`;
3989
+ * case "MaxLength":
3990
+ * return `Cannot exceed ${error.max} characters`;
3844
3991
  * 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(", ")}`;
3992
+ * return "Cannot have leading or trailing spaces";
3993
+ * case "Positive":
3994
+ * return "Must be a positive number";
3995
+ * case "NonNegative":
3996
+ * return "Must be zero or positive";
3997
+ * case "Int":
3998
+ * return "Must be an integer";
3853
3999
  * 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.";
4000
+ * if (error.reason.kind === "NotObject") return "Must be an object";
4001
+ * if (error.reason.kind === "ExtraKeys")
4002
+ * return "Contains unexpected fields";
3859
4003
  * const firstError = Object.values(error.reason.errors).find(
3860
4004
  * (e) => e !== undefined,
3861
4005
  * )!;
@@ -3902,8 +4046,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3902
4046
  return formatIsTypeError(error);
3903
4047
  case "CurrencyCode":
3904
4048
  return formatCurrencyCodeError(error);
3905
- case "DateIsoString":
3906
- return formatDateIsoStringError(error);
4049
+ case "DateIso":
4050
+ return formatDateIsoError(error);
3907
4051
  case "Trimmed":
3908
4052
  return formatTrimmedError(error);
3909
4053
  case "MinLength":
@@ -3918,6 +4062,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3918
4062
  return formatRegexError(error);
3919
4063
  case "Id":
3920
4064
  return formatIdError(error);
4065
+ case "TableId":
4066
+ return formatTableIdError(error);
3921
4067
  case "Positive":
3922
4068
  return formatPositiveError(error);
3923
4069
  case "Negative":
@@ -3948,14 +4094,12 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3948
4094
  return formatLiteralError(error);
3949
4095
  case "Int64":
3950
4096
  return formatInt64Error(error);
3951
- case "BigIntFromString":
3952
- return formatBigIntFromStringError(error);
3953
4097
  case "Int64String":
3954
4098
  return formatInt64StringError(error);
3955
- case "JsonValueFromString":
3956
- return formatJsonValueFromStringError(error);
3957
4099
  case "Json":
3958
4100
  return formatJsonError(error);
4101
+ case "ValidMutationSize":
4102
+ return formatValidMutationSizeError(error);
3959
4103
  // Composite Types
3960
4104
  case "SimplePassword":
3961
4105
  return formatSimplePasswordError(formatTypeError)(error);
@@ -3971,8 +4115,220 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
3971
4115
  return formatUnionError(formatTypeError)(error);
3972
4116
  case "Tuple":
3973
4117
  return formatTupleError(formatTypeError)(error);
4118
+ default: {
4119
+ // Fallback for unknown error types
4120
+ const unknownError = error as TypeError;
4121
+ return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
4122
+ }
3974
4123
  }
3975
4124
  };
3976
4125
 
3977
4126
  return formatTypeError;
3978
4127
  };
4128
+
4129
+ /**
4130
+ * Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
4131
+ *
4132
+ * This function recursively converts Evolu's typed errors into the Standard
4133
+ * Schema issue format with proper path tracking for nested structures.
4134
+ *
4135
+ * @category Utilities
4136
+ */
4137
+ export const typeErrorToStandardSchemaIssues = <
4138
+ ExtraErrors extends TypeError = never,
4139
+ >(
4140
+ error: TypeErrors<ExtraErrors>,
4141
+ formatTypeError: TypeErrorFormatter<TypeErrors<ExtraErrors>>,
4142
+ path: ReadonlyArray<PropertyKey> = [],
4143
+ ): ReadonlyArray<StandardSchemaV1.Issue> => {
4144
+ if (error.type === "Array") {
4145
+ const arrayError = error as ArrayError;
4146
+ if (arrayError.reason.kind === "NotArray") {
4147
+ return [{ message: formatTypeError(error), path }];
4148
+ }
4149
+ return typeErrorToStandardSchemaIssues(
4150
+ arrayError.reason.error as TypeErrors<ExtraErrors>,
4151
+ formatTypeError,
4152
+ [...path, arrayError.reason.index],
4153
+ );
4154
+ }
4155
+
4156
+ if (error.type === "Object") {
4157
+ const objectError = error as ObjectError;
4158
+ if (
4159
+ objectError.reason.kind === "NotObject" ||
4160
+ objectError.reason.kind === "ExtraKeys"
4161
+ ) {
4162
+ return [{ message: formatTypeError(error), path }];
4163
+ }
4164
+ const issues: Array<StandardSchemaV1.Issue> = [];
4165
+ for (const [key, propError] of Object.entries(objectError.reason.errors)) {
4166
+ issues.push(
4167
+ ...typeErrorToStandardSchemaIssues(
4168
+ propError as TypeErrors<ExtraErrors>,
4169
+ formatTypeError,
4170
+ [...path, key],
4171
+ ),
4172
+ );
4173
+ }
4174
+ return issues;
4175
+ }
4176
+
4177
+ if (error.type === "ObjectWithRecord") {
4178
+ const objectWithRecordError = error as ObjectWithRecordError;
4179
+ if (objectWithRecordError.reason.kind === "NotObject") {
4180
+ return [{ message: formatTypeError(error), path }];
4181
+ }
4182
+ if (
4183
+ objectWithRecordError.reason.kind === "IndexKey" ||
4184
+ objectWithRecordError.reason.kind === "IndexValue"
4185
+ ) {
4186
+ return typeErrorToStandardSchemaIssues(
4187
+ objectWithRecordError.reason.error as TypeErrors<ExtraErrors>,
4188
+ formatTypeError,
4189
+ [...path, objectWithRecordError.reason.key as PropertyKey],
4190
+ );
4191
+ }
4192
+ const issues: Array<StandardSchemaV1.Issue> = [];
4193
+ for (const [key, propError] of Object.entries(
4194
+ objectWithRecordError.reason.errors,
4195
+ )) {
4196
+ issues.push(
4197
+ ...typeErrorToStandardSchemaIssues(
4198
+ propError as TypeErrors<ExtraErrors>,
4199
+ formatTypeError,
4200
+ [...path, key],
4201
+ ),
4202
+ );
4203
+ }
4204
+ return issues;
4205
+ }
4206
+
4207
+ if (error.type === "Record") {
4208
+ const recordError = error as RecordError;
4209
+ if (recordError.reason.kind === "NotRecord") {
4210
+ return [{ message: formatTypeError(error), path }];
4211
+ }
4212
+ return typeErrorToStandardSchemaIssues(
4213
+ recordError.reason.error as TypeErrors<ExtraErrors>,
4214
+ formatTypeError,
4215
+ [...path, recordError.reason.key as PropertyKey],
4216
+ );
4217
+ }
4218
+
4219
+ if (error.type === "Tuple") {
4220
+ const tupleError = error as TupleError;
4221
+ if (tupleError.reason.kind === "InvalidLength") {
4222
+ return [{ message: formatTypeError(error), path }];
4223
+ }
4224
+ return typeErrorToStandardSchemaIssues(
4225
+ tupleError.reason.error as TypeErrors<ExtraErrors>,
4226
+ formatTypeError,
4227
+ [...path, tupleError.reason.index],
4228
+ );
4229
+ }
4230
+
4231
+ if (error.type === "Union") {
4232
+ const unionError = error as UnionError;
4233
+ return unionError.errors.flatMap((err) =>
4234
+ typeErrorToStandardSchemaIssues(
4235
+ err as TypeErrors<ExtraErrors>,
4236
+ formatTypeError,
4237
+ path,
4238
+ ),
4239
+ );
4240
+ }
4241
+
4242
+ if (error.type === "Brand") {
4243
+ const brandError = error as BrandWithoutRefineError<TypeName, TypeError>;
4244
+ if ("parentError" in brandError) {
4245
+ return typeErrorToStandardSchemaIssues(
4246
+ brandError.parentError as TypeErrors<ExtraErrors>,
4247
+ formatTypeError,
4248
+ path,
4249
+ );
4250
+ }
4251
+ return [{ message: formatTypeError(error), path }];
4252
+ }
4253
+
4254
+ return [{ message: formatTypeError(error), path }];
4255
+ };
4256
+
4257
+ /** The Standard Schema interface. */
4258
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
4259
+ /** The Standard Schema properties. */
4260
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
4261
+ }
4262
+
4263
+ // eslint-disable-next-line @typescript-eslint/no-namespace
4264
+ export declare namespace StandardSchemaV1 {
4265
+ /** The Standard Schema properties interface. */
4266
+ export interface Props<Input = unknown, Output = Input> {
4267
+ /** The version number of the standard. */
4268
+ readonly version: 1;
4269
+ /** The vendor name of the schema library. */
4270
+ readonly vendor: string;
4271
+ /** Validates unknown input values. */
4272
+ readonly validate: (
4273
+ value: unknown,
4274
+ ) => Result<Output> | Promise<Result<Output>>;
4275
+ /** Inferred types associated with the schema. */
4276
+ readonly types?: Types<Input, Output> | undefined;
4277
+ }
4278
+
4279
+ /** The result interface of the validate function. */
4280
+ export type Result<Output> = SuccessResult<Output> | FailureResult;
4281
+
4282
+ /** The result interface if validation succeeds. */
4283
+ export interface SuccessResult<Output> {
4284
+ /** The typed output value. */
4285
+ readonly value: Output;
4286
+ /** The non-existent issues. */
4287
+ readonly issues?: undefined;
4288
+ }
4289
+
4290
+ /** The result interface if validation fails. */
4291
+ export interface FailureResult {
4292
+ /** The issues of failed validation. */
4293
+ readonly issues: ReadonlyArray<Issue>;
4294
+ }
4295
+
4296
+ /** The issue interface of the failure output. */
4297
+ export interface Issue {
4298
+ /** The error message of the issue. */
4299
+ readonly message: string;
4300
+ /** The path of the issue, if any. */
4301
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
4302
+ }
4303
+
4304
+ /** The path segment interface of the issue. */
4305
+ export interface PathSegment {
4306
+ /** The key representing a path segment. */
4307
+ readonly key: PropertyKey;
4308
+ }
4309
+
4310
+ /** The Standard Schema types interface. */
4311
+ export interface Types<Input = unknown, Output = Input> {
4312
+ /** The input type of the schema. */
4313
+ readonly input: Input;
4314
+ /** The output type of the schema. */
4315
+ readonly output: Output;
4316
+ }
4317
+
4318
+ /** Infers the input type of a Standard Schema. */
4319
+ export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
4320
+ Schema["~standard"]["types"]
4321
+ >["input"];
4322
+
4323
+ /** Infers the output type of a Standard Schema. */
4324
+ export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
4325
+ Schema["~standard"]["types"]
4326
+ >["output"];
4327
+ }
4328
+
4329
+ /**
4330
+ * Shared formatter cache for Standard Schema integration - avoids circular
4331
+ * dependency by lazily creating the formatter on first use rather than during
4332
+ * module initialization.
4333
+ */
4334
+ let cachedStandardSchemaFormatTypeError: TypeErrorFormatter<any> | undefined;