@evolu/common 8.8.0 → 8.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/dist/src/Assert.d.ts +21 -1
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +31 -27
  4. package/dist/src/Bytes.d.ts +647 -0
  5. package/dist/src/Bytes.d.ts.map +1 -0
  6. package/dist/src/{Binary.js → Bytes.js} +266 -16
  7. package/dist/src/Config.d.ts +142 -0
  8. package/dist/src/Config.d.ts.map +1 -0
  9. package/dist/src/Config.js +181 -0
  10. package/dist/src/Fs.d.ts +302 -0
  11. package/dist/src/Fs.d.ts.map +1 -0
  12. package/dist/src/Fs.js +111 -0
  13. package/dist/src/Number.d.ts +50 -7
  14. package/dist/src/Number.d.ts.map +1 -1
  15. package/dist/src/Number.js +47 -8
  16. package/dist/src/Object.d.ts +32 -0
  17. package/dist/src/Object.d.ts.map +1 -1
  18. package/dist/src/Object.js +41 -0
  19. package/dist/src/Polyfills.d.ts +6 -0
  20. package/dist/src/Polyfills.d.ts.map +1 -1
  21. package/dist/src/Polyfills.js +6 -0
  22. package/dist/src/Sqlite.js +1 -1
  23. package/dist/src/Time.d.ts +74 -12
  24. package/dist/src/Time.d.ts.map +1 -1
  25. package/dist/src/Time.js +40 -2
  26. package/dist/src/Type.d.ts +1451 -79
  27. package/dist/src/Type.d.ts.map +1 -1
  28. package/dist/src/Type.js +1303 -96
  29. package/dist/src/index.d.ts +3 -1
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/index.js +3 -1
  32. package/dist/src/intl/_en.d.ts +24 -1
  33. package/dist/src/intl/_en.d.ts.map +1 -1
  34. package/dist/src/intl/_en.js +20 -0
  35. package/dist/src/intl/ar.d.ts +24 -1
  36. package/dist/src/intl/ar.d.ts.map +1 -1
  37. package/dist/src/intl/ar.js +20 -0
  38. package/dist/src/intl/bn.d.ts +24 -1
  39. package/dist/src/intl/bn.d.ts.map +1 -1
  40. package/dist/src/intl/bn.js +20 -0
  41. package/dist/src/intl/ca.d.ts +24 -1
  42. package/dist/src/intl/ca.d.ts.map +1 -1
  43. package/dist/src/intl/ca.js +20 -0
  44. package/dist/src/intl/cs.d.ts +24 -1
  45. package/dist/src/intl/cs.d.ts.map +1 -1
  46. package/dist/src/intl/cs.js +20 -0
  47. package/dist/src/intl/da.d.ts +24 -1
  48. package/dist/src/intl/da.d.ts.map +1 -1
  49. package/dist/src/intl/da.js +20 -0
  50. package/dist/src/intl/de.d.ts +24 -1
  51. package/dist/src/intl/de.d.ts.map +1 -1
  52. package/dist/src/intl/de.js +20 -0
  53. package/dist/src/intl/el.d.ts +24 -1
  54. package/dist/src/intl/el.d.ts.map +1 -1
  55. package/dist/src/intl/el.js +20 -0
  56. package/dist/src/intl/es.d.ts +24 -1
  57. package/dist/src/intl/es.d.ts.map +1 -1
  58. package/dist/src/intl/es.js +20 -0
  59. package/dist/src/intl/fa.d.ts +24 -1
  60. package/dist/src/intl/fa.d.ts.map +1 -1
  61. package/dist/src/intl/fa.js +20 -0
  62. package/dist/src/intl/fi.d.ts +24 -1
  63. package/dist/src/intl/fi.d.ts.map +1 -1
  64. package/dist/src/intl/fi.js +20 -0
  65. package/dist/src/intl/fil.d.ts +24 -1
  66. package/dist/src/intl/fil.d.ts.map +1 -1
  67. package/dist/src/intl/fil.js +20 -0
  68. package/dist/src/intl/fr.d.ts +24 -1
  69. package/dist/src/intl/fr.d.ts.map +1 -1
  70. package/dist/src/intl/fr.js +20 -0
  71. package/dist/src/intl/he.d.ts +24 -1
  72. package/dist/src/intl/he.d.ts.map +1 -1
  73. package/dist/src/intl/he.js +20 -0
  74. package/dist/src/intl/hi.d.ts +24 -1
  75. package/dist/src/intl/hi.d.ts.map +1 -1
  76. package/dist/src/intl/hi.js +20 -0
  77. package/dist/src/intl/hr.d.ts +24 -1
  78. package/dist/src/intl/hr.d.ts.map +1 -1
  79. package/dist/src/intl/hr.js +20 -0
  80. package/dist/src/intl/hu.d.ts +22 -1
  81. package/dist/src/intl/hu.d.ts.map +1 -1
  82. package/dist/src/intl/hu.js +18 -0
  83. package/dist/src/intl/id.d.ts +24 -1
  84. package/dist/src/intl/id.d.ts.map +1 -1
  85. package/dist/src/intl/id.js +20 -0
  86. package/dist/src/intl/it.d.ts +24 -1
  87. package/dist/src/intl/it.d.ts.map +1 -1
  88. package/dist/src/intl/it.js +20 -0
  89. package/dist/src/intl/ja.d.ts +24 -1
  90. package/dist/src/intl/ja.d.ts.map +1 -1
  91. package/dist/src/intl/ja.js +20 -0
  92. package/dist/src/intl/ko.d.ts +24 -1
  93. package/dist/src/intl/ko.d.ts.map +1 -1
  94. package/dist/src/intl/ko.js +20 -0
  95. package/dist/src/intl/ml.d.ts +24 -1
  96. package/dist/src/intl/ml.d.ts.map +1 -1
  97. package/dist/src/intl/ml.js +20 -0
  98. package/dist/src/intl/mr.d.ts +24 -1
  99. package/dist/src/intl/mr.d.ts.map +1 -1
  100. package/dist/src/intl/mr.js +20 -0
  101. package/dist/src/intl/ms.d.ts +24 -1
  102. package/dist/src/intl/ms.d.ts.map +1 -1
  103. package/dist/src/intl/ms.js +20 -0
  104. package/dist/src/intl/nb.d.ts +22 -1
  105. package/dist/src/intl/nb.d.ts.map +1 -1
  106. package/dist/src/intl/nb.js +18 -0
  107. package/dist/src/intl/nl.d.ts +24 -1
  108. package/dist/src/intl/nl.d.ts.map +1 -1
  109. package/dist/src/intl/nl.js +20 -0
  110. package/dist/src/intl/pa.d.ts +24 -1
  111. package/dist/src/intl/pa.d.ts.map +1 -1
  112. package/dist/src/intl/pa.js +20 -0
  113. package/dist/src/intl/pl.d.ts +23 -0
  114. package/dist/src/intl/pl.d.ts.map +1 -1
  115. package/dist/src/intl/pl.js +20 -0
  116. package/dist/src/intl/pt-BR.d.ts +24 -1
  117. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  118. package/dist/src/intl/pt-BR.js +20 -0
  119. package/dist/src/intl/pt.d.ts +24 -1
  120. package/dist/src/intl/pt.d.ts.map +1 -1
  121. package/dist/src/intl/pt.js +20 -0
  122. package/dist/src/intl/ro.d.ts +24 -1
  123. package/dist/src/intl/ro.d.ts.map +1 -1
  124. package/dist/src/intl/ro.js +20 -0
  125. package/dist/src/intl/sk.d.ts +24 -1
  126. package/dist/src/intl/sk.d.ts.map +1 -1
  127. package/dist/src/intl/sk.js +20 -0
  128. package/dist/src/intl/sl.d.ts +24 -1
  129. package/dist/src/intl/sl.d.ts.map +1 -1
  130. package/dist/src/intl/sl.js +20 -0
  131. package/dist/src/intl/sv.d.ts +24 -1
  132. package/dist/src/intl/sv.d.ts.map +1 -1
  133. package/dist/src/intl/sv.js +20 -0
  134. package/dist/src/intl/sw.d.ts +21 -0
  135. package/dist/src/intl/sw.d.ts.map +1 -1
  136. package/dist/src/intl/sw.js +18 -0
  137. package/dist/src/intl/ta.d.ts +24 -1
  138. package/dist/src/intl/ta.d.ts.map +1 -1
  139. package/dist/src/intl/ta.js +20 -0
  140. package/dist/src/intl/te.d.ts +24 -1
  141. package/dist/src/intl/te.d.ts.map +1 -1
  142. package/dist/src/intl/te.js +20 -0
  143. package/dist/src/intl/th.d.ts +24 -1
  144. package/dist/src/intl/th.d.ts.map +1 -1
  145. package/dist/src/intl/th.js +20 -0
  146. package/dist/src/intl/tr.d.ts +24 -1
  147. package/dist/src/intl/tr.d.ts.map +1 -1
  148. package/dist/src/intl/tr.js +20 -0
  149. package/dist/src/intl/uk.d.ts +80 -57
  150. package/dist/src/intl/uk.d.ts.map +1 -1
  151. package/dist/src/intl/uk.js +174 -149
  152. package/dist/src/intl/ur.d.ts +24 -1
  153. package/dist/src/intl/ur.d.ts.map +1 -1
  154. package/dist/src/intl/ur.js +20 -0
  155. package/dist/src/intl/vi.d.ts +24 -1
  156. package/dist/src/intl/vi.d.ts.map +1 -1
  157. package/dist/src/intl/vi.js +20 -0
  158. package/dist/src/intl/zh-CN.d.ts +24 -1
  159. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  160. package/dist/src/intl/zh-CN.js +20 -0
  161. package/dist/src/intl/zh-TW.d.ts +24 -1
  162. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  163. package/dist/src/intl/zh-TW.js +20 -0
  164. package/dist/src/local-first/Protocol.d.ts +1 -1
  165. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  166. package/dist/src/local-first/Protocol.js +1 -1
  167. package/dist/src/local-first/Schema.d.ts +10 -0
  168. package/dist/src/local-first/Schema.d.ts.map +1 -1
  169. package/dist/src/local-first/Storage.js +1 -1
  170. package/dist/src/local-first/Timestamp.js +1 -1
  171. package/package.json +1 -1
  172. package/src/Assert.test.ts +51 -0
  173. package/src/Assert.ts +56 -4
  174. package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
  175. package/src/{Binary.ts → Bytes.ts} +652 -21
  176. package/src/Config.test.ts +672 -0
  177. package/src/Config.ts +410 -0
  178. package/src/Fs.test.ts +105 -0
  179. package/src/Fs.ts +414 -0
  180. package/src/Number.test.ts +82 -18
  181. package/src/Number.ts +76 -8
  182. package/src/Object.test.ts +115 -1
  183. package/src/Object.ts +44 -0
  184. package/src/Polyfills.ts +6 -0
  185. package/src/Sqlite.ts +1 -1
  186. package/src/Time.test.ts +82 -11
  187. package/src/Time.ts +141 -16
  188. package/src/Type.test.ts +3114 -157
  189. package/src/Type.ts +2553 -196
  190. package/src/index.ts +3 -1
  191. package/src/intl/_en.ts +70 -0
  192. package/src/intl/ar.ts +71 -0
  193. package/src/intl/bn.ts +70 -0
  194. package/src/intl/ca.ts +70 -0
  195. package/src/intl/cs.ts +70 -0
  196. package/src/intl/da.ts +70 -0
  197. package/src/intl/de.ts +70 -0
  198. package/src/intl/el.ts +70 -0
  199. package/src/intl/es.ts +70 -0
  200. package/src/intl/fa.ts +70 -0
  201. package/src/intl/fi.ts +70 -0
  202. package/src/intl/fil.ts +70 -0
  203. package/src/intl/fr.ts +70 -0
  204. package/src/intl/he.ts +70 -0
  205. package/src/intl/hi.ts +70 -0
  206. package/src/intl/hr.ts +70 -0
  207. package/src/intl/hu.ts +69 -0
  208. package/src/intl/id.ts +70 -0
  209. package/src/intl/intl.test.ts +819 -1
  210. package/src/intl/it.ts +70 -0
  211. package/src/intl/ja.ts +70 -0
  212. package/src/intl/ko.ts +68 -0
  213. package/src/intl/ml.ts +70 -0
  214. package/src/intl/mr.ts +70 -0
  215. package/src/intl/ms.ts +71 -0
  216. package/src/intl/nb.ts +69 -0
  217. package/src/intl/nl.ts +70 -0
  218. package/src/intl/pa.ts +70 -0
  219. package/src/intl/pl.ts +63 -0
  220. package/src/intl/pt-BR.ts +70 -0
  221. package/src/intl/pt.ts +71 -0
  222. package/src/intl/ro.ts +70 -0
  223. package/src/intl/sk.ts +71 -0
  224. package/src/intl/sl.ts +70 -0
  225. package/src/intl/sv.ts +70 -0
  226. package/src/intl/sw.ts +62 -0
  227. package/src/intl/ta.ts +70 -0
  228. package/src/intl/te.ts +70 -0
  229. package/src/intl/th.ts +68 -0
  230. package/src/intl/tr.ts +70 -0
  231. package/src/intl/uk.ts +228 -155
  232. package/src/intl/ur.ts +70 -0
  233. package/src/intl/vi.ts +70 -0
  234. package/src/intl/zh-CN.ts +68 -0
  235. package/src/intl/zh-TW.ts +68 -0
  236. package/src/local-first/Protocol.test.ts +1 -1
  237. package/src/local-first/Protocol.ts +1 -1
  238. package/src/local-first/Schema.ts +11 -0
  239. package/src/local-first/Storage.ts +1 -1
  240. package/src/local-first/Timestamp.ts +1 -1
  241. package/dist/src/Binary.d.ts +0 -254
  242. package/dist/src/Binary.d.ts.map +0 -1
package/src/Type.ts CHANGED
@@ -341,9 +341,9 @@
341
341
  * parameter type. If application code claims an accessor-backed object or an
342
342
  * object with excess properties is an Object Output, the assertion throws
343
343
  * because the application contract is broken. `orThrow` and `orNull` preserve
344
- * the assertion at their typed `Input` boundary, then apply {@link getOrThrow}
345
- * or {@link getOrNull} only to validation failures returned by the remaining
346
- * pipeline.
344
+ * the assertion at their typed `Input` boundary. After that boundary, `orThrow`
345
+ * throws returned validation errors with a formatted message and the original
346
+ * error as `cause`, while `orNull` maps them to `null`.
347
347
  *
348
348
  * Consequently, structural representation errors such as sparse Arrays,
349
349
  * accessors, and excess properties normally do not enter user-facing validation
@@ -461,17 +461,23 @@ import {
461
461
  import { assert, assertNonNullable } from "./Assert.ts";
462
462
  import type { Brand } from "./Brand.ts";
463
463
  import type { RandomBytesDep } from "./Crypto.ts";
464
+ import { eqData } from "./Eq.ts";
464
465
  import { identity, type Thunk } from "./Function.ts";
465
- import { createMutableRecord, getObjectKind, isPlainObject } from "./Object.ts";
466
+ import {
467
+ createMutableRecord,
468
+ getObjectKind,
469
+ isPlainObject,
470
+ type ReadonlyRecord,
471
+ } from "./Object.ts";
466
472
  import { hasNodeBuffer } from "./Platform.ts";
467
473
  import {
468
474
  err,
469
475
  flatMapResult,
470
476
  getOk,
471
477
  getOrNull,
472
- getOrThrow,
473
478
  ok,
474
479
  trySync,
480
+ type getOrThrow,
475
481
  type Result,
476
482
  } from "./Result.ts";
477
483
  import { safelyStringifyUnknownValue } from "./String.ts";
@@ -767,15 +773,22 @@ export interface Type<
767
773
  : (value: Output) => CanonicalInput;
768
774
 
769
775
  /**
770
- * Shorthand for calling {@link getOrThrow} with the result of the deepest
771
- * `from` operation, which accepts this Type's `Input`.
776
+ * Runs the deepest `from` operation, which accepts this Type's `Input`, and
777
+ * returns its decoded value or throws an Error.
778
+ *
779
+ * Returned validation errors use this Type's {@link Type.formatError} as the
780
+ * Error message and preserve the original validation error as `cause`. With
781
+ * `{ errors: "all" }`, the cause retains all collected errors; the message
782
+ * still follows `formatError`, which describes the first issue.
772
783
  *
773
784
  * The typed `Input` boundary is asserted before the remaining pipeline runs.
774
- * A boundary violation is a bug, so it throws directly; `getOrThrow` maps
775
- * only a validation error returned after that boundary.
785
+ * A boundary violation is a bug, so it throws directly with its assertion
786
+ * message instead of formatting a returned validation error.
776
787
  *
777
788
  * `Type.orThrow.parent(value)` does not exist. To throw after starting from a
778
- * typed boundary, call `getOrThrow` with the corresponding `from` operation.
789
+ * typed boundary, call {@link getOrThrow} with the corresponding `from`
790
+ * operation. Its generic Error message differs from this operation's
791
+ * message.
779
792
  *
780
793
  * Use `orThrow` for startup and configuration, module constants, test
781
794
  * fixtures, and internal invariants where failure must stop the current flow.
@@ -787,20 +800,31 @@ export interface Type<
787
800
  * ```ts
788
801
  * import {
789
802
  * assertEqual,
790
- * getOrThrow,
803
+ * assertInstanceOf,
804
+ * assertErr,
791
805
  * minLength,
792
806
  * String,
807
+ * trySync,
793
808
  * } from "@evolu/common";
794
809
  *
795
810
  * const NonEmptyString = minLength(1)(String);
796
811
  *
797
812
  * const value = NonEmptyString.orThrow("Evolu");
798
813
  *
799
- * // Equivalent because `from.parent` is this Type's deepest `from` operation:
800
- * const sameValue = getOrThrow(NonEmptyString.from.parent("Evolu"));
801
- *
802
814
  * assertEqual(value, "Evolu");
803
- * assertEqual(sameValue, value);
815
+ *
816
+ * const failed = trySync(() => NonEmptyString.orThrow(""));
817
+ * assertErr(failed);
818
+ * assertInstanceOf(failed.error, Error);
819
+ * assertEqual(
820
+ * failed.error.message,
821
+ * 'The value "" does not meet the minimum length of 1.',
822
+ * );
823
+ * assertEqual(failed.error.cause, {
824
+ * type: "MinLength1",
825
+ * value: "",
826
+ * min: 1,
827
+ * });
804
828
  * ```
805
829
  */
806
830
  readonly orThrow: TypeOperationFn<"orThrow", Input, Output, never>;
@@ -900,6 +924,61 @@ export type TypeErrorFormatter<Error extends TypeError> = (
900
924
  error: Error,
901
925
  ) => string;
902
926
 
927
+ /**
928
+ * A formatted validation issue located by its path from the root value.
929
+ *
930
+ * @group Core
931
+ */
932
+ export interface TypeIssue {
933
+ readonly path: ReadonlyArray<PropertyKey>;
934
+ readonly message: string;
935
+ }
936
+
937
+ /**
938
+ * Converts an error from a {@link Type} into formatted issues with paths.
939
+ *
940
+ * Pass the Type that produced the error. This uses its nested and localized
941
+ * formatters without validating the input again. To retain every issue, decode
942
+ * with `{ errors: "all" }`; this function cannot recover errors omitted during
943
+ * validation. A root issue has an empty path.
944
+ *
945
+ * ### Example
946
+ *
947
+ * ```ts
948
+ * import {
949
+ * assertEqual,
950
+ * assertErr,
951
+ * IntFromString,
952
+ * object,
953
+ * typeErrorToIssues,
954
+ * } from "@evolu/common";
955
+ *
956
+ * const Settings = object({ port: IntFromString });
957
+ *
958
+ * const result = Settings.fromUnknown({ port: "http" }, { errors: "all" });
959
+ * assertErr(result);
960
+ *
961
+ * assertEqual(typeErrorToIssues(Settings, result.error), [
962
+ * {
963
+ * path: ["port"],
964
+ * message: 'The value "http" is not a decimal integer.',
965
+ * },
966
+ * ]);
967
+ * ```
968
+ *
969
+ * @group Core
970
+ */
971
+ export const typeErrorToIssues = <T extends TypeNode>(
972
+ type: T,
973
+ error: InferErrors<NoInfer<T>>,
974
+ ): NonEmptyReadonlyArray<TypeIssue> => {
975
+ const runtimeType = type as unknown as RuntimeTypeNode;
976
+ return runtimeType[getRuntimeTypeIssuesSymbol](error, "all").map((issue) => ({
977
+ path: issue.path,
978
+ message: formatRuntimeTypeIssue(issue),
979
+ })) as unknown as NonEmptyReadonlyArray<TypeIssue>;
980
+ };
981
+
903
982
  /**
904
983
  * The common structural shape of every {@link Type}, with its specific type
905
984
  * parameters erased.
@@ -950,6 +1029,11 @@ interface RuntimeTypeIssue {
950
1029
  readonly error: TypeError;
951
1030
  readonly path: ReadonlyArray<PropertyKey>;
952
1031
  readonly formatError: TypeErrorFormatter<TypeError>;
1032
+ readonly alternatives?: ReadonlyArray<{
1033
+ readonly index: number;
1034
+ readonly name: TypeName;
1035
+ readonly issues: NonEmptyReadonlyArray<RuntimeTypeIssue>;
1036
+ }>;
953
1037
  }
954
1038
 
955
1039
  type RuntimeGetTypeIssues = (
@@ -1033,6 +1117,34 @@ const createCollectionRuntimeTypeIssues =
1033
1117
  const formatDefaultRuntimeTypeIssue: RuntimeFormatTypeIssue = (issue) =>
1034
1118
  issue.formatError(issue.error);
1035
1119
 
1120
+ const formatRuntimeTypeIssue = (
1121
+ issue: RuntimeTypeIssue,
1122
+ formatIssue: RuntimeFormatTypeIssue = formatDefaultRuntimeTypeIssue,
1123
+ ): string => {
1124
+ const summary = formatIssue(issue);
1125
+ if (issue.alternatives === undefined) return summary;
1126
+
1127
+ return [
1128
+ summary,
1129
+ ...issue.alternatives.flatMap(({ index, name, issues }) =>
1130
+ issues.map((issue) => {
1131
+ const path = issue.path
1132
+ .map((key) =>
1133
+ typeof key === "string"
1134
+ ? `[${JSON.stringify(key)}]`
1135
+ : `[${globalThis.String(key)}]`,
1136
+ )
1137
+ .join("");
1138
+ const message = formatRuntimeTypeIssue(issue, formatIssue).replaceAll(
1139
+ "\n",
1140
+ "\n ",
1141
+ );
1142
+ return `- ${index}: ${name}${path}: ${message}`;
1143
+ }),
1144
+ ),
1145
+ ].join("\n");
1146
+ };
1147
+
1036
1148
  /**
1037
1149
  * Asserts type equality or validates a value with a {@link Type}.
1038
1150
  *
@@ -1134,6 +1246,11 @@ const assertTypeOutput = <Error extends TypeError>(
1134
1246
  * that produced them. Different localized Type sets can coexist in separate
1135
1247
  * application or dependency-injection scopes.
1136
1248
  *
1249
+ * A Union formatter supplies the summary of the failure. Retained member
1250
+ * failures are appended using their own localized formatters, with member
1251
+ * indexes, Type names, and paths identifying each alternative. The Union still
1252
+ * produces one issue at its enclosing path.
1253
+ *
1137
1254
  * Localization is scoped to the selected Types instead of a package-wide
1138
1255
  * translation registry. Static imports give bundlers an explicit dependency
1139
1256
  * graph, so unrelated Types, locales, and formatters can be removed. Bundling
@@ -1319,11 +1436,24 @@ const withFormatError = (
1319
1436
  localizedTypeBySource,
1320
1437
  )
1321
1438
  : null;
1439
+ const localizeIssue = (issue: RuntimeTypeIssue): RuntimeTypeIssue => ({
1440
+ ...issue,
1441
+ formatError: () => formatIssue(issue),
1442
+ ...(issue.alternatives === undefined
1443
+ ? {}
1444
+ : {
1445
+ alternatives: issue.alternatives.map((alternative) => ({
1446
+ ...alternative,
1447
+ issues: alternative.issues.map(
1448
+ localizeIssue,
1449
+ ) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>,
1450
+ })),
1451
+ }),
1452
+ });
1322
1453
  const getTypeIssues: RuntimeGetTypeIssues = (error, mode) =>
1323
- source[getRuntimeTypeIssuesSymbol](error, mode).map((issue) => ({
1324
- ...issue,
1325
- formatError: () => formatIssue(issue),
1326
- })) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>;
1454
+ source[getRuntimeTypeIssuesSymbol](error, mode).map(
1455
+ localizeIssue,
1456
+ ) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>;
1327
1457
  const derived = createTypeNode<RuntimeTypeNode>(
1328
1458
  source.name,
1329
1459
  parent,
@@ -1342,7 +1472,10 @@ const withFormatError = (
1342
1472
  // boundary. Private operation chains on derived still support composition.
1343
1473
  globalThis.Object.assign(derived, {
1344
1474
  from: source.from,
1345
- orThrow: source.orThrow,
1475
+ orThrow: createRuntimeOrThrow(
1476
+ getTerminalRuntimeNode(source.from),
1477
+ derived.formatError,
1478
+ ),
1346
1479
  orNull: source.orNull,
1347
1480
  });
1348
1481
 
@@ -1396,11 +1529,12 @@ const localizeTypeReflection = (
1396
1529
  ) as Record<string | symbol, unknown>;
1397
1530
 
1398
1531
  for (const key of Reflect.ownKeys(value)) {
1399
- localized[key] = localizeTypeReflection(
1400
- (value as Record<string | symbol, unknown>)[key],
1401
- formatIssue,
1402
- localizedTypeBySource,
1403
- );
1532
+ const property = (value as Record<string | symbol, unknown>)[key];
1533
+ // Configured defaults are opaque data whose identity must be preserved.
1534
+ localized[key] =
1535
+ key === "value" && globalThis.Object.hasOwn(value, defaultPropertySymbol)
1536
+ ? property
1537
+ : localizeTypeReflection(property, formatIssue, localizedTypeBySource);
1404
1538
  }
1405
1539
 
1406
1540
  return localized;
@@ -1678,7 +1812,8 @@ type ReflectedTypes<T extends TypeNode> =
1678
1812
  readonly props: infer Props extends ObjectProps;
1679
1813
  readonly record?: infer Rest;
1680
1814
  }
1681
- ? ObjectPropertyType<Props[keyof Props]> | Extract<Rest, TypeNode>
1815
+ ? | DefaultableObjectPropertyType<Props[keyof Props]>
1816
+ | Extract<Rest, TypeNode>
1682
1817
  : never;
1683
1818
 
1684
1819
  type RootType<T extends TypeNode> = T extends {
@@ -1845,7 +1980,42 @@ type ValidateParent<T extends ConcreteTypeNode> =
1845
1980
  "Parent must be one concrete Type node. Pass a Union Type node instead of a union of Type nodes."
1846
1981
  >;
1847
1982
 
1848
- type ValidateOutput<T extends ConcreteTypeNode> =
1983
+ /**
1984
+ * Requires one concrete output {@link Type} node in a factory parameter.
1985
+ *
1986
+ * Rejects TypeScript unions of Type nodes. A {@link union} Type is one concrete
1987
+ * node and remains valid. Use an intersection with the inferred parameter type
1988
+ * to preserve its specific Type. This guard performs no runtime validation.
1989
+ *
1990
+ * ### Example
1991
+ *
1992
+ * ```ts
1993
+ * import {
1994
+ * assertSame,
1995
+ * Number,
1996
+ * String,
1997
+ * union,
1998
+ * type AnyType,
1999
+ * type ValidateOutput,
2000
+ * } from "@evolu/common";
2001
+ *
2002
+ * const defineOutput = <T extends AnyType>(
2003
+ * type: T & ValidateOutput<T>,
2004
+ * ): T => type;
2005
+ *
2006
+ * assertSame(defineOutput(String), String);
2007
+ *
2008
+ * const Value = union(String, Number);
2009
+ * assertSame(defineOutput(Value), Value);
2010
+ *
2011
+ * const uncertain = String as typeof String | typeof Number;
2012
+ * // @ts-expect-error Output Type must be one concrete Type node. Pass a Union Type node instead of a union of Type nodes.
2013
+ * defineOutput(uncertain);
2014
+ * ```
2015
+ *
2016
+ * @group Construction
2017
+ */
2018
+ export type ValidateOutput<T extends AnyType> =
1849
2019
  IsUnion<T> extends false
1850
2020
  ? T
1851
2021
  : CompileTimeError<
@@ -1927,6 +2097,8 @@ type ConcreteChildTypeNameError = CompileTimeError<
1927
2097
  * its error's `type` must equal that name and must not duplicate an inherited
1928
2098
  * error type. An infallible child has no own error to format.
1929
2099
  *
2100
+ * Use {@link createTypeWithError} to wrap an existing validator's errors.
2101
+ *
1930
2102
  * ### Example
1931
2103
  *
1932
2104
  * A root Type for a custom external value category:
@@ -1957,6 +2129,7 @@ type ConcreteChildTypeNameError = CompileTimeError<
1957
2129
  * );
1958
2130
  *
1959
2131
  * assertOk(Text.fromUnknown("Evolu"), "Evolu");
2132
+ *
1960
2133
  * const invalid = Text.fromUnknown(42);
1961
2134
  * assertErr(invalid);
1962
2135
  * assertType(Data, invalid.error);
@@ -2051,10 +2224,8 @@ export function createType(
2051
2224
  }
2052
2225
 
2053
2226
  const assertRefinementIdentity =
2054
- (
2055
- refinement: (value: unknown) => Result<unknown, TypeError>,
2056
- ): ((value: unknown) => Result<unknown, TypeError>) =>
2057
- (value) => {
2227
+ (refinement: (value: unknown) => Result<unknown, TypeError>) =>
2228
+ (value: unknown): Result<unknown, TypeError> => {
2058
2229
  const result = refinement(value);
2059
2230
  if (result.ok) {
2060
2231
  assert(
@@ -2065,6 +2236,70 @@ const assertRefinementIdentity =
2065
2236
  return result;
2066
2237
  };
2067
2238
 
2239
+ /**
2240
+ * Creates a root {@link Type} with a custom error for an existing validator.
2241
+ *
2242
+ * The source must use identity encoding. The new Type accepts its Output as
2243
+ * Input and hides its parent boundaries. Validation and error collection are
2244
+ * delegated automatically; the mapper receives the failure and original value.
2245
+ * The formatter presents the mapped error as one issue.
2246
+ *
2247
+ * ### Example
2248
+ *
2249
+ * ```ts
2250
+ * import {
2251
+ * assertEqual,
2252
+ * assertErr,
2253
+ * createTypeWithError,
2254
+ * Number,
2255
+ * String,
2256
+ * union,
2257
+ * type TypeError,
2258
+ * type UnionError,
2259
+ * } from "@evolu/common";
2260
+ *
2261
+ * interface ValueError extends TypeError<"Value"> {
2262
+ * readonly cause: UnionError;
2263
+ * }
2264
+ *
2265
+ * const Value = createTypeWithError(
2266
+ * "Value",
2267
+ * union(String, Number),
2268
+ * (cause): ValueError => ({ type: "Value", cause }),
2269
+ * () => "Enter text or a number.",
2270
+ * );
2271
+ *
2272
+ * const result = Value.fromUnknown(false, { errors: "all" });
2273
+ * assertErr(result);
2274
+ * assertEqual(result.error.cause.errors.length, 2);
2275
+ * assertEqual(Value.formatError(result.error), "Enter text or a number.");
2276
+ * ```
2277
+ *
2278
+ * @group Construction
2279
+ */
2280
+ export const createTypeWithError = <
2281
+ Name extends TypeName,
2282
+ T extends ConcreteTypeNode,
2283
+ Error extends TypeError<Name>,
2284
+ >(
2285
+ name: Name & ValidateConcreteTypeName<Name>,
2286
+ type: T &
2287
+ ValidateOutput<T> &
2288
+ (IdentityEncodingOf<T> extends true
2289
+ ? unknown
2290
+ : CompileTimeError<"Type", "Source Type must use identity encoding.">),
2291
+ mapError: (error: InferErrors<T>, value: unknown) => Error,
2292
+ formatError: TypeErrorFormatter<NoInfer<Error>>,
2293
+ ): Type<Name, T["Output"], T["Output"], Error> =>
2294
+ createRootType<Name, T["Output"], Error>(
2295
+ name,
2296
+ (value, options) => {
2297
+ const result = type.fromUnknown(value, options);
2298
+ return result.ok ? result : err(mapError(result.error, value));
2299
+ },
2300
+ formatError,
2301
+ );
2302
+
2068
2303
  const createRootType = <Name extends TypeName, Output, Error extends TypeError>(
2069
2304
  name: Name,
2070
2305
  fromUnknown: (
@@ -2581,6 +2816,16 @@ const mapRuntimeResult =
2581
2816
  (value: never, options = firstValidationOptions) =>
2582
2817
  map(operation(value, options), options);
2583
2818
 
2819
+ const createRuntimeOrThrow = (
2820
+ fromInput: RuntimeOperation<Result<unknown, TypeError>>,
2821
+ formatError: TypeErrorFormatter<TypeError>,
2822
+ ): RuntimeOperation<unknown> =>
2823
+ mapRuntimeResult(fromInput, (result) => {
2824
+ if (result.ok) return result.value;
2825
+
2826
+ throw new Error(formatError(result.error), { cause: result.error });
2827
+ });
2828
+
2584
2829
  // `map` must return a fresh operation because this function can attach `.parent`.
2585
2830
  const mapRuntimeOperations = <Input, Output>(
2586
2831
  operation: RuntimeOperation<Input>,
@@ -2666,7 +2911,8 @@ const createTypeNode = <Node extends TypeNode = TypeNode>(
2666
2911
  const runtimeFormatError: TypeErrorFormatter<TypeError> =
2667
2912
  runtimeParent?.[getRuntimeTypeIssuesSymbol] === getTypeIssues
2668
2913
  ? runtimeParent.formatError
2669
- : (error) => formatIssue(getTypeIssues(error, "first")[0]);
2914
+ : (error) =>
2915
+ formatRuntimeTypeIssue(getTypeIssues(error, "first")[0], formatIssue);
2670
2916
  const typedFrom = addRuntimeAssertions(
2671
2917
  name,
2672
2918
  is,
@@ -2690,7 +2936,7 @@ const createTypeNode = <Node extends TypeNode = TypeNode>(
2690
2936
  is,
2691
2937
  from: typedFrom,
2692
2938
  to: typedTo,
2693
- orThrow: mapRuntimeResult(fromInput, getOrThrow),
2939
+ orThrow: createRuntimeOrThrow(fromInput, runtimeFormatError),
2694
2940
  orNull: mapRuntimeResult(fromInput, getOrNull),
2695
2941
  "~standard": createStandardSchemaProps(
2696
2942
  fromUnknown,
@@ -2724,7 +2970,7 @@ const createStandardSchemaProps = (
2724
2970
  ? { value: result.value }
2725
2971
  : {
2726
2972
  issues: getTypeIssues(result.error, "all").map((issue) => ({
2727
- message: formatIssue(issue),
2973
+ message: formatRuntimeTypeIssue(issue, formatIssue),
2728
2974
  path: issue.path,
2729
2975
  })),
2730
2976
  };
@@ -2940,6 +3186,7 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
2940
3186
  * >();
2941
3187
  *
2942
3188
  * assertOk(Age.fromUnknown(122), 122);
3189
+ *
2943
3190
  * const invalid = Age.fromUnknown(200);
2944
3191
  * assertErr(invalid);
2945
3192
  * assertType(Data, invalid.error);
@@ -2968,6 +3215,64 @@ export const BigInt = /*#__PURE__*/ createTypeOfType("BigInt");
2968
3215
  */
2969
3216
  export const Boolean = /*#__PURE__*/ createTypeOfType("Boolean");
2970
3217
 
3218
+ /**
3219
+ * Error returned when a string is neither `true` nor `false`.
3220
+ *
3221
+ * @group Base
3222
+ */
3223
+ export interface BooleanFromStringError extends TypeError<"BooleanFromString"> {
3224
+ readonly value: string;
3225
+ }
3226
+
3227
+ /**
3228
+ * Transforms a boolean spelled as text into a {@link Boolean}.
3229
+ *
3230
+ * This is useful for inputs that carry booleans as text, such as environment
3231
+ * variables, URL query parameters, and form fields. Exactly `true` and `false`
3232
+ * are accepted, the spellings JSON and JavaScript use, so a boolean has one
3233
+ * representation in every source.
3234
+ *
3235
+ * ### Example
3236
+ *
3237
+ * ```ts
3238
+ * import {
3239
+ * assertEqual,
3240
+ * assertErr,
3241
+ * assertOk,
3242
+ * BooleanFromString,
3243
+ * } from "@evolu/common";
3244
+ *
3245
+ * assertOk(BooleanFromString.fromUnknown("true"), true);
3246
+ * assertOk(BooleanFromString.fromUnknown("false"), false);
3247
+ * assertEqual(BooleanFromString.to(true), "true");
3248
+ *
3249
+ * const invalid = BooleanFromString.fromUnknown("yes");
3250
+ * assertErr(invalid, { type: "BooleanFromString", value: "yes" });
3251
+ * assertEqual(
3252
+ * BooleanFromString.formatError(invalid.error),
3253
+ * 'The value "yes" is not a boolean. Use true or false.',
3254
+ * );
3255
+ * ```
3256
+ *
3257
+ * @group Base
3258
+ */
3259
+ export const BooleanFromString = /*#__PURE__*/ transform(
3260
+ "BooleanFromString",
3261
+ String,
3262
+ Boolean,
3263
+ {
3264
+ from: (value): Result<boolean, BooleanFromStringError> =>
3265
+ value === "true"
3266
+ ? ok(true)
3267
+ : value === "false"
3268
+ ? ok(false)
3269
+ : err({ type: "BooleanFromString", value }),
3270
+ to: (value) => (value ? "true" : "false"),
3271
+ },
3272
+ (error) =>
3273
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a boolean. Use true or false.`,
3274
+ );
3275
+
2971
3276
  /**
2972
3277
  * A JavaScript symbol {@link Type}.
2973
3278
  *
@@ -3142,6 +3447,7 @@ export function objectTag<Name extends keyof ObjectTagOutputByName>(
3142
3447
  * "TaggedValue",
3143
3448
  * instanceOf(TaggedValue),
3144
3449
  * );
3450
+ *
3145
3451
  * const value = new TaggedValue();
3146
3452
  * const result = TaggedValueType.fromUnknown(value);
3147
3453
  *
@@ -3344,6 +3650,40 @@ type InstanceConstructorCompileTimeError = CompileTimeError<
3344
3650
  "Constructor must preserve one concrete constructor. Create a Union Type from separate Instance Types instead of passing a union or erased constructor."
3345
3651
  >;
3346
3652
 
3653
+ /**
3654
+ * Requires one concrete literal type in a factory parameter.
3655
+ *
3656
+ * Preserves an exact literal and produces a compile-time error for widened,
3657
+ * union, branded, or open template literal types. Use an intersection with the
3658
+ * inferred parameter type, as {@link prefixed} does. This guard performs no
3659
+ * runtime validation.
3660
+ *
3661
+ * ### Example
3662
+ *
3663
+ * ```ts
3664
+ * import { assertType, type ValidateLiteral } from "@evolu/common";
3665
+ *
3666
+ * const definePrefix = <Prefix extends string>(
3667
+ * prefix: Prefix & ValidateLiteral<Prefix>,
3668
+ * ): Prefix => prefix;
3669
+ *
3670
+ * const prefix = definePrefix("APP_");
3671
+ * assertType<typeof prefix, "APP_">();
3672
+ *
3673
+ * const widened: string = "APP_";
3674
+ * // @ts-expect-error Expected must be one concrete literal value.
3675
+ * definePrefix(widened);
3676
+ * ```
3677
+ *
3678
+ * @group Unions
3679
+ */
3680
+ export type ValidateLiteral<Expected extends Literal> =
3681
+ IsUnion<Expected> extends false
3682
+ ? {} extends Readonly<Record<`${Expected}`, never>>
3683
+ ? LiteralCompileTimeError
3684
+ : Expected
3685
+ : LiteralCompileTimeError;
3686
+
3347
3687
  /**
3348
3688
  * Literal {@link Type}.
3349
3689
  *
@@ -3372,6 +3712,7 @@ type InstanceConstructorCompileTimeError = CompileTimeError<
3372
3712
  *
3373
3713
  * assertType<typeof Ready.Output, "ready">();
3374
3714
  * assertOk(Ready.fromUnknown("ready"), "ready");
3715
+ *
3375
3716
  * const invalid = Ready.fromUnknown("pending");
3376
3717
  * assertErr(invalid);
3377
3718
  * assertType(Data, invalid.error);
@@ -3463,13 +3804,6 @@ type LiteralParentErrors<Expected extends Literal> =
3463
3804
  ? InferErrors<Parent>
3464
3805
  : never;
3465
3806
 
3466
- type ValidateLiteral<Expected extends Literal> =
3467
- IsUnion<Expected> extends false
3468
- ? {} extends Readonly<Record<`${Expected}`, never>>
3469
- ? LiteralCompileTimeError
3470
- : Expected
3471
- : LiteralCompileTimeError;
3472
-
3473
3807
  type LiteralCompileTimeError = CompileTimeError<
3474
3808
  "Type",
3475
3809
  "Expected must be one concrete literal value."
@@ -3523,6 +3857,11 @@ export const Null = /*#__PURE__*/ literal(null);
3523
3857
  * errors: "all" }` to retain every member failure and collect nested errors
3524
3858
  * within each member.
3525
3859
  *
3860
+ * Formatting keeps one Union issue at the enclosing path and appends the
3861
+ * retained failures below its summary. Member indexes identify alternatives,
3862
+ * not positions in the input. Nested member paths appear in the message.
3863
+ * Formatting never retries validation or recovers discarded failures.
3864
+ *
3526
3865
  * Member order matters when multiple members accept the same value: validation
3527
3866
  * and encoding use the first matching member. When member Inputs overlap,
3528
3867
  * decoding the value emitted by the first member selected for an Output must
@@ -3611,10 +3950,7 @@ export function union(
3611
3950
  inputMembers,
3612
3951
  (member, value, options) => member[outputValidationSymbol](value, options),
3613
3952
  );
3614
- const defaultFormatter = (() =>
3615
- "A value does not match any allowed variant.") as TypeErrorFormatter<TypeError>;
3616
- const getTypeIssues: RuntimeGetTypeIssues = (error) =>
3617
- singleRuntimeTypeIssue("Union", error, defaultFormatter);
3953
+ const getTypeIssues = createUnionRuntimeTypeIssues(members);
3618
3954
  const input = createTypeNode<
3619
3955
  UnionInputType<unknown, UnionErrorValue<TypeError>>
3620
3956
  >(
@@ -3625,7 +3961,7 @@ export function union(
3625
3961
  inputValidateOutput,
3626
3962
  ok,
3627
3963
  identity,
3628
- getTypeIssues,
3964
+ createUnionRuntimeTypeIssues(inputMembers),
3629
3965
  );
3630
3966
  const fromUnknown = createUnionValidation(members, (member, value, options) =>
3631
3967
  member.fromUnknown(value, options),
@@ -3667,6 +4003,24 @@ export function union(
3667
4003
  );
3668
4004
  }
3669
4005
 
4006
+ const createUnionRuntimeTypeIssues =
4007
+ (members: ReadonlyArray<RuntimeTypeNode>): RuntimeGetTypeIssues =>
4008
+ (error) => [
4009
+ {
4010
+ name: "Union",
4011
+ error,
4012
+ path: [],
4013
+ formatError: () => "A value does not match any allowed variant.",
4014
+ alternatives: (error as UnionErrorValue<TypeError>).errors.map(
4015
+ ({ index, error }) => ({
4016
+ index,
4017
+ name: members[index].name,
4018
+ issues: members[index][getRuntimeTypeIssuesSymbol](error, "all"),
4019
+ }),
4020
+ ),
4021
+ },
4022
+ ];
4023
+
3670
4024
  const createUnionValidation =
3671
4025
  (
3672
4026
  members: ReadonlyArray<RuntimeTypeNode>,
@@ -4024,6 +4378,7 @@ interface UnionErrorValue<
4024
4378
  * assertOk(result, ["cs", "CZ"]);
4025
4379
  * const locale = result.value;
4026
4380
  * assertType<typeof locale, SupportedLocale>();
4381
+ *
4027
4382
  * const invalid = SupportedLocale.fromUnknown("cs/CZ");
4028
4383
  * assertErr(invalid);
4029
4384
  * assertType(Data, invalid.error);
@@ -4892,6 +5247,7 @@ const getTemplateLiteralPartFraming = (
4892
5247
  * }
4893
5248
  *
4894
5249
  * assertOk(Int64.fromUnknown(42n), 42n);
5250
+ *
4895
5251
  * const invalid = Int64.fromUnknown(2n ** 63n);
4896
5252
  * assertErr(invalid);
4897
5253
  * assertType(Data, invalid.error);
@@ -4990,6 +5346,7 @@ export interface BrandType<
4990
5346
  *
4991
5347
  * const value = "2023-01-01T12:00:00.000Z";
4992
5348
  * assertOk(DateIso.fromUnknown(value), value);
5349
+ *
4993
5350
  * const invalid = DateIso.fromUnknown("2023-01-01");
4994
5351
  * assertErr(invalid);
4995
5352
  * assertType(Data, invalid.error);
@@ -5164,6 +5521,7 @@ export interface UInt64Error extends TypeError<"UInt64"> {
5164
5521
  * }
5165
5522
  *
5166
5523
  * assertOk(TrimmedString.fromUnknown("Evolu"), "Evolu");
5524
+ *
5167
5525
  * const invalid = TrimmedString.fromUnknown(" Evolu");
5168
5526
  * assertErr(invalid);
5169
5527
  * assertType(Data, invalid.error);
@@ -5260,144 +5618,1147 @@ type BrandFactoryNumberError = CompileTimeError<
5260
5618
  >;
5261
5619
 
5262
5620
  /**
5263
- * Capitalized {@link Brand}.
5621
+ * Naming conventions accepted by {@link identifier}.
5622
+ *
5623
+ * @group String
5624
+ */
5625
+ export type IdentifierCasing =
5626
+ "camelCase" | "PascalCase" | "snake_case" | "kebab-case" | "CONSTANT_CASE";
5627
+
5628
+ /**
5629
+ * Brand names of the Types that {@link identifier} creates, by casing.
5630
+ *
5631
+ * @group String
5632
+ */
5633
+ export interface IdentifierBrandByCasing {
5634
+ readonly camelCase: "CamelCaseIdentifier";
5635
+ readonly PascalCase: "PascalCaseIdentifier";
5636
+ readonly snake_case: "SnakeCaseIdentifier";
5637
+ readonly "kebab-case": "KebabCaseIdentifier";
5638
+ readonly CONSTANT_CASE: "ConstantCaseIdentifier";
5639
+ }
5640
+
5641
+ /**
5642
+ * Adds identifier validation in a naming convention to an existing string Type.
5643
+ *
5644
+ * An identifier is one or more ASCII words, each starting with a letter and
5645
+ * continuing with letters or digits. camelCase and PascalCase start every word
5646
+ * after the first with an uppercase letter, so `httpUrl` has two words and
5647
+ * `httpURL` has four. snake_case, kebab-case, and CONSTANT_CASE put exactly one
5648
+ * separator between words. Validation keeps the spelling and rejects empty
5649
+ * strings, whitespace, punctuation, and non-ASCII characters.
5264
5650
  *
5265
- * Requires the first character of a string to be uppercase.
5651
+ * Words never start with a digit. Identifier grammars in most languages forbid
5652
+ * a leading digit, and camelCase cannot mark a word boundary before one, so the
5653
+ * rule applies to every word and keeps every conversion exact. Join an
5654
+ * abbreviation such as `2FA` to the previous word, as in `MAX2FA_ATTEMPTS` and
5655
+ * `max2faAttempts`, or spell the number out, as in `TWO_FACTOR_SECRET`.
5656
+ *
5657
+ * Convert validated identifiers with functions such as
5658
+ * {@link camelCaseToSnakeCase}. Conversions preserve word boundaries, and
5659
+ * converting back restores the original spelling. They return only the
5660
+ * destination brand, dropping unrelated constraints such as input length.
5266
5661
  *
5267
5662
  * ### Example
5268
5663
  *
5269
5664
  * ```ts
5270
5665
  * import {
5271
- * assertEqual,
5272
5666
  * assertErr,
5273
5667
  * assertOk,
5274
- * assertType,
5275
- * Data,
5668
+ * identifier,
5669
+ * maxLength,
5276
5670
  * String,
5277
- * capitalized,
5278
- * type Brand,
5279
5671
  * } from "@evolu/common";
5280
5672
  *
5281
- * const CapitalizedString = capitalized(String);
5282
- * type CapitalizedString = typeof CapitalizedString.Output;
5283
- *
5284
- * assertType<CapitalizedString, string & Brand<"Capitalized">>();
5673
+ * const EnvName = identifier("CONSTANT_CASE")(maxLength(16)(String));
5285
5674
  *
5286
- * assertOk(CapitalizedString.fromUnknown("Evolu"), "Evolu");
5287
- * const invalid = CapitalizedString.fromUnknown("evolu");
5288
- * assertErr(invalid);
5289
- * assertType(Data, invalid.error);
5290
- * assertEqual(invalid.error, {
5291
- * type: "Capitalized",
5292
- * value: "evolu",
5293
- * });
5675
+ * assertOk(EnvName.fromUnknown("HTTP2_PORT"), "HTTP2_PORT");
5676
+ * assertErr(EnvName.fromUnknown("HTTP_2_PORT"));
5294
5677
  * ```
5295
5678
  *
5296
5679
  * @group String
5297
5680
  */
5298
- export const capitalized: BrandFactory<
5299
- "Capitalized",
5300
- string,
5301
- CapitalizedError
5302
- > = (parent) =>
5303
- brand(
5304
- "Capitalized",
5305
- parent,
5306
- (value) => {
5307
- const [first = ""] = value;
5681
+ export const identifier =
5682
+ <Casing extends IdentifierCasing>(
5683
+ casing: Casing,
5684
+ ): BrandFactory<
5685
+ IdentifierBrandByCasing[Casing],
5686
+ string,
5687
+ IdentifierError<Casing>
5688
+ > =>
5689
+ (parent) => {
5690
+ const name = identifierBrandByCasing[casing];
5691
+ const pattern = identifierPatternByCasing[casing];
5308
5692
 
5309
- return value === first.toUpperCase() + value.slice(first.length)
5310
- ? ok()
5311
- : err<CapitalizedError>({ type: "Capitalized", value });
5312
- },
5313
- (error) =>
5314
- `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`,
5315
- );
5693
+ return brand(
5694
+ name,
5695
+ parent,
5696
+ (value) =>
5697
+ pattern.test(value)
5698
+ ? ok()
5699
+ : err<IdentifierError<Casing>>({ type: name, value, casing }),
5700
+ (error) =>
5701
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a ${error.casing} identifier.`,
5702
+ );
5703
+ };
5316
5704
 
5317
5705
  /**
5318
- * Error returned when {@link capitalized} rejects a string.
5706
+ * Error returned when {@link identifier} rejects a string.
5319
5707
  *
5320
5708
  * @group String
5321
5709
  */
5322
- export interface CapitalizedError extends TypeError<"Capitalized"> {
5710
+ export interface IdentifierError<
5711
+ Casing extends IdentifierCasing = IdentifierCasing,
5712
+ > extends TypeError<IdentifierBrandByCasing[Casing]> {
5323
5713
  readonly value: string;
5714
+ readonly casing: Casing;
5324
5715
  }
5325
5716
 
5717
+ const identifierBrandByCasing: IdentifierBrandByCasing = {
5718
+ camelCase: "CamelCaseIdentifier",
5719
+ PascalCase: "PascalCaseIdentifier",
5720
+ snake_case: "SnakeCaseIdentifier",
5721
+ "kebab-case": "KebabCaseIdentifier",
5722
+ CONSTANT_CASE: "ConstantCaseIdentifier",
5723
+ };
5724
+
5725
+ const identifierPatternByCasing: ReadonlyRecord<IdentifierCasing, RegExp> = {
5726
+ camelCase: /^[a-z][a-zA-Z0-9]*$/u,
5727
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/u,
5728
+ snake_case: /^[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*$/u,
5729
+ "kebab-case": /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*$/u,
5730
+ CONSTANT_CASE: /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)*$/u,
5731
+ };
5732
+
5326
5733
  /**
5327
- * Capitalized {@link String}.
5734
+ * A validated camelCase identifier, such as `http2Port`.
5735
+ *
5736
+ * See {@link identifier} for the grammar and the conversion functions.
5328
5737
  *
5329
5738
  * @group String
5330
5739
  */
5331
- export const CapitalizedString = /*#__PURE__*/ capitalized(String);
5332
- export type CapitalizedString = typeof CapitalizedString.Output;
5740
+ export const CamelCaseIdentifier =
5741
+ /*#__PURE__*/ identifier("camelCase")(String);
5742
+ export type CamelCaseIdentifier = typeof CamelCaseIdentifier.Output;
5333
5743
 
5334
5744
  /**
5335
- * String {@link Brand} without surrounding whitespace.
5745
+ * A validated PascalCase identifier, such as `Http2Port`.
5336
5746
  *
5337
- * ### Example
5747
+ * See {@link identifier} for the grammar and the conversion functions.
5338
5748
  *
5339
- * ```ts
5749
+ * @group String
5750
+ */
5751
+ export const PascalCaseIdentifier =
5752
+ /*#__PURE__*/ identifier("PascalCase")(String);
5753
+ export type PascalCaseIdentifier = typeof PascalCaseIdentifier.Output;
5754
+
5755
+ /**
5756
+ * A validated snake_case identifier, such as `http2_port`.
5757
+ *
5758
+ * See {@link identifier} for the grammar and the conversion functions.
5759
+ *
5760
+ * @group String
5761
+ */
5762
+ export const SnakeCaseIdentifier =
5763
+ /*#__PURE__*/ identifier("snake_case")(String);
5764
+ export type SnakeCaseIdentifier = typeof SnakeCaseIdentifier.Output;
5765
+
5766
+ /**
5767
+ * A validated kebab-case identifier, such as `http2-port`.
5768
+ *
5769
+ * See {@link identifier} for the grammar and the conversion functions.
5770
+ *
5771
+ * @group String
5772
+ */
5773
+ export const KebabCaseIdentifier =
5774
+ /*#__PURE__*/ identifier("kebab-case")(String);
5775
+ export type KebabCaseIdentifier = typeof KebabCaseIdentifier.Output;
5776
+
5777
+ /**
5778
+ * A validated CONSTANT_CASE identifier, such as `HTTP2_PORT`.
5779
+ *
5780
+ * See {@link identifier} for the grammar and the conversion functions.
5781
+ *
5782
+ * @group String
5783
+ */
5784
+ export const ConstantCaseIdentifier =
5785
+ /*#__PURE__*/ identifier("CONSTANT_CASE")(String);
5786
+ export type ConstantCaseIdentifier = typeof ConstantCaseIdentifier.Output;
5787
+
5788
+ /**
5789
+ * Converts a {@link CamelCaseIdentifier} to a {@link PascalCaseIdentifier}.
5790
+ *
5791
+ * Converts `http2Port` to `Http2Port`. Use {@link pascalCaseToCamelCase} to
5792
+ * recover the original spelling.
5793
+ *
5794
+ * @group String
5795
+ */
5796
+ export const camelCaseToPascalCase = (
5797
+ value: CamelCaseIdentifier,
5798
+ ): PascalCaseIdentifier =>
5799
+ (value.charAt(0).toUpperCase() + value.slice(1)) as PascalCaseIdentifier;
5800
+
5801
+ /**
5802
+ * Converts a {@link CamelCaseIdentifier} to a {@link SnakeCaseIdentifier}.
5803
+ *
5804
+ * Converts `http2Port` to `http2_port`. Use {@link snakeCaseToCamelCase} to
5805
+ * recover the original spelling.
5806
+ *
5807
+ * @group String
5808
+ */
5809
+ export const camelCaseToSnakeCase = (
5810
+ value: CamelCaseIdentifier,
5811
+ ): SnakeCaseIdentifier =>
5812
+ value.replaceAll(
5813
+ /[A-Z]/gu,
5814
+ (letter) => `_${letter.toLowerCase()}`,
5815
+ ) as SnakeCaseIdentifier;
5816
+
5817
+ /**
5818
+ * Converts a {@link CamelCaseIdentifier} to a {@link KebabCaseIdentifier}.
5819
+ *
5820
+ * Converts `http2Port` to `http2-port`. Use {@link kebabCaseToCamelCase} to
5821
+ * recover the original spelling.
5822
+ *
5823
+ * @group String
5824
+ */
5825
+ export const camelCaseToKebabCase = (
5826
+ value: CamelCaseIdentifier,
5827
+ ): KebabCaseIdentifier => snakeCaseToKebabCase(camelCaseToSnakeCase(value));
5828
+
5829
+ /**
5830
+ * Converts a {@link CamelCaseIdentifier} to a {@link ConstantCaseIdentifier}.
5831
+ *
5832
+ * Converts `http2Port` to `HTTP2_PORT`. Use {@link constantCaseToCamelCase} to
5833
+ * recover the original spelling.
5834
+ *
5835
+ * @group String
5836
+ */
5837
+ export const camelCaseToConstantCase = (
5838
+ value: CamelCaseIdentifier,
5839
+ ): ConstantCaseIdentifier =>
5840
+ snakeCaseToConstantCase(camelCaseToSnakeCase(value));
5841
+
5842
+ /**
5843
+ * Converts a {@link PascalCaseIdentifier} to a {@link CamelCaseIdentifier}.
5844
+ *
5845
+ * Converts `Http2Port` to `http2Port`. Use {@link camelCaseToPascalCase} to
5846
+ * recover the original spelling.
5847
+ *
5848
+ * @group String
5849
+ */
5850
+ export const pascalCaseToCamelCase = (
5851
+ value: PascalCaseIdentifier,
5852
+ ): CamelCaseIdentifier =>
5853
+ (value.charAt(0).toLowerCase() + value.slice(1)) as CamelCaseIdentifier;
5854
+
5855
+ /**
5856
+ * Converts a {@link PascalCaseIdentifier} to a {@link SnakeCaseIdentifier}.
5857
+ *
5858
+ * Converts `Http2Port` to `http2_port`. Use {@link snakeCaseToPascalCase} to
5859
+ * recover the original spelling.
5860
+ *
5861
+ * @group String
5862
+ */
5863
+ export const pascalCaseToSnakeCase = (
5864
+ value: PascalCaseIdentifier,
5865
+ ): SnakeCaseIdentifier => camelCaseToSnakeCase(pascalCaseToCamelCase(value));
5866
+
5867
+ /**
5868
+ * Converts a {@link PascalCaseIdentifier} to a {@link KebabCaseIdentifier}.
5869
+ *
5870
+ * Converts `Http2Port` to `http2-port`. Use {@link kebabCaseToPascalCase} to
5871
+ * recover the original spelling.
5872
+ *
5873
+ * @group String
5874
+ */
5875
+ export const pascalCaseToKebabCase = (
5876
+ value: PascalCaseIdentifier,
5877
+ ): KebabCaseIdentifier => snakeCaseToKebabCase(pascalCaseToSnakeCase(value));
5878
+
5879
+ /**
5880
+ * Converts a {@link PascalCaseIdentifier} to a {@link ConstantCaseIdentifier}.
5881
+ *
5882
+ * Converts `Http2Port` to `HTTP2_PORT`. Use {@link constantCaseToPascalCase} to
5883
+ * recover the original spelling.
5884
+ *
5885
+ * @group String
5886
+ */
5887
+ export const pascalCaseToConstantCase = (
5888
+ value: PascalCaseIdentifier,
5889
+ ): ConstantCaseIdentifier =>
5890
+ snakeCaseToConstantCase(pascalCaseToSnakeCase(value));
5891
+
5892
+ /**
5893
+ * Converts a {@link SnakeCaseIdentifier} to a {@link CamelCaseIdentifier}.
5894
+ *
5895
+ * Converts `http2_port` to `http2Port`. Use {@link camelCaseToSnakeCase} to
5896
+ * recover the original spelling.
5897
+ *
5898
+ * @group String
5899
+ */
5900
+ export const snakeCaseToCamelCase = (
5901
+ value: SnakeCaseIdentifier,
5902
+ ): CamelCaseIdentifier =>
5903
+ value.replaceAll(/_[a-z]/gu, (word) =>
5904
+ word.charAt(1).toUpperCase(),
5905
+ ) as CamelCaseIdentifier;
5906
+
5907
+ /**
5908
+ * Converts a {@link SnakeCaseIdentifier} to a {@link PascalCaseIdentifier}.
5909
+ *
5910
+ * Converts `http2_port` to `Http2Port`. Use {@link pascalCaseToSnakeCase} to
5911
+ * recover the original spelling.
5912
+ *
5913
+ * @group String
5914
+ */
5915
+ export const snakeCaseToPascalCase = (
5916
+ value: SnakeCaseIdentifier,
5917
+ ): PascalCaseIdentifier => camelCaseToPascalCase(snakeCaseToCamelCase(value));
5918
+
5919
+ /**
5920
+ * Converts a {@link SnakeCaseIdentifier} to a {@link KebabCaseIdentifier}.
5921
+ *
5922
+ * Converts `http2_port` to `http2-port`. Use {@link kebabCaseToSnakeCase} to
5923
+ * recover the original spelling.
5924
+ *
5925
+ * @group String
5926
+ */
5927
+ export const snakeCaseToKebabCase = (
5928
+ value: SnakeCaseIdentifier,
5929
+ ): KebabCaseIdentifier => value.replaceAll("_", "-") as KebabCaseIdentifier;
5930
+
5931
+ /**
5932
+ * Converts a {@link SnakeCaseIdentifier} to a {@link ConstantCaseIdentifier}.
5933
+ *
5934
+ * Converts `http2_port` to `HTTP2_PORT`. Use {@link constantCaseToSnakeCase} to
5935
+ * recover the original spelling.
5936
+ *
5937
+ * @group String
5938
+ */
5939
+ export const snakeCaseToConstantCase = (
5940
+ value: SnakeCaseIdentifier,
5941
+ ): ConstantCaseIdentifier => value.toUpperCase() as ConstantCaseIdentifier;
5942
+
5943
+ /**
5944
+ * Converts a {@link KebabCaseIdentifier} to a {@link CamelCaseIdentifier}.
5945
+ *
5946
+ * Converts `http2-port` to `http2Port`. Use {@link camelCaseToKebabCase} to
5947
+ * recover the original spelling.
5948
+ *
5949
+ * @group String
5950
+ */
5951
+ export const kebabCaseToCamelCase = (
5952
+ value: KebabCaseIdentifier,
5953
+ ): CamelCaseIdentifier => snakeCaseToCamelCase(kebabCaseToSnakeCase(value));
5954
+
5955
+ /**
5956
+ * Converts a {@link KebabCaseIdentifier} to a {@link PascalCaseIdentifier}.
5957
+ *
5958
+ * Converts `http2-port` to `Http2Port`. Use {@link pascalCaseToKebabCase} to
5959
+ * recover the original spelling.
5960
+ *
5961
+ * @group String
5962
+ */
5963
+ export const kebabCaseToPascalCase = (
5964
+ value: KebabCaseIdentifier,
5965
+ ): PascalCaseIdentifier => camelCaseToPascalCase(kebabCaseToCamelCase(value));
5966
+
5967
+ /**
5968
+ * Converts a {@link KebabCaseIdentifier} to a {@link SnakeCaseIdentifier}.
5969
+ *
5970
+ * Converts `http2-port` to `http2_port`. Use {@link snakeCaseToKebabCase} to
5971
+ * recover the original spelling.
5972
+ *
5973
+ * @group String
5974
+ */
5975
+ export const kebabCaseToSnakeCase = (
5976
+ value: KebabCaseIdentifier,
5977
+ ): SnakeCaseIdentifier => value.replaceAll("-", "_") as SnakeCaseIdentifier;
5978
+
5979
+ /**
5980
+ * Converts a {@link KebabCaseIdentifier} to a {@link ConstantCaseIdentifier}.
5981
+ *
5982
+ * Converts `http2-port` to `HTTP2_PORT`. Use {@link constantCaseToKebabCase} to
5983
+ * recover the original spelling.
5984
+ *
5985
+ * @group String
5986
+ */
5987
+ export const kebabCaseToConstantCase = (
5988
+ value: KebabCaseIdentifier,
5989
+ ): ConstantCaseIdentifier =>
5990
+ snakeCaseToConstantCase(kebabCaseToSnakeCase(value));
5991
+
5992
+ /**
5993
+ * Converts a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier}.
5994
+ *
5995
+ * Converts `HTTP2_PORT` to `http2Port`. Use {@link camelCaseToConstantCase} to
5996
+ * recover the original spelling.
5997
+ *
5998
+ * @group String
5999
+ */
6000
+ export const constantCaseToCamelCase = (
6001
+ value: ConstantCaseIdentifier,
6002
+ ): CamelCaseIdentifier => snakeCaseToCamelCase(constantCaseToSnakeCase(value));
6003
+
6004
+ /**
6005
+ * Decodes a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier} and
6006
+ * restores the original spelling when encoding.
6007
+ *
6008
+ * ### Example
6009
+ *
6010
+ * ```ts
6011
+ * import {
6012
+ * assertEqual,
6013
+ * assertOk,
6014
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
6015
+ * } from "@evolu/common";
6016
+ *
6017
+ * const Key = CamelCaseIdentifierFromConstantCaseIdentifier;
6018
+ * const result = Key.fromUnknown("HTTP2_PORT");
6019
+ * assertOk(result, "http2Port");
6020
+ * assertEqual(Key.to(result.value), "HTTP2_PORT");
6021
+ * ```
6022
+ *
6023
+ * @group String
6024
+ */
6025
+ export const CamelCaseIdentifierFromConstantCaseIdentifier =
6026
+ /*#__PURE__*/ transform(
6027
+ "CamelCaseIdentifierFromConstantCaseIdentifier",
6028
+ ConstantCaseIdentifier,
6029
+ CamelCaseIdentifier,
6030
+ {
6031
+ from: (value) => ok(constantCaseToCamelCase(value)),
6032
+ to: camelCaseToConstantCase,
6033
+ },
6034
+ );
6035
+
6036
+ /**
6037
+ * Converts a {@link ConstantCaseIdentifier} to a {@link PascalCaseIdentifier}.
6038
+ *
6039
+ * Converts `HTTP2_PORT` to `Http2Port`. Use {@link pascalCaseToConstantCase} to
6040
+ * recover the original spelling.
6041
+ *
6042
+ * @group String
6043
+ */
6044
+ export const constantCaseToPascalCase = (
6045
+ value: ConstantCaseIdentifier,
6046
+ ): PascalCaseIdentifier =>
6047
+ camelCaseToPascalCase(constantCaseToCamelCase(value));
6048
+
6049
+ /**
6050
+ * Converts a {@link ConstantCaseIdentifier} to a {@link SnakeCaseIdentifier}.
6051
+ *
6052
+ * Converts `HTTP2_PORT` to `http2_port`. Use {@link snakeCaseToConstantCase} to
6053
+ * recover the original spelling.
6054
+ *
6055
+ * @group String
6056
+ */
6057
+ export const constantCaseToSnakeCase = (
6058
+ value: ConstantCaseIdentifier,
6059
+ ): SnakeCaseIdentifier => value.toLowerCase() as SnakeCaseIdentifier;
6060
+
6061
+ /**
6062
+ * Converts a {@link ConstantCaseIdentifier} to a {@link KebabCaseIdentifier}.
6063
+ *
6064
+ * Converts `HTTP2_PORT` to `http2-port`. Use {@link kebabCaseToConstantCase} to
6065
+ * recover the original spelling.
6066
+ *
6067
+ * @group String
6068
+ */
6069
+ export const constantCaseToKebabCase = (
6070
+ value: ConstantCaseIdentifier,
6071
+ ): KebabCaseIdentifier => snakeCaseToKebabCase(constantCaseToSnakeCase(value));
6072
+
6073
+ /**
6074
+ * Adds capitalized text validation to an existing string Type.
6075
+ *
6076
+ * Narrows the output to TypeScript's `Capitalize<string>` while preserving the
6077
+ * parent Type's constraints. Validation leaves the text unchanged. Use
6078
+ * {@link capitalize} to change its casing.
6079
+ *
6080
+ * ### Example
6081
+ *
6082
+ * ```ts
6083
+ * import {
6084
+ * assertErr,
6085
+ * assertOk,
6086
+ * capitalized,
6087
+ * maxLength,
6088
+ * String,
6089
+ * } from "@evolu/common";
6090
+ *
6091
+ * const Label = capitalized(maxLength(50)(String));
6092
+ * assertOk(Label.fromUnknown("Hello world"), "Hello world");
6093
+ * assertErr(Label.fromUnknown("hello world"));
6094
+ * ```
6095
+ *
6096
+ * @group String
6097
+ */
6098
+ export const capitalized = <
6099
+ ParentType extends ConcreteTypeNode & { readonly Output: string },
6100
+ >(
6101
+ parent: ValidateBrandParent<"Capitalized", ParentType>,
6102
+ ): ReturnType<
6103
+ typeof createType<
6104
+ "Capitalized",
6105
+ ParentType,
6106
+ ParentType["Output"] & Capitalize<string>,
6107
+ CapitalizedError
6108
+ >
6109
+ > =>
6110
+ createType<
6111
+ "Capitalized",
6112
+ ParentType,
6113
+ ParentType["Output"] & Capitalize<string>,
6114
+ CapitalizedError
6115
+ >(
6116
+ "Capitalized",
6117
+ parent,
6118
+ (value) =>
6119
+ value === capitalize(value)
6120
+ ? ok(value as ParentType["Output"] & Capitalize<string>)
6121
+ : err({ type: "Capitalized", value }),
6122
+ (error) =>
6123
+ `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`,
6124
+ );
6125
+
6126
+ /**
6127
+ * Error returned when {@link capitalized} rejects a string.
6128
+ *
6129
+ * @group String
6130
+ */
6131
+ export interface CapitalizedError extends TypeError<"Capitalized"> {
6132
+ readonly value: string;
6133
+ }
6134
+
6135
+ /**
6136
+ * Validates capitalized text as TypeScript's `Capitalize<string>`.
6137
+ *
6138
+ * The rest of the text can use any casing. Empty strings and text starting with
6139
+ * an uncased character, such as a digit or emoji, are valid. Use
6140
+ * {@link capitalize} to produce a capitalized value from any string.
6141
+ *
6142
+ * Capitalization applies to general text, including spaces and punctuation.
6143
+ * Both `hello` and `Hello` become `Hello`, so the original initial casing
6144
+ * cannot be recovered. Capitalization does not identify words or turn text into
6145
+ * an identifier.
6146
+ *
6147
+ * ### Example
6148
+ *
6149
+ * ```ts
6150
+ * import { assertErr, assertOk, CapitalizedString } from "@evolu/common";
6151
+ *
6152
+ * const text: CapitalizedString = "Hello world";
6153
+ * assertOk(CapitalizedString.fromUnknown(text), text);
6154
+ * assertErr(CapitalizedString.fromUnknown("hello world"));
6155
+ * assertOk(CapitalizedString.fromUnknown(""), "");
6156
+ * ```
6157
+ *
6158
+ * @group String
6159
+ */
6160
+ export const CapitalizedString = /*#__PURE__*/ capitalized(String);
6161
+ export type CapitalizedString = typeof CapitalizedString.Output;
6162
+
6163
+ /**
6164
+ * Uppercases the first Unicode code point and returns a
6165
+ * {@link CapitalizedString}.
6166
+ *
6167
+ * Preserves the remainder of the string and leaves an empty string unchanged.
6168
+ * Uses JavaScript's default Unicode casing without locale-specific rules.
6169
+ * Changing case can change the length, so input brands are not retained.
6170
+ *
6171
+ * ### Example
6172
+ *
6173
+ * ```ts
6174
+ * import { assertEqual, assertType, capitalize } from "@evolu/common";
6175
+ *
6176
+ * const text = capitalize("hello world");
6177
+ * assertEqual(text, "Hello world");
6178
+ * assertType<typeof text, "Hello world">();
6179
+ * assertEqual(capitalize(text), text);
6180
+ * ```
6181
+ *
6182
+ * @group String
6183
+ */
6184
+ export const capitalize = <S extends string>(value: S): Capitalize<S> => {
6185
+ const [first = ""] = value;
6186
+ return (first.toUpperCase() + value.slice(first.length)) as Capitalize<S>;
6187
+ };
6188
+
6189
+ /**
6190
+ * Adds uncapitalized text validation to an existing string Type.
6191
+ *
6192
+ * Narrows the output to TypeScript's `Uncapitalize<string>` while preserving
6193
+ * the parent Type's constraints. Validation leaves the text unchanged. Use
6194
+ * {@link uncapitalize} to change its casing.
6195
+ *
6196
+ * ### Example
6197
+ *
6198
+ * ```ts
6199
+ * import {
6200
+ * assertErr,
6201
+ * assertOk,
6202
+ * uncapitalized,
6203
+ * maxLength,
6204
+ * String,
6205
+ * } from "@evolu/common";
6206
+ *
6207
+ * const Label = uncapitalized(maxLength(50)(String));
6208
+ * assertOk(Label.fromUnknown("hello WORLD"), "hello WORLD");
6209
+ * assertErr(Label.fromUnknown("Hello WORLD"));
6210
+ * ```
6211
+ *
6212
+ * @group String
6213
+ */
6214
+ export const uncapitalized = <
6215
+ ParentType extends ConcreteTypeNode & { readonly Output: string },
6216
+ >(
6217
+ parent: ValidateBrandParent<"Uncapitalized", ParentType>,
6218
+ ): ReturnType<
6219
+ typeof createType<
6220
+ "Uncapitalized",
6221
+ ParentType,
6222
+ ParentType["Output"] & Uncapitalize<string>,
6223
+ UncapitalizedError
6224
+ >
6225
+ > =>
6226
+ createType<
6227
+ "Uncapitalized",
6228
+ ParentType,
6229
+ ParentType["Output"] & Uncapitalize<string>,
6230
+ UncapitalizedError
6231
+ >(
6232
+ "Uncapitalized",
6233
+ parent,
6234
+ (value) =>
6235
+ value === uncapitalize(value)
6236
+ ? ok(value as ParentType["Output"] & Uncapitalize<string>)
6237
+ : err({ type: "Uncapitalized", value }),
6238
+ (error) =>
6239
+ `The value ${safelyStringifyUnknownValue(error.value)} must not start with an uppercase letter.`,
6240
+ );
6241
+
6242
+ /**
6243
+ * Error returned when {@link uncapitalized} rejects a string.
6244
+ *
6245
+ * @group String
6246
+ */
6247
+ export interface UncapitalizedError extends TypeError<"Uncapitalized"> {
6248
+ readonly value: string;
6249
+ }
6250
+
6251
+ /**
6252
+ * Validates uncapitalized text as TypeScript's `Uncapitalize<string>`.
6253
+ *
6254
+ * The rest of the text can use any casing. Empty strings and text starting with
6255
+ * an uncased character, such as a digit or emoji, are valid. Use
6256
+ * {@link uncapitalize} to produce an uncapitalized value from any string.
6257
+ *
6258
+ * ### Example
6259
+ *
6260
+ * ```ts
6261
+ * import { assertErr, assertOk, UncapitalizedString } from "@evolu/common";
6262
+ *
6263
+ * const text: UncapitalizedString = "hello WORLD";
6264
+ * assertOk(UncapitalizedString.fromUnknown(text), text);
6265
+ * assertErr(UncapitalizedString.fromUnknown("Hello WORLD"));
6266
+ * assertOk(UncapitalizedString.fromUnknown(""), "");
6267
+ * ```
6268
+ *
6269
+ * @group String
6270
+ */
6271
+ export const UncapitalizedString = /*#__PURE__*/ uncapitalized(String);
6272
+ export type UncapitalizedString = typeof UncapitalizedString.Output;
6273
+
6274
+ /**
6275
+ * Lowercases the first Unicode code point and returns a
6276
+ * {@link UncapitalizedString}.
6277
+ *
6278
+ * Preserves the remainder of the string and leaves an empty string unchanged.
6279
+ * Uses JavaScript's default Unicode casing without locale-specific rules.
6280
+ * Changing case can change the length, so input brands are not retained.
6281
+ *
6282
+ * ### Example
6283
+ *
6284
+ * ```ts
6285
+ * import { assertEqual, assertType, uncapitalize } from "@evolu/common";
6286
+ *
6287
+ * const text = uncapitalize("Hello WORLD");
6288
+ * assertEqual(text, "hello WORLD");
6289
+ * assertType<typeof text, "hello WORLD">();
6290
+ * assertEqual(uncapitalize(text), text);
6291
+ * ```
6292
+ *
6293
+ * @group String
6294
+ */
6295
+ export const uncapitalize = <S extends string>(value: S): Uncapitalize<S> => {
6296
+ const [first = ""] = value;
6297
+ return (first.toLowerCase() + value.slice(first.length)) as Uncapitalize<S>;
6298
+ };
6299
+
6300
+ /**
6301
+ * Adds uppercased text validation to an existing string Type.
6302
+ *
6303
+ * Narrows the output to TypeScript's `Uppercase<string>` while preserving the
6304
+ * parent Type's constraints. Validation leaves the text unchanged. Use
6305
+ * {@link uppercase} to change its casing.
6306
+ *
6307
+ * ### Example
6308
+ *
6309
+ * ```ts
6310
+ * import {
6311
+ * assertErr,
6312
+ * assertOk,
6313
+ * uppercased,
6314
+ * maxLength,
6315
+ * String,
6316
+ * } from "@evolu/common";
6317
+ *
6318
+ * const Label = uppercased(maxLength(50)(String));
6319
+ * assertOk(Label.fromUnknown("HELLO WORLD"), "HELLO WORLD");
6320
+ * assertErr(Label.fromUnknown("Hello world"));
6321
+ * ```
6322
+ *
6323
+ * @group String
6324
+ */
6325
+ export const uppercased = <
6326
+ ParentType extends ConcreteTypeNode & { readonly Output: string },
6327
+ >(
6328
+ parent: ValidateBrandParent<"Uppercased", ParentType>,
6329
+ ): ReturnType<
6330
+ typeof createType<
6331
+ "Uppercased",
6332
+ ParentType,
6333
+ ParentType["Output"] & Uppercase<string>,
6334
+ UppercasedError
6335
+ >
6336
+ > =>
6337
+ createType<
6338
+ "Uppercased",
6339
+ ParentType,
6340
+ ParentType["Output"] & Uppercase<string>,
6341
+ UppercasedError
6342
+ >(
6343
+ "Uppercased",
6344
+ parent,
6345
+ (value) =>
6346
+ value === uppercase(value)
6347
+ ? ok(value as ParentType["Output"] & Uppercase<string>)
6348
+ : err({ type: "Uppercased", value }),
6349
+ (error) =>
6350
+ `The value ${safelyStringifyUnknownValue(error.value)} must be uppercased.`,
6351
+ );
6352
+
6353
+ /**
6354
+ * Error returned when {@link uppercased} rejects a string.
6355
+ *
6356
+ * @group String
6357
+ */
6358
+ export interface UppercasedError extends TypeError<"Uppercased"> {
6359
+ readonly value: string;
6360
+ }
6361
+
6362
+ /**
6363
+ * Validates uppercased text as TypeScript's `Uppercase<string>`.
6364
+ *
6365
+ * Checks the whole string using JavaScript's Unicode uppercase mapping. Empty
6366
+ * strings and uncased characters, such as digits and emoji, are valid. Use
6367
+ * {@link uppercase} to produce an uppercased value from any string.
6368
+ *
6369
+ * ### Example
6370
+ *
6371
+ * ```ts
6372
+ * import { assertErr, assertOk, UppercasedString } from "@evolu/common";
6373
+ *
6374
+ * const text: UppercasedString = "HELLO WORLD";
6375
+ * assertOk(UppercasedString.fromUnknown(text), text);
6376
+ * assertErr(UppercasedString.fromUnknown("Hello world"));
6377
+ * assertOk(UppercasedString.fromUnknown(""), "");
6378
+ * ```
6379
+ *
6380
+ * @group String
6381
+ */
6382
+ export const UppercasedString = /*#__PURE__*/ uppercased(String);
6383
+ export type UppercasedString = typeof UppercasedString.Output;
6384
+
6385
+ /**
6386
+ * Uppercases the whole string and returns a {@link UppercasedString}.
6387
+ *
6388
+ * Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
6389
+ * without locale-specific rules. Changing case can change the length, so input
6390
+ * brands are not retained.
6391
+ *
6392
+ * ### Example
6393
+ *
6394
+ * ```ts
6395
+ * import { assertEqual, assertType, uppercase } from "@evolu/common";
6396
+ *
6397
+ * const text = uppercase("Hello world");
6398
+ * assertEqual(text, "HELLO WORLD");
6399
+ * assertType<typeof text, "HELLO WORLD">();
6400
+ * assertEqual(uppercase(text), text);
6401
+ * ```
6402
+ *
6403
+ * @group String
6404
+ */
6405
+ export const uppercase = <S extends string>(value: S): Uppercase<S> =>
6406
+ value.toUpperCase() as Uppercase<S>;
6407
+
6408
+ /**
6409
+ * Adds lowercased text validation to an existing string Type.
6410
+ *
6411
+ * Narrows the output to TypeScript's `Lowercase<string>` while preserving the
6412
+ * parent Type's constraints. Validation leaves the text unchanged. Use
6413
+ * {@link lowercase} to change its casing.
6414
+ *
6415
+ * ### Example
6416
+ *
6417
+ * ```ts
6418
+ * import {
6419
+ * assertErr,
6420
+ * assertOk,
6421
+ * lowercased,
6422
+ * maxLength,
6423
+ * String,
6424
+ * } from "@evolu/common";
6425
+ *
6426
+ * const Label = lowercased(maxLength(50)(String));
6427
+ * assertOk(Label.fromUnknown("hello world"), "hello world");
6428
+ * assertErr(Label.fromUnknown("Hello WORLD"));
6429
+ * ```
6430
+ *
6431
+ * @group String
6432
+ */
6433
+ export const lowercased = <
6434
+ ParentType extends ConcreteTypeNode & { readonly Output: string },
6435
+ >(
6436
+ parent: ValidateBrandParent<"Lowercased", ParentType>,
6437
+ ): ReturnType<
6438
+ typeof createType<
6439
+ "Lowercased",
6440
+ ParentType,
6441
+ ParentType["Output"] & Lowercase<string>,
6442
+ LowercasedError
6443
+ >
6444
+ > =>
6445
+ createType<
6446
+ "Lowercased",
6447
+ ParentType,
6448
+ ParentType["Output"] & Lowercase<string>,
6449
+ LowercasedError
6450
+ >(
6451
+ "Lowercased",
6452
+ parent,
6453
+ (value) =>
6454
+ value === lowercase(value)
6455
+ ? ok(value as ParentType["Output"] & Lowercase<string>)
6456
+ : err({ type: "Lowercased", value }),
6457
+ (error) =>
6458
+ `The value ${safelyStringifyUnknownValue(error.value)} must be lowercased.`,
6459
+ );
6460
+
6461
+ /**
6462
+ * Error returned when {@link lowercased} rejects a string.
6463
+ *
6464
+ * @group String
6465
+ */
6466
+ export interface LowercasedError extends TypeError<"Lowercased"> {
6467
+ readonly value: string;
6468
+ }
6469
+
6470
+ /**
6471
+ * Validates lowercased text as TypeScript's `Lowercase<string>`.
6472
+ *
6473
+ * Checks the whole string using JavaScript's Unicode lowercase mapping. Empty
6474
+ * strings and uncased characters, such as digits and emoji, are valid. Use
6475
+ * {@link lowercase} to produce a lowercased value from any string.
6476
+ *
6477
+ * ### Example
6478
+ *
6479
+ * ```ts
6480
+ * import { assertErr, assertOk, LowercasedString } from "@evolu/common";
6481
+ *
6482
+ * const text: LowercasedString = "hello world";
6483
+ * assertOk(LowercasedString.fromUnknown(text), text);
6484
+ * assertErr(LowercasedString.fromUnknown("Hello WORLD"));
6485
+ * assertOk(LowercasedString.fromUnknown(""), "");
6486
+ * ```
6487
+ *
6488
+ * @group String
6489
+ */
6490
+ export const LowercasedString = /*#__PURE__*/ lowercased(String);
6491
+ export type LowercasedString = typeof LowercasedString.Output;
6492
+
6493
+ /**
6494
+ * Lowercases the whole string and returns a {@link LowercasedString}.
6495
+ *
6496
+ * Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
6497
+ * without locale-specific rules. Changing case can change the length, so input
6498
+ * brands are not retained.
6499
+ *
6500
+ * ### Example
6501
+ *
6502
+ * ```ts
6503
+ * import { assertEqual, assertType, lowercase } from "@evolu/common";
6504
+ *
6505
+ * const text = lowercase("Hello WORLD");
6506
+ * assertEqual(text, "hello world");
6507
+ * assertType<typeof text, "hello world">();
6508
+ * assertEqual(lowercase(text), text);
6509
+ * ```
6510
+ *
6511
+ * @group String
6512
+ */
6513
+ export const lowercase = <S extends string>(value: S): Lowercase<S> =>
6514
+ value.toLowerCase() as Lowercase<S>;
6515
+
6516
+ /**
6517
+ * String {@link Brand} without surrounding whitespace.
6518
+ *
6519
+ * ### Example
6520
+ *
6521
+ * ```ts
5340
6522
  * import { assertOk, String, trimmed } from "@evolu/common";
5341
6523
  *
5342
- * const Trimmed = trimmed(String);
6524
+ * const Trimmed = trimmed(String);
6525
+ *
6526
+ * assertOk(Trimmed.fromUnknown("Evolu"), "Evolu");
6527
+ * ```
6528
+ *
6529
+ * @group String
6530
+ */
6531
+ export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
6532
+ parent,
6533
+ ) =>
6534
+ brand(
6535
+ "Trimmed",
6536
+ parent,
6537
+ (value) =>
6538
+ value === value.trim()
6539
+ ? ok()
6540
+ : err<TrimmedError>({ type: "Trimmed", value }),
6541
+ (error) =>
6542
+ `The value ${safelyStringifyUnknownValue(error.value)} must be trimmed.`,
6543
+ );
6544
+
6545
+ /**
6546
+ * Error returned when {@link trimmed} rejects a string.
6547
+ *
6548
+ * @group String
6549
+ */
6550
+ export interface TrimmedError extends TypeError<"Trimmed"> {
6551
+ readonly value: string;
6552
+ }
6553
+
6554
+ /**
6555
+ * A {@link String} without surrounding whitespace.
6556
+ *
6557
+ * This Type validates that a string is already trimmed; it does not modify the
6558
+ * value. Use {@link trim} to normalize a string. Because an empty string is
6559
+ * valid, this Type is useful as an intermediate boundary for input controls
6560
+ * that trim their values before domain validation. If an empty string is
6561
+ * invalid, use {@link NonEmptyTrimmedString}.
6562
+ *
6563
+ * @group String
6564
+ */
6565
+ export const TrimmedString = /*#__PURE__*/ trimmed(String);
6566
+ export type TrimmedString = typeof TrimmedString.Output;
6567
+
6568
+ /**
6569
+ * Trims a string and returns a {@link TrimmedString}.
6570
+ *
6571
+ * ### Example
6572
+ *
6573
+ * ```ts
6574
+ * import { assertEqual, trim } from "@evolu/common";
6575
+ *
6576
+ * assertEqual(trim(" Evolu "), "Evolu");
6577
+ * ```
6578
+ *
6579
+ * @group String
6580
+ */
6581
+ export const trim = (value: string): TrimmedString =>
6582
+ value.trim() as TrimmedString;
6583
+
6584
+ /**
6585
+ * String {@link Brand} requiring an exact, case-sensitive prefix.
6586
+ *
6587
+ * Validation preserves the complete string, including the prefix. An empty
6588
+ * prefix accepts every string allowed by the parent Type. The prefix must be
6589
+ * one concrete string literal so different prefixes have distinct brands.
6590
+ *
6591
+ * ### Example
6592
+ *
6593
+ * ```ts
6594
+ * import {
6595
+ * assertEqual,
6596
+ * assertErr,
6597
+ * assertOk,
6598
+ * assertType,
6599
+ * maxLength,
6600
+ * startsWith,
6601
+ * String,
6602
+ * type Brand,
6603
+ * } from "@evolu/common";
6604
+ *
6605
+ * const EnvName = startsWith("APP_")(maxLength(64)(String));
5343
6606
  *
5344
- * assertOk(Trimmed.fromUnknown("Evolu"), "Evolu");
6607
+ * const name = EnvName.fromUnknown("APP_PORT");
6608
+ * assertOk(name, "APP_PORT");
6609
+ * assertType<
6610
+ * typeof name.value,
6611
+ * string & Brand<"MaxLength64"> & Brand<"StartsWithAPP_">
6612
+ * >();
6613
+ *
6614
+ * assertEqual(EnvName.to(name.value), "APP_PORT");
6615
+ *
6616
+ * assertErr(EnvName.fromUnknown("app_PORT"));
6617
+ * assertErr(EnvName.fromUnknown("APP_" + "X".repeat(61)));
5345
6618
  * ```
5346
6619
  *
5347
6620
  * @group String
5348
6621
  */
5349
- export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
5350
- parent,
5351
- ) =>
5352
- brand(
5353
- "Trimmed",
5354
- parent,
5355
- (value) =>
5356
- value === value.trim()
5357
- ? ok()
5358
- : err<TrimmedError>({ type: "Trimmed", value }),
5359
- (error) =>
5360
- `The value ${safelyStringifyUnknownValue(error.value)} must be trimmed.`,
5361
- );
6622
+ export const startsWith = <Prefix extends string>(
6623
+ prefix: Prefix & ValidateLiteral<Prefix>,
6624
+ ): BrandFactory<`StartsWith${Prefix}`, string, StartsWithError<Prefix>> => {
6625
+ const name = `StartsWith${prefix}` as const;
6626
+
6627
+ return (parent) =>
6628
+ brand(
6629
+ name,
6630
+ parent,
6631
+ (value) =>
6632
+ value.startsWith(prefix)
6633
+ ? ok()
6634
+ : err<StartsWithError<Prefix>>({
6635
+ type: name,
6636
+ value,
6637
+ prefix,
6638
+ }),
6639
+ (error) =>
6640
+ `The value ${safelyStringifyUnknownValue(error.value)} must start with ${safelyStringifyUnknownValue(error.prefix)}.`,
6641
+ );
6642
+ };
5362
6643
 
5363
6644
  /**
5364
- * Error returned when {@link trimmed} rejects a string.
6645
+ * Error returned when {@link startsWith} rejects a string.
5365
6646
  *
5366
6647
  * @group String
5367
6648
  */
5368
- export interface TrimmedError extends TypeError<"Trimmed"> {
6649
+ export interface StartsWithError<
6650
+ Prefix extends string = string,
6651
+ > extends TypeError<`StartsWith${Prefix}`> {
5369
6652
  readonly value: string;
6653
+ readonly prefix: Prefix;
5370
6654
  }
5371
6655
 
5372
6656
  /**
5373
- * A {@link String} without surrounding whitespace.
6657
+ * Decodes a prefixed string with another {@link Type} and restores the prefix
6658
+ * when encoding.
5374
6659
  *
5375
- * This Type validates that a string is already trimmed; it does not modify the
5376
- * value. Use {@link trim} to normalize a string. Because an empty string is
5377
- * valid, this Type is useful as an intermediate boundary for input controls
5378
- * that trim their values before domain validation. If an empty string is
5379
- * invalid, use {@link NonEmptyTrimmedString}.
6660
+ * Uses {@link startsWith} to validate an exact, case-sensitive prefix before
6661
+ * removing one occurrence. The wrapped Type validates and decodes the suffix;
6662
+ * its constraints apply to the suffix, and its Output is preserved. Encoding
6663
+ * prepends the prefix to the wrapped Type's canonical string representation. An
6664
+ * empty prefix leaves that representation unchanged.
5380
6665
  *
5381
- * @group String
5382
- */
5383
- export const TrimmedString = /*#__PURE__*/ trimmed(String);
5384
- export type TrimmedString = typeof TrimmedString.Output;
5385
-
5386
- /**
5387
- * Trims a string and returns a {@link TrimmedString}.
6666
+ * The prefix must be one concrete string literal. The wrapped Type must accept
6667
+ * a string Input and encode to strings; its Output can have another type, as
6668
+ * with {@link PortFromString}.
5388
6669
  *
5389
6670
  * ### Example
5390
6671
  *
5391
6672
  * ```ts
5392
- * import { assertEqual, trim } from "@evolu/common";
6673
+ * import {
6674
+ * assertEqual,
6675
+ * assertErr,
6676
+ * assertOk,
6677
+ * assertType,
6678
+ * ConstantCaseIdentifier,
6679
+ * prefixed,
6680
+ * PortFromString,
6681
+ * type Port,
6682
+ * } from "@evolu/common";
5393
6683
  *
5394
- * assertEqual(trim(" Evolu "), "Evolu");
6684
+ * const EnvName = prefixed("APP_")(ConstantCaseIdentifier);
6685
+ *
6686
+ * const name = EnvName.fromUnknown("APP_PORT");
6687
+ * assertOk(name, "PORT");
6688
+ * assertType<typeof name.value, ConstantCaseIdentifier>();
6689
+ *
6690
+ * assertEqual(EnvName.to(name.value), "APP_PORT");
6691
+ *
6692
+ * assertErr(EnvName.fromUnknown("OTHER_PORT"));
6693
+ * assertErr(EnvName.fromUnknown("APP_port"));
6694
+ *
6695
+ * const PortSetting = prefixed("port:")(PortFromString);
6696
+ *
6697
+ * const port = PortSetting.fromUnknown("port:04000");
6698
+ * assertOk(port, 4000);
6699
+ * assertType<typeof port.value, Port>();
6700
+ *
6701
+ * assertEqual(PortSetting.to(port.value), "port:4000");
5395
6702
  * ```
5396
6703
  *
5397
6704
  * @group String
5398
6705
  */
5399
- export const trim = (value: string): TrimmedString =>
5400
- value.trim() as TrimmedString;
6706
+ export const prefixed =
6707
+ <Prefix extends string>(prefix: Prefix & ValidateLiteral<Prefix>) =>
6708
+ <
6709
+ T extends ConcreteTypeNode & {
6710
+ readonly Input: string;
6711
+ readonly CanonicalInput: string;
6712
+ },
6713
+ >(
6714
+ type: T &
6715
+ ValidateOutput<T> &
6716
+ (string extends T["Input"] ? unknown : PrefixedInputTypeError),
6717
+ ): PrefixedType<Prefix, T> => {
6718
+ // A literal prefix makes the generated names concrete and distinct.
6719
+ const source = startsWith<Prefix>(prefix)(
6720
+ String as ValidateBrandParent<`StartsWith${Prefix}`, typeof String>,
6721
+ );
6722
+
6723
+ return transform<
6724
+ `Prefixed${Prefix}`,
6725
+ typeof source,
6726
+ T,
6727
+ typeof source.Output
6728
+ >(
6729
+ `Prefixed${prefix}` as ValidateChildTypeName<
6730
+ `Prefixed${Prefix}`,
6731
+ typeof source
6732
+ >,
6733
+ source,
6734
+ type,
6735
+ {
6736
+ // The Input guard permits any string; concatenation establishes the prefix brand.
6737
+ from: (value) => ok(value.slice(prefix.length) as T["Input"]),
6738
+ to: (value) => `${prefix}${value}` as typeof source.Output,
6739
+ },
6740
+ );
6741
+ };
6742
+
6743
+ /**
6744
+ * The {@link Type} returned by {@link prefixed}.
6745
+ *
6746
+ * @group String
6747
+ */
6748
+ export interface PrefixedType<
6749
+ Prefix extends string,
6750
+ T extends TypeNode,
6751
+ > extends TransformType<
6752
+ BrandType<typeof String, `StartsWith${Prefix}`, StartsWithError<Prefix>>,
6753
+ T,
6754
+ `Prefixed${Prefix}`,
6755
+ never
6756
+ > {}
6757
+
6758
+ type PrefixedInputTypeError = CompileTimeError<
6759
+ "Type",
6760
+ "Prefixed Type Input must accept every string."
6761
+ >;
5401
6762
 
5402
6763
  /**
5403
6764
  * Minimum-length {@link Brand} for values whose `length` is at least `min`.
@@ -5611,6 +6972,7 @@ export interface LengthError<
5611
6972
  * assertType<UrlSafeString, string & Brand<"UrlSafeString">>();
5612
6973
  *
5613
6974
  * assertOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
6975
+ *
5614
6976
  * const invalid = UrlSafeString.fromUnknown("not safe");
5615
6977
  * assertErr(invalid);
5616
6978
  * assertType(Data, invalid.error);
@@ -5803,7 +7165,13 @@ export const base64UrlToUint8Array = (value: Base64Url): Uint8Array =>
5803
7165
  base64UrlStringToUint8Array(value);
5804
7166
 
5805
7167
  /**
5806
- * A non-empty URL-safe name containing at most 64 UTF-16 code units.
7168
+ * A non-empty file-system-safe and URL-safe token of at most 64 UTF-16 code
7169
+ * units.
7170
+ *
7171
+ * Evolu uses it for database file names, storage pool names, and log prefixes.
7172
+ * It accepts the {@link UrlSafeString} alphabet in any order, so it may start
7173
+ * with a digit, `-`, or `_`. It is not a language identifier; for
7174
+ * word-structured names, use {@link identifier}.
5807
7175
  *
5808
7176
  * @group String
5809
7177
  */
@@ -6593,6 +7961,7 @@ export type PositiveFiniteNumber = typeof PositiveFiniteNumber.Output;
6593
7961
  * assertType<Int, number & Brand<"Int">>();
6594
7962
  *
6595
7963
  * assertOk(Int.fromUnknown(42), 42);
7964
+ *
6596
7965
  * const invalid = Int.fromUnknown(1.5);
6597
7966
  * assertErr(invalid);
6598
7967
  * assertType(Data, invalid.error);
@@ -6657,6 +8026,68 @@ export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
6657
8026
  export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
6658
8027
  export type PositiveInt = typeof PositiveInt.Output;
6659
8028
 
8029
+ /**
8030
+ * Error returned when a string is not a decimal integer.
8031
+ *
8032
+ * @group Number
8033
+ */
8034
+ export interface IntFromStringError extends TypeError<"IntFromString"> {
8035
+ readonly value: string;
8036
+ }
8037
+
8038
+ /**
8039
+ * Transforms a decimal integer string into an {@link Int}.
8040
+ *
8041
+ * This is useful for inputs that carry numbers as text, such as environment
8042
+ * variables, URL query parameters, and form fields. The string must consist of
8043
+ * an optional minus sign and digits; the {@link Int} constraint then rejects
8044
+ * values outside the safe integer range.
8045
+ *
8046
+ * ### Example
8047
+ *
8048
+ * ```ts
8049
+ * import {
8050
+ * assertEqual,
8051
+ * assertErr,
8052
+ * assertOk,
8053
+ * assertSame,
8054
+ * IntFromString,
8055
+ * } from "@evolu/common";
8056
+ *
8057
+ * assertOk(IntFromString.fromUnknown("4000"), 4000);
8058
+ * assertOk(IntFromString.fromUnknown("-1"), -1);
8059
+ * assertEqual(IntFromString.to(IntFromString.orThrow("42")), "42");
8060
+ *
8061
+ * const negativeZero = IntFromString.orThrow("-0");
8062
+ * assertSame(negativeZero, -0);
8063
+ * assertEqual(IntFromString.to(negativeZero), "-0");
8064
+ *
8065
+ * const invalid = IntFromString.fromUnknown("4000.5");
8066
+ * assertErr(invalid, { type: "IntFromString", value: "4000.5" });
8067
+ * assertEqual(
8068
+ * IntFromString.formatError(invalid.error),
8069
+ * 'The value "4000.5" is not a decimal integer.',
8070
+ * );
8071
+ * ```
8072
+ *
8073
+ * @group Number
8074
+ */
8075
+ export const IntFromString = /*#__PURE__*/ transform(
8076
+ "IntFromString",
8077
+ String,
8078
+ Int,
8079
+ {
8080
+ from: (value): Result<number, IntFromStringError> =>
8081
+ /^-?\d+$/u.test(value)
8082
+ ? ok(globalThis.Number(value))
8083
+ : err({ type: "IntFromString", value }),
8084
+ to: (value) =>
8085
+ globalThis.Object.is(value, -0) ? "-0" : globalThis.String(value),
8086
+ },
8087
+ (error) =>
8088
+ `The value ${safelyStringifyUnknownValue(error.value)} is not a decimal integer.`,
8089
+ );
8090
+
6660
8091
  /**
6661
8092
  * Minimum {@link PositiveInt} value.
6662
8093
  *
@@ -6898,6 +8329,65 @@ export interface LessThanOrEqualToError<
6898
8329
  readonly max: Max;
6899
8330
  }
6900
8331
 
8332
+ /**
8333
+ * A TCP or UDP port as an integer from zero through 65535, inclusive.
8334
+ *
8335
+ * When binding a server, zero requests an automatically assigned port. Use
8336
+ * {@link PortFromString} for configuration values supplied as text.
8337
+ *
8338
+ * ### Example
8339
+ *
8340
+ * ```ts
8341
+ * import { assertErr, assertOk, Port } from "@evolu/common";
8342
+ *
8343
+ * assertOk(Port.fromUnknown(0), 0);
8344
+ * assertOk(Port.fromUnknown(4000), 4000);
8345
+ * assertOk(Port.fromUnknown(65535), 65535);
8346
+ * assertErr(Port.fromUnknown(-1));
8347
+ * assertErr(Port.fromUnknown(65536));
8348
+ * assertErr(Port.fromUnknown(4000.5));
8349
+ * ```
8350
+ *
8351
+ * @group Number
8352
+ */
8353
+ export const Port = /*#__PURE__*/ brand(
8354
+ "Port",
8355
+ /*#__PURE__*/ lessThanOrEqualTo(65535)(NonNegativeInt),
8356
+ );
8357
+ export type Port = typeof Port.Output;
8358
+
8359
+ /**
8360
+ * Parses a decimal integer string and validates it as a {@link Port}.
8361
+ *
8362
+ * Uses {@link IntFromString} for decimal parsing, including its rejection of
8363
+ * whitespace, plus signs, fractions, and exponent notation.
8364
+ *
8365
+ * ### Example
8366
+ *
8367
+ * ```ts
8368
+ * import {
8369
+ * assertEqual,
8370
+ * assertErr,
8371
+ * assertOk,
8372
+ * PortFromString,
8373
+ * } from "@evolu/common";
8374
+ *
8375
+ * assertOk(PortFromString.fromUnknown("0"), 0);
8376
+ * assertOk(PortFromString.fromUnknown("4000"), 4000);
8377
+ * assertErr(PortFromString.fromUnknown("65536"));
8378
+ * assertErr(PortFromString.fromUnknown("http"));
8379
+ * assertEqual(PortFromString.to(PortFromString.orThrow("04000")), "4000");
8380
+ * ```
8381
+ *
8382
+ * @group Number
8383
+ */
8384
+ export const PortFromString = /*#__PURE__*/ transform(
8385
+ "PortFromString",
8386
+ IntFromString,
8387
+ Port,
8388
+ { from: ok, to: identity },
8389
+ );
8390
+
6901
8391
  /**
6902
8392
  * Finite {@link Number} from zero to one, inclusive.
6903
8393
  *
@@ -7248,6 +8738,7 @@ export type NegativeDecimalString = typeof NegativeDecimalString.Output;
7248
8738
  * >();
7249
8739
  *
7250
8740
  * assertOk(Tenths.fromUnknown(0.3), 0.3);
8741
+ *
7251
8742
  * const invalid = Tenths.fromUnknown(0.31);
7252
8743
  * assertErr(invalid);
7253
8744
  * assertType(Data, invalid.error);
@@ -7476,6 +8967,7 @@ export interface BetweenError<
7476
8967
  *
7477
8968
  * const UserId = brand("UserId", String);
7478
8969
  * const UserIds = array(UserId);
8970
+ *
7479
8971
  * const result = UserIds.from.parent(["ada", "grace"]);
7480
8972
  *
7481
8973
  * assertType<
@@ -7484,6 +8976,7 @@ export interface BetweenError<
7484
8976
  * >();
7485
8977
  * assertOk(result, ["ada", "grace"]);
7486
8978
  * assertOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
8979
+ *
7487
8980
  * const invalid = UserIds.fromUnknown("ada");
7488
8981
  * assertErr(invalid);
7489
8982
  * assertType(Data, invalid.error);
@@ -9238,7 +10731,7 @@ const validateTupleItems = (
9238
10731
  );
9239
10732
 
9240
10733
  /**
9241
- * Decimal digit from `"0"` to `"9"`.
10734
+ * Decimal integer string from `"0"` to `"9"`.
9242
10735
  *
9243
10736
  * @group String
9244
10737
  */
@@ -9257,7 +10750,7 @@ export const Digit = /*#__PURE__*/ union(
9257
10750
  export type Digit = typeof Digit.Output;
9258
10751
 
9259
10752
  /**
9260
- * Decimal digit from `"1"` to `"9"`.
10753
+ * Decimal integer string from `"1"` to `"9"`.
9261
10754
  *
9262
10755
  * @group String
9263
10756
  */
@@ -9275,7 +10768,7 @@ export const Digit1To9 = /*#__PURE__*/ union(
9275
10768
  export type Digit1To9 = typeof Digit1To9.Output;
9276
10769
 
9277
10770
  /**
9278
- * Decimal string from `"1"` to `"6"`.
10771
+ * Decimal integer string from `"1"` to `"6"`.
9279
10772
  *
9280
10773
  * @group String
9281
10774
  */
@@ -9283,11 +10776,29 @@ export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
9283
10776
  export type Digit1To6 = typeof Digit1To6.Output;
9284
10777
 
9285
10778
  /**
9286
- * Decimal string from `"1"` to `"23"`.
10779
+ * Decimal integer string from `"1"` to `"23"`, without a leading zero.
9287
10780
  *
9288
10781
  * @group String
9289
10782
  */
9290
- export const Digit1To23 = /*#__PURE__*/ union(
10783
+ export const Digit1To23: UnionType<
10784
+ readonly [
10785
+ typeof Digit1To9,
10786
+ TemplateLiteralType<readonly ["1", typeof Digit]>,
10787
+ TemplateLiteralType<
10788
+ readonly [
10789
+ "2",
10790
+ UnionType<
10791
+ readonly [
10792
+ LiteralType<"0">,
10793
+ LiteralType<"1">,
10794
+ LiteralType<"2">,
10795
+ LiteralType<"3">,
10796
+ ]
10797
+ >,
10798
+ ]
10799
+ >,
10800
+ ]
10801
+ > = /*#__PURE__*/ union(
9291
10802
  Digit1To9,
9292
10803
  /*#__PURE__*/ templateLiteral("1", Digit),
9293
10804
  /*#__PURE__*/ templateLiteral("2", /*#__PURE__*/ union("0", "1", "2", "3")),
@@ -9295,11 +10806,31 @@ export const Digit1To23 = /*#__PURE__*/ union(
9295
10806
  export type Digit1To23 = typeof Digit1To23.Output;
9296
10807
 
9297
10808
  /**
9298
- * Decimal string from `"1"` to `"51"`.
10809
+ * Decimal integer string from `"1"` to `"51"`, without a leading zero.
9299
10810
  *
9300
10811
  * @group String
9301
10812
  */
9302
- export const Digit1To51 = /*#__PURE__*/ union(
10813
+ export const Digit1To51: UnionType<
10814
+ readonly [
10815
+ typeof Digit1To9,
10816
+ TemplateLiteralType<
10817
+ readonly [
10818
+ UnionType<
10819
+ readonly [
10820
+ LiteralType<"1">,
10821
+ LiteralType<"2">,
10822
+ LiteralType<"3">,
10823
+ LiteralType<"4">,
10824
+ ]
10825
+ >,
10826
+ typeof Digit,
10827
+ ]
10828
+ >,
10829
+ TemplateLiteralType<
10830
+ readonly ["5", UnionType<readonly [LiteralType<"0">, LiteralType<"1">]>]
10831
+ >,
10832
+ ]
10833
+ > = /*#__PURE__*/ union(
9303
10834
  Digit1To9,
9304
10835
  /*#__PURE__*/ templateLiteral(/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
9305
10836
  /*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")),
@@ -9307,22 +10838,45 @@ export const Digit1To51 = /*#__PURE__*/ union(
9307
10838
  export type Digit1To51 = typeof Digit1To51.Output;
9308
10839
 
9309
10840
  /**
9310
- * Decimal string from `"1"` to `"99"`.
10841
+ * Decimal integer string from `"1"` to `"99"`, without a leading zero.
9311
10842
  *
9312
10843
  * @group String
9313
10844
  */
9314
- export const Digit1To99 = /*#__PURE__*/ union(
10845
+ export const Digit1To99: UnionType<
10846
+ readonly [
10847
+ typeof Digit1To9,
10848
+ TemplateLiteralType<readonly [typeof Digit1To9, typeof Digit]>,
10849
+ ]
10850
+ > = /*#__PURE__*/ union(
9315
10851
  Digit1To9,
9316
10852
  /*#__PURE__*/ templateLiteral(Digit1To9, Digit),
9317
10853
  );
9318
10854
  export type Digit1To99 = typeof Digit1To99.Output;
9319
10855
 
9320
10856
  /**
9321
- * Decimal string from `"1"` to `"59"`.
10857
+ * Decimal integer string from `"1"` to `"59"`, without a leading zero.
9322
10858
  *
9323
10859
  * @group String
9324
10860
  */
9325
- export const Digit1To59 = /*#__PURE__*/ union(
10861
+ export const Digit1To59: UnionType<
10862
+ readonly [
10863
+ typeof Digit1To9,
10864
+ TemplateLiteralType<
10865
+ readonly [
10866
+ UnionType<
10867
+ readonly [
10868
+ LiteralType<"1">,
10869
+ LiteralType<"2">,
10870
+ LiteralType<"3">,
10871
+ LiteralType<"4">,
10872
+ LiteralType<"5">,
10873
+ ]
10874
+ >,
10875
+ typeof Digit,
10876
+ ]
10877
+ >,
10878
+ ]
10879
+ > = /*#__PURE__*/ union(
9326
10880
  Digit1To9,
9327
10881
  /*#__PURE__*/ templateLiteral(
9328
10882
  /*#__PURE__*/ union("1", "2", "3", "4", "5"),
@@ -9645,6 +11199,7 @@ export { _Object as Object };
9645
11199
  * const valueType = typeof value;
9646
11200
  *
9647
11201
  * assertEqual(valueType, "function");
11202
+ *
9648
11203
  * const called = trySync(
9649
11204
  * () => {
9650
11205
  * if (value !== undefined) value.toFixed(0);
@@ -9680,6 +11235,7 @@ export { _Object as Object };
9680
11235
  * const valueType = typeof value;
9681
11236
  *
9682
11237
  * assertEqual(valueType, "function");
11238
+ *
9683
11239
  * const called = trySync(
9684
11240
  * () => {
9685
11241
  * if (value !== undefined) value.toFixed(0);
@@ -10290,28 +11846,389 @@ export interface OptionalProperty<T extends TypeNode> {
10290
11846
  * });
10291
11847
  * ```
10292
11848
  *
10293
- * @group Objects
11849
+ * @group Objects
11850
+ */
11851
+ export const optional = <T extends TypeNode>(
11852
+ type: ValidateOptionalPropertyType<T>,
11853
+ ): OptionalProperty<T> => createOptionalProperty(type as T);
11854
+
11855
+ declare const optionalPropertySymbolType: unique symbol;
11856
+ const optionalPropertySymbol: typeof optionalPropertySymbolType =
11857
+ /*#__PURE__*/ globalThis.Symbol() as typeof optionalPropertySymbolType;
11858
+
11859
+ const createOptionalProperty = <T extends TypeNode>(
11860
+ type: T,
11861
+ ): OptionalProperty<T> =>
11862
+ ({
11863
+ type,
11864
+ [optionalPropertySymbol]: true,
11865
+ }) as OptionalProperty<T>;
11866
+
11867
+ type ValidateOptionalPropertyType<T extends TypeNode> =
11868
+ IsUnion<T> extends false
11869
+ ? T extends ConcreteTypeNode
11870
+ ? T
11871
+ : ObjectPropertyTypeError
11872
+ : ObjectPropertyTypeError;
11873
+
11874
+ /**
11875
+ * A value decoded by {@link withDefault} with the `preserve` strategy.
11876
+ *
11877
+ * `defaultUsed` distinguishes supplied values from defaults, even when their
11878
+ * values are equal. `original` records the decoded absence, not the raw input
11879
+ * before other transformations.
11880
+ *
11881
+ * @group Construction
11882
+ */
11883
+ export type Defaulted<
11884
+ Value,
11885
+ Default = Value,
11886
+ Original extends "missing" | "null" | "undefined" =
11887
+ "missing" | "null" | "undefined",
11888
+ > =
11889
+ | { readonly value: Value; readonly defaultUsed: false }
11890
+ | {
11891
+ readonly value: Default;
11892
+ readonly defaultUsed: true;
11893
+ readonly original: Original;
11894
+ };
11895
+
11896
+ /**
11897
+ * The value Type returned by {@link withDefault}.
11898
+ *
11899
+ * @group Construction
11900
+ */
11901
+ export interface WithDefaultType<
11902
+ T extends TypeNode,
11903
+ Value,
11904
+ Strategy extends "replace" | "preserve",
11905
+ > extends Type<
11906
+ "WithDefault",
11907
+ T["Input"],
11908
+ WithDefaultOutput<T, Value, Strategy>,
11909
+ never,
11910
+ T,
11911
+ InferErrors<T>,
11912
+ ChildCustomFrom<T, WithDefaultOutput<T, Value, Strategy>, never>,
11913
+ T["CanonicalInput"],
11914
+ false
11915
+ > {}
11916
+
11917
+ /**
11918
+ * An optional input property made required in the Output by {@link withDefault}.
11919
+ *
11920
+ * @group Objects
11921
+ */
11922
+ export interface WithDefaultProperty<
11923
+ T extends TypeNode,
11924
+ Value,
11925
+ Strategy extends "replace" | "preserve",
11926
+ > {
11927
+ readonly type: T;
11928
+ readonly value: Value;
11929
+ readonly strategy: Strategy;
11930
+ /** @ignore */
11931
+ readonly [errorsSymbol]: InferErrors<T>;
11932
+ /** @ignore */
11933
+ readonly [defaultPropertySymbol]: RuntimeDefaultOperations;
11934
+ }
11935
+
11936
+ /**
11937
+ * Supplies a decoded default for absence accepted by another {@link Type}.
11938
+ *
11939
+ * You might not need this: use `??` where a value is consumed if the default
11940
+ * does not need to become part of the decoded data. By default, replacement
11941
+ * loses the distinction between absence and an explicitly supplied value. Set
11942
+ * `strategy: "preserve"` to retain it.
11943
+ *
11944
+ * Defaults apply after decoding, to `null` and `undefined` accepted by the
11945
+ * wrapped Type, and to missing properties when wrapping {@link optional}. Types
11946
+ * whose Output excludes both `null` and `undefined` require `optional`;
11947
+ * otherwise there is no accepted absence to default. Invalid supplied values
11948
+ * still fail. An optional property does not accept an explicit `undefined`
11949
+ * unless its Type does. The default must be an Output of the wrapped Type; it
11950
+ * is checked when constructing the declaration. It may itself be `null` or
11951
+ * `undefined` when the wrapped Output accepts that value.
11952
+ *
11953
+ * Omitting `strategy` returns the effective value and encodes it as supplied
11954
+ * data. `strategy: "preserve"` returns {@link Defaulted} and restores absence
11955
+ * when encoding. A supplied value equal to the default still has `defaultUsed:
11956
+ * false`. Preserved defaults must equal the configured value: {@link Data}
11957
+ * values use structural equality, and other values use reference identity.
11958
+ * Every decoded absence reuses the configured default by reference; defaults
11959
+ * are not cloned. Treat default values as immutable, like other Type
11960
+ * declarations.
11961
+ *
11962
+ * Pass the default value as the second argument. Add `{ strategy: "preserve" }`
11963
+ * as the third argument to retain supplied-input evidence. Explicit `strategy:
11964
+ * "replace"` is not accepted.
11965
+ *
11966
+ * ### Example
11967
+ *
11968
+ * ```ts
11969
+ * import {
11970
+ * assertEqual,
11971
+ * assertErr,
11972
+ * assertOk,
11973
+ * Boolean,
11974
+ * nullOr,
11975
+ * object,
11976
+ * optional,
11977
+ * withDefault,
11978
+ * } from "@evolu/common";
11979
+ *
11980
+ * const Settings = object({
11981
+ * enabled: withDefault(optional(Boolean), true, {
11982
+ * strategy: "preserve",
11983
+ * }),
11984
+ * });
11985
+ *
11986
+ * const missing = Settings.fromUnknown({});
11987
+ * assertOk(missing, {
11988
+ * enabled: { value: true, defaultUsed: true, original: "missing" },
11989
+ * });
11990
+ * assertEqual(Settings.to(missing.value), {});
11991
+ *
11992
+ * assertOk(Settings.fromUnknown({ enabled: true }), {
11993
+ * enabled: { value: true, defaultUsed: false },
11994
+ * });
11995
+ * assertErr(Settings.fromUnknown({ enabled: undefined }));
11996
+ *
11997
+ * const Enabled = withDefault(nullOr(Boolean), true);
11998
+ *
11999
+ * assertOk(Enabled.fromUnknown(null), true);
12000
+ * assertEqual(Enabled.to(true), true);
12001
+ * ```
12002
+ *
12003
+ * @group Construction
10294
12004
  */
10295
- export const optional = <T extends TypeNode>(
10296
- type: ValidateOptionalPropertyType<T>,
10297
- ): OptionalProperty<T> => createOptionalProperty(type as T);
12005
+ export function withDefault<
12006
+ T extends TypeNode,
12007
+ const Value extends NoInfer<T["Output"]>,
12008
+ >(
12009
+ property: OptionalProperty<T> & {
12010
+ readonly type: ValidateDefaultType<T, true>;
12011
+ },
12012
+ value: Value,
12013
+ ): WithDefaultProperty<T, Value, "replace">;
12014
+ export function withDefault<
12015
+ T extends TypeNode,
12016
+ const Value extends NoInfer<T["Output"]>,
12017
+ >(
12018
+ property: OptionalProperty<T> & {
12019
+ readonly type: ValidateDefaultType<T, true>;
12020
+ },
12021
+ value: Value,
12022
+ options: { readonly strategy: "preserve" },
12023
+ ): WithDefaultProperty<T, Value, "preserve">;
12024
+ export function withDefault<
12025
+ T extends TypeNode,
12026
+ const Value extends NoInfer<T["Output"]>,
12027
+ >(
12028
+ type: T & ValidateDefaultType<T>,
12029
+ value: Value,
12030
+ ): WithDefaultType<T, Value, "replace">;
12031
+ export function withDefault<
12032
+ T extends TypeNode,
12033
+ const Value extends NoInfer<T["Output"]>,
12034
+ >(
12035
+ type: T & ValidateDefaultType<T>,
12036
+ value: Value,
12037
+ options: { readonly strategy: "preserve" },
12038
+ ): WithDefaultType<T, Value, "preserve">;
12039
+ export function withDefault(
12040
+ property: TypeNode | OptionalProperty<TypeNode>,
12041
+ value: unknown,
12042
+ { strategy }: { readonly strategy?: string } = {},
12043
+ ): TypeNode | WithDefaultProperty<TypeNode, unknown, "replace" | "preserve"> {
12044
+ assert(
12045
+ strategy === undefined || strategy === "preserve",
12046
+ 'withDefault strategy must be omitted or "preserve".',
12047
+ );
12048
+ const optionalInput = optionalPropertySymbol in property;
12049
+ const type = (optionalInput ? property.type : property) as RuntimeTypeNode;
12050
+ assert(
12051
+ type.is(value),
12052
+ `withDefault value must be an Output of ${type.name}.`,
12053
+ );
12054
+ const originals = new Set([
12055
+ ...(optionalInput ? ["missing"] : []),
12056
+ ...(type.is(null) ? ["null"] : []),
12057
+ ...(type.is(undefined) ? ["undefined"] : []),
12058
+ ]);
12059
+ const supplied = createRootType(
12060
+ "DefaultSupplied",
12061
+ (candidate): Result<unknown, TypeError<"DefaultValue">> =>
12062
+ (strategy === undefined && globalThis.Object.is(candidate, value)) ||
12063
+ (candidate !== null && candidate !== undefined && type.is(candidate))
12064
+ ? ok(candidate)
12065
+ : err({ type: "DefaultValue" }),
12066
+ () =>
12067
+ "The value must be a non-nullish Output of its Type or, in replacement mode, the configured default.",
12068
+ );
12069
+ const defaultValue = createRootType(
12070
+ "DefaultValue",
12071
+ (candidate): Result<unknown, TypeError<"DefaultValue">> =>
12072
+ type.is(candidate) &&
12073
+ (globalThis.Object.is(candidate, value) ||
12074
+ (Data.is(value) && Data.is(candidate) && eqData(candidate, value)))
12075
+ ? ok(candidate)
12076
+ : err({ type: "DefaultValue" }),
12077
+ () => "A preserved default must equal the configured default value.",
12078
+ );
12079
+ const output = (strategy === undefined
12080
+ ? supplied
12081
+ : discriminatedUnion(
12082
+ "defaultUsed",
12083
+ object({ value: supplied, defaultUsed: literal(false) }),
12084
+ object({
12085
+ value: defaultValue,
12086
+ defaultUsed: literal(true),
12087
+ original: createRootType(
12088
+ "DefaultOriginal",
12089
+ (candidate): Result<string, TypeError<"DefaultOriginal">> =>
12090
+ typeof candidate === "string" && originals.has(candidate)
12091
+ ? ok(candidate)
12092
+ : err({ type: "DefaultOriginal" }),
12093
+ () =>
12094
+ "The original absence must be handled by this default declaration.",
12095
+ ),
12096
+ }),
12097
+ )) as unknown as RuntimeTypeNode;
12098
+ const ownGetTypeIssues = output[getRuntimeTypeIssuesSymbol];
12099
+ const outputWithIssues: RuntimeTypeNode = {
12100
+ ...output,
12101
+ [outputValidationSymbol]: (value: unknown, options?: ValidationOptions) => {
12102
+ const result = output[outputValidationSymbol](value, options);
12103
+ return result.ok
12104
+ ? result
12105
+ : err({ type: "WithDefault", outputError: result.error });
12106
+ },
12107
+ [getRuntimeTypeIssuesSymbol]: (error, mode) =>
12108
+ error.type === "WithDefault" && "outputError" in error
12109
+ ? ownGetTypeIssues(error.outputError as TypeError, mode)
12110
+ : type[getRuntimeTypeIssuesSymbol](error, mode),
12111
+ };
12112
+
12113
+ const operations: RuntimeDefaultOperations = {
12114
+ output: outputWithIssues,
12115
+ createObject: createDefaultObjectType,
12116
+ partial: (type) =>
12117
+ createOptionalProperty(
12118
+ strategy === "preserve"
12119
+ ? withDefault(type as unknown as typeof Unknown, value, {
12120
+ strategy,
12121
+ })
12122
+ : withDefault(type as unknown as typeof Unknown, value),
12123
+ ),
12124
+ decode: (candidate) => {
12125
+ const original =
12126
+ candidate === missingDefaultValue
12127
+ ? "missing"
12128
+ : candidate === null
12129
+ ? "null"
12130
+ : candidate === undefined
12131
+ ? "undefined"
12132
+ : undefined;
12133
+ const effective = original === undefined ? candidate : value;
12134
+ return strategy === undefined
12135
+ ? effective
12136
+ : original === undefined
12137
+ ? { value: effective, defaultUsed: false }
12138
+ : { value: effective, defaultUsed: true, original };
12139
+ },
12140
+ encode: (candidate: unknown) => {
12141
+ if (strategy === undefined) return candidate;
12142
+ const preserved = candidate as Defaulted<unknown>;
12143
+ if (!preserved.defaultUsed) return preserved.value;
12144
+ switch (preserved.original) {
12145
+ case "missing":
12146
+ return missingDefaultValue;
12147
+ case "null":
12148
+ return null;
12149
+ case "undefined":
12150
+ return undefined;
12151
+ }
12152
+ },
12153
+ };
10298
12154
 
10299
- const optionalPropertySymbol = /*#__PURE__*/ globalThis.Symbol();
12155
+ if (optionalInput) {
12156
+ return {
12157
+ type,
12158
+ value,
12159
+ strategy: strategy ?? "replace",
12160
+ [defaultPropertySymbol]: operations,
12161
+ } as unknown as WithDefaultProperty<
12162
+ TypeNode,
12163
+ unknown,
12164
+ "replace" | "preserve"
12165
+ >;
12166
+ }
10300
12167
 
10301
- const createOptionalProperty = <T extends TypeNode>(
10302
- type: T,
10303
- ): OptionalProperty<T> =>
10304
- ({
12168
+ const fromOwn = (value: unknown) => ok(operations.decode(value));
12169
+ const fromParent = mapRuntimeOperations(
12170
+ type[fromSymbol],
12171
+ (operation) => (value: never, options) =>
12172
+ flatMapResult(operation(value, options), fromOwn),
12173
+ );
12174
+
12175
+ return createTypeNode(
12176
+ "WithDefault",
10305
12177
  type,
10306
- [optionalPropertySymbol]: true,
10307
- }) as OptionalProperty<T>;
12178
+ (value, options) =>
12179
+ flatMapResult(type.fromUnknown(value, options), fromOwn),
12180
+ operations.output.is,
12181
+ operations.output[outputValidationSymbol],
12182
+ createFromOperation(fromParent),
12183
+ operations.encode,
12184
+ operations.output[getRuntimeTypeIssuesSymbol],
12185
+ );
12186
+ }
10308
12187
 
10309
- type ValidateOptionalPropertyType<T extends TypeNode> =
10310
- IsUnion<T> extends false
10311
- ? T extends ConcreteTypeNode
10312
- ? T
10313
- : ObjectPropertyTypeError
10314
- : ObjectPropertyTypeError;
12188
+ type ValidateDefaultType<
12189
+ T extends TypeNode,
12190
+ Optional extends boolean = false,
12191
+ > = ValidateOptionalPropertyType<T> &
12192
+ ([ChildTypeNameValidationError<"WithDefault", T>] extends [never]
12193
+ ? unknown
12194
+ : ChildTypeNameValidationError<"WithDefault", T>) &
12195
+ (Optional extends true
12196
+ ? unknown
12197
+ : [DefaultOriginal<T>] extends [never]
12198
+ ? CompileTimeError<
12199
+ "Type",
12200
+ "withDefault requires an optional property or a Type whose Output includes null or undefined."
12201
+ >
12202
+ : unknown);
12203
+
12204
+ type DefaultOriginal<T extends TypeNode> =
12205
+ | (null extends T["Output"] ? "null" : never)
12206
+ | (undefined extends T["Output"] ? "undefined" : never);
12207
+
12208
+ type WithDefaultOutput<
12209
+ T extends TypeNode,
12210
+ Value,
12211
+ Strategy extends "replace" | "preserve",
12212
+ Original extends "missing" | "null" | "undefined" = DefaultOriginal<T>,
12213
+ > = Strategy extends "replace"
12214
+ ? NonNullable<T["Output"]> | Value
12215
+ : Defaulted<NonNullable<T["Output"]>, Value, Original>;
12216
+
12217
+ declare const defaultPropertySymbolType: unique symbol;
12218
+ const defaultPropertySymbol: typeof defaultPropertySymbolType =
12219
+ /*#__PURE__*/ globalThis.Symbol() as typeof defaultPropertySymbolType;
12220
+ const missingDefaultValue = /*#__PURE__*/ globalThis.Symbol();
12221
+
12222
+ interface RuntimeDefaultOperations {
12223
+ readonly output: RuntimeTypeNode;
12224
+ readonly decode: (value: unknown) => unknown;
12225
+ readonly encode: (value: never) => unknown;
12226
+ readonly createObject: (
12227
+ props: ObjectProps,
12228
+ record: RuntimeRecordTypeNode | undefined,
12229
+ ) => ObjectTypeNode;
12230
+ readonly partial: (type: RuntimeTypeNode) => OptionalProperty<TypeNode>;
12231
+ }
10315
12232
 
10316
12233
  /**
10317
12234
  * Properties used to construct an {@link object} Type.
@@ -10319,6 +12236,15 @@ type ValidateOptionalPropertyType<T extends TypeNode> =
10319
12236
  * @group Objects
10320
12237
  */
10321
12238
  export type ObjectProps = Readonly<
12239
+ Record<
12240
+ string,
12241
+ | TypeNode
12242
+ | OptionalProperty<TypeNode>
12243
+ | WithDefaultProperty<TypeNode, unknown, "replace" | "preserve">
12244
+ >
12245
+ >;
12246
+
12247
+ type ObjectValueProps = Readonly<
10322
12248
  Record<string, TypeNode | OptionalProperty<TypeNode>>
10323
12249
  >;
10324
12250
 
@@ -10330,7 +12256,10 @@ type ObjectProperty = ObjectProps[string];
10330
12256
  * Use `object(props)` for objects with fixed property names. Properties are
10331
12257
  * required unless wrapped with {@link optional}. An optional property may be
10332
12258
  * absent, but a present value is still validated and does not implicitly accept
10333
- * `undefined`.
12259
+ * `undefined`. Wrapping an optional property with {@link withDefault} keeps the
12260
+ * input optional and makes the decoded output required. A default for a
12261
+ * nullable or undefined-accepting Type alone does not make the property
12262
+ * optional.
10334
12263
  *
10335
12264
  * Without a second argument, `fromUnknown` rejects additional properties. Pass
10336
12265
  * a {@link record} with the predefined {@link String} key Type to validate and
@@ -10418,6 +12347,7 @@ type ObjectProperty = ObjectProps[string];
10418
12347
  * const valueType = typeof value;
10419
12348
  *
10420
12349
  * assertEqual(valueType, "function");
12350
+ *
10421
12351
  * const called = trySync(
10422
12352
  * () => {
10423
12353
  * if (value !== undefined) value.toFixed(0);
@@ -10453,6 +12383,7 @@ type ObjectProperty = ObjectProps[string];
10453
12383
  * const valueType = typeof value;
10454
12384
  *
10455
12385
  * assertEqual(valueType, "function");
12386
+ *
10456
12387
  * const called = trySync(
10457
12388
  * () => {
10458
12389
  * if (value !== undefined) value.toFixed(0);
@@ -10468,7 +12399,7 @@ type ObjectProperty = ObjectProps[string];
10468
12399
  *
10469
12400
  * @group Objects
10470
12401
  */
10471
- export function object<const Props extends ObjectProps>(
12402
+ export function object<const Props extends ObjectValueProps>(
10472
12403
  props: Props,
10473
12404
  ...validation: [ObjectValidationError<Props>] extends [never]
10474
12405
  ? []
@@ -10493,6 +12424,7 @@ export function object<const Props extends ObjectProps>(
10493
12424
  * { authorization: String },
10494
12425
  * record(String, String),
10495
12426
  * );
12427
+ *
10496
12428
  * const result = RequestHeaders.fromUnknown({
10497
12429
  * authorization: "Bearer token",
10498
12430
  * "x-request-id": "request-1",
@@ -10506,7 +12438,7 @@ export function object<const Props extends ObjectProps>(
10506
12438
  * ```
10507
12439
  */
10508
12440
  export function object<
10509
- const Props extends ObjectProps,
12441
+ const Props extends ObjectValueProps,
10510
12442
  const Rest extends RecordTypeNode & ConcreteTypeNode,
10511
12443
  >(
10512
12444
  props: Props,
@@ -10525,6 +12457,34 @@ export function object<
10525
12457
  Props,
10526
12458
  Rest extends ObjectRecordTypeNode ? Rest : never
10527
12459
  >;
12460
+ /** Creates an Object Type with explicit defaults for optional inputs. */
12461
+ export function object<const Props extends ObjectProps>(
12462
+ props: Props,
12463
+ ...validation: [ObjectValidationError<Props>] extends [never]
12464
+ ? []
12465
+ : [ValidationFailure<ObjectValidationError<Props>>]
12466
+ ): ObjectType<Props>;
12467
+ /**
12468
+ * Creates an Object Type with explicit defaults and additional record
12469
+ * properties.
12470
+ */
12471
+ export function object<
12472
+ const Props extends ObjectProps,
12473
+ const Rest extends ObjectRecordTypeNode & ConcreteTypeNode,
12474
+ >(
12475
+ props: Props,
12476
+ record: Rest,
12477
+ ...validation: [
12478
+ ObjectValidationError<Props> | ObjectRecordValidationError<Props, Rest>,
12479
+ ] extends [never]
12480
+ ? []
12481
+ : [
12482
+ ValidationFailure<
12483
+ | ObjectValidationError<Props>
12484
+ | ObjectRecordValidationError<Props, Rest>
12485
+ >,
12486
+ ]
12487
+ ): ObjectType<Props, Rest>;
10528
12488
  export function object(props: ObjectProps, recordType?: unknown): TypeNode {
10529
12489
  return createObjectType(
10530
12490
  snapshotObjectProps(props),
@@ -10537,6 +12497,14 @@ const createObjectType = (
10537
12497
  recordType?: RuntimeRecordTypeNode,
10538
12498
  ): ObjectTypeNode => {
10539
12499
  const runtimeProps = props as Readonly<Record<string, RuntimeObjectProperty>>;
12500
+ const defaultProperty =
12501
+ globalThis.Object.values(runtimeProps).find(isDefaultProperty);
12502
+ if (defaultProperty) {
12503
+ return defaultProperty[defaultPropertySymbol].createObject(
12504
+ props,
12505
+ recordType,
12506
+ );
12507
+ }
10540
12508
  const keys = globalThis.Object.keys(runtimeProps);
10541
12509
 
10542
12510
  const validate = (
@@ -10933,6 +12901,100 @@ const createObjectType = (
10933
12901
  );
10934
12902
  };
10935
12903
 
12904
+ const createDefaultObjectType = (
12905
+ props: ObjectProps,
12906
+ recordType: RuntimeRecordTypeNode | undefined,
12907
+ ): ObjectTypeNode => {
12908
+ const inputProps = createMutableRecord<string, ObjectProperty>();
12909
+ const outputProps = createMutableRecord<string, ObjectProperty>();
12910
+ const defaults = new Map<string, RuntimeDefaultOperations>();
12911
+
12912
+ for (const [key, property] of globalThis.Object.entries(props)) {
12913
+ if (isDefaultProperty(property as RuntimeObjectProperty)) {
12914
+ const defaultProperty = property as WithDefaultProperty<
12915
+ TypeNode,
12916
+ unknown,
12917
+ "replace" | "preserve"
12918
+ >;
12919
+ inputProps[key] = createOptionalProperty(defaultProperty.type);
12920
+ outputProps[key] = defaultProperty[defaultPropertySymbol].output;
12921
+ defaults.set(key, defaultProperty[defaultPropertySymbol]);
12922
+ } else {
12923
+ inputProps[key] = property;
12924
+ const type = objectPropertyToType(property as RuntimeObjectProperty);
12925
+ const output = createRootType(
12926
+ type.name,
12927
+ type[outputValidationSymbol],
12928
+ type.formatError,
12929
+ type[getRuntimeTypeIssuesSymbol],
12930
+ );
12931
+ outputProps[key] =
12932
+ optionalPropertySymbol in property
12933
+ ? createOptionalProperty(output)
12934
+ : output;
12935
+ }
12936
+ }
12937
+
12938
+ const source = createObjectType(
12939
+ inputProps,
12940
+ recordType,
12941
+ ) as unknown as RuntimeTypeNode;
12942
+ // The object parent validates only its input structure and root value Types.
12943
+ const parent = getTerminalRuntimeNode(source);
12944
+ const output = createObjectType(
12945
+ outputProps,
12946
+ recordType,
12947
+ ) as unknown as RuntimeTypeNode;
12948
+ const finish = (value: unknown): Result<unknown> => {
12949
+ const result = createMutableRecord(
12950
+ value as Readonly<Record<string, unknown>>,
12951
+ );
12952
+ for (const [key, operations] of defaults) {
12953
+ result[key] = operations.decode(
12954
+ globalThis.Object.hasOwn(result, key)
12955
+ ? result[key]
12956
+ : missingDefaultValue,
12957
+ );
12958
+ }
12959
+ return ok(result);
12960
+ };
12961
+ const fromInput = getTerminalRuntimeNode(source[fromSymbol]);
12962
+ const fromParent: RuntimeOperation<Result<unknown, TypeError>> = (
12963
+ value: never,
12964
+ options,
12965
+ ) => flatMapResult(fromInput(value, options), finish);
12966
+ const to = (value: never): unknown => {
12967
+ const result = createMutableRecord(
12968
+ value as Readonly<Record<string, unknown>>,
12969
+ );
12970
+ for (const [key, operations] of defaults) {
12971
+ const original = operations.encode(result[key] as never);
12972
+ if (original === missingDefaultValue) delete result[key];
12973
+ else result[key] = original;
12974
+ }
12975
+ return source[encoderSymbol](result as never);
12976
+ };
12977
+
12978
+ return createTypeNode<ObjectTypeNode>(
12979
+ "Object",
12980
+ parent,
12981
+ (value, options) =>
12982
+ flatMapResult(source.fromUnknown(value, options), finish),
12983
+ output.is,
12984
+ output[outputValidationSymbol],
12985
+ createFromOperation(fromParent),
12986
+ to,
12987
+ createObjectRuntimeTypeIssues(
12988
+ source.formatError,
12989
+ props as Readonly<Record<string, RuntimeObjectProperty>>,
12990
+ recordType,
12991
+ ),
12992
+ recordType
12993
+ ? { props, record: recordType as unknown as RecordTypeNode }
12994
+ : { props },
12995
+ );
12996
+ };
12997
+
10936
12998
  // Read descriptors instead of spreading so accessors are not invoked,
10937
12999
  // non-enumerable declarations are retained, and later mutations are isolated.
10938
13000
  const snapshotObjectProps = (
@@ -10992,9 +13054,9 @@ type ObjectRecordPropertyValidationError<
10992
13054
  Props extends ObjectProps,
10993
13055
  ValueType extends TypeNode,
10994
13056
  > = [
10995
- ObjectPropertyType<Props[keyof Props]>["Input"],
10996
- ObjectPropertyType<Props[keyof Props]>["Output"],
10997
- ObjectPropertyType<Props[keyof Props]>["CanonicalInput"],
13057
+ DefaultableObjectPropertyType<Props[keyof Props]>["Input"],
13058
+ DefaultableObjectPropertyType<Props[keyof Props]>["Output"],
13059
+ DefaultableObjectPropertyType<Props[keyof Props]>["CanonicalInput"],
10998
13060
  ] extends [ValueType["Input"], ValueType["Output"], ValueType["CanonicalInput"]]
10999
13061
  ? never
11000
13062
  : ObjectRecordPropertyTypeError<Props, ValueType>;
@@ -11002,10 +13064,10 @@ type ObjectRecordPropertyValidationError<
11002
13064
  type ObjectRecordPropertyTypeError<
11003
13065
  Props extends ObjectProps,
11004
13066
  ValueType extends TypeNode,
11005
- > = [ObjectPropertyType<Props[keyof Props]>["Input"]] extends [
13067
+ > = [DefaultableObjectPropertyType<Props[keyof Props]>["Input"]] extends [
11006
13068
  ValueType["Input"],
11007
13069
  ]
11008
- ? [ObjectPropertyType<Props[keyof Props]>["Output"]] extends [
13070
+ ? [DefaultableObjectPropertyType<Props[keyof Props]>["Output"]] extends [
11009
13071
  ValueType["Output"],
11010
13072
  ]
11011
13073
  ? ObjectRecordCanonicalInputTypeError
@@ -11014,10 +13076,10 @@ type ObjectRecordPropertyTypeError<
11014
13076
 
11015
13077
  type ObjectPropertyValidationError<Property extends ObjectProperty> =
11016
13078
  IsUnion<Property> extends false
11017
- ? Property extends OptionalProperty<infer T extends TypeNode>
11018
- ? ObjectPropertyTypeValidationError<T>
11019
- : Property extends TypeNode
11020
- ? ObjectPropertyTypeValidationError<Property>
13079
+ ? Property extends TypeNode
13080
+ ? ObjectPropertyTypeValidationError<Property>
13081
+ : Property extends { readonly type: infer T extends TypeNode }
13082
+ ? ObjectPropertyTypeValidationError<T>
11021
13083
  : ObjectPropertyTypeError
11022
13084
  : ObjectPropertyTypeError;
11023
13085
 
@@ -11109,15 +13171,83 @@ type ObjectRecordCanonicalInputTypeError = CompileTimeError<
11109
13171
  export type ObjectType<
11110
13172
  Props extends ObjectProps,
11111
13173
  Rest extends ObjectRecordTypeNode | undefined = undefined,
13174
+ > = Props extends ObjectValueProps
13175
+ ? Rest extends ObjectRecordTypeNode
13176
+ ? ObjectWithRecordType<Props, Rest>
13177
+ : StrictObjectType<Props>
13178
+ : DefaultObjectType<Props, Rest>;
13179
+
13180
+ type ObjectValueType<
13181
+ Props extends ObjectValueProps,
13182
+ Rest extends ObjectRecordTypeNode | undefined,
11112
13183
  > = Rest extends ObjectRecordTypeNode
11113
13184
  ? ObjectWithRecordType<Props, Rest>
11114
13185
  : StrictObjectType<Props>;
11115
13186
 
13187
+ type DefaultableObjectPropertyType<Property extends ObjectProperty> =
13188
+ Property extends WithDefaultProperty<infer T, infer Value, infer Strategy>
13189
+ ? Type<
13190
+ "WithDefault",
13191
+ T["Input"],
13192
+ WithDefaultOutput<T, Value, Strategy, "missing" | DefaultOriginal<T>>,
13193
+ never,
13194
+ T,
13195
+ InferErrors<T>,
13196
+ never,
13197
+ T["CanonicalInput"],
13198
+ false
13199
+ >
13200
+ : Property extends TypeNode | OptionalProperty<TypeNode>
13201
+ ? ObjectPropertyType<Property>
13202
+ : never;
13203
+
13204
+ type DefaultInputProps<Props extends ObjectProps> = {
13205
+ readonly [Key in keyof Props]: Props[Key] extends WithDefaultProperty<
13206
+ infer T,
13207
+ unknown,
13208
+ "replace" | "preserve"
13209
+ >
13210
+ ? OptionalProperty<T>
13211
+ : Extract<Props[Key], TypeNode | OptionalProperty<TypeNode>>;
13212
+ };
13213
+
13214
+ type DefaultOutputProps<Props extends ObjectProps> = {
13215
+ readonly [Key in keyof Props]: Props[Key] extends WithDefaultProperty<
13216
+ infer T,
13217
+ infer Value,
13218
+ infer Strategy
13219
+ >
13220
+ ? Type<
13221
+ "DefaultValue",
13222
+ WithDefaultOutput<T, Value, Strategy, "missing" | DefaultOriginal<T>>,
13223
+ WithDefaultOutput<T, Value, Strategy, "missing" | DefaultOriginal<T>>,
13224
+ never
13225
+ >
13226
+ : Extract<Props[Key], TypeNode | OptionalProperty<TypeNode>>;
13227
+ };
13228
+
13229
+ type DefaultObjectType<
13230
+ Props extends ObjectProps,
13231
+ Rest extends ObjectRecordTypeNode | undefined,
13232
+ > = Type<
13233
+ "Object",
13234
+ ObjectValueType<DefaultInputProps<Props>, Rest>["Input"],
13235
+ ObjectValueType<DefaultOutputProps<Props>, Rest>["Output"],
13236
+ TypeFromError<ObjectValueType<DefaultInputProps<Props>, Rest>>,
13237
+ RootType<ObjectValueType<DefaultInputProps<Props>, Rest>>,
13238
+ InferErrors<ObjectValueType<DefaultInputProps<Props>, Rest>>,
13239
+ never,
13240
+ ObjectValueType<DefaultInputProps<Props>, Rest>["CanonicalInput"],
13241
+ false
13242
+ > & { readonly props: Readonly<Props> } & (Rest extends ObjectRecordTypeNode
13243
+ ? { readonly record: Rest }
13244
+ : {});
13245
+
11116
13246
  // `fromUnknown` can report Object root errors plus declared-property child,
11117
13247
  // missing, access, and excess/Record-rest errors. When Object has a parent, its
11118
13248
  // own `Error` contains only the child and rest errors remaining after that
11119
13249
  // parent validated the Object structure.
11120
- type StrictObjectType<Props extends ObjectProps> = Type<
13250
+ type StrictObjectType<Props extends ObjectValueProps> = Type<
11121
13251
  "Object",
11122
13252
  StrictObjectShape<Props, "Input">,
11123
13253
  StrictObjectShape<Props, "Output">,
@@ -11136,7 +13266,7 @@ type StrictObjectType<Props extends ObjectProps> = Type<
11136
13266
  };
11137
13267
 
11138
13268
  type ObjectWithRecordType<
11139
- Props extends ObjectProps,
13269
+ Props extends ObjectValueProps,
11140
13270
  Rest extends ObjectRecordTypeNode,
11141
13271
  > = Type<
11142
13272
  "Object",
@@ -11159,7 +13289,7 @@ type ObjectWithRecordType<
11159
13289
  ObjectWithRecordReflection<Props, Rest>;
11160
13290
 
11161
13291
  type StrictObjectShape<
11162
- Props extends ObjectProps,
13292
+ Props extends ObjectValueProps,
11163
13293
  Field extends "Input" | "Output" | "CanonicalInput",
11164
13294
  > = keyof Props extends never
11165
13295
  ? Readonly<Record<string, never>>
@@ -11176,7 +13306,7 @@ type StrictObjectShape<
11176
13306
  };
11177
13307
 
11178
13308
  type ObjectWithRecordShape<
11179
- Props extends ObjectProps,
13309
+ Props extends ObjectValueProps,
11180
13310
  Rest extends ObjectRecordTypeNode,
11181
13311
  Field extends "Input" | "Output" | "CanonicalInput",
11182
13312
  > = keyof Props extends never
@@ -11184,7 +13314,7 @@ type ObjectWithRecordShape<
11184
13314
  : ObjectDeclaredShape<Props, Field> & ObjectRestShape<Rest, Field>;
11185
13315
 
11186
13316
  type ObjectDeclaredShape<
11187
- Props extends ObjectProps,
13317
+ Props extends ObjectValueProps,
11188
13318
  Field extends "Input" | "Output" | "CanonicalInput",
11189
13319
  > = Simplify<
11190
13320
  {
@@ -11210,25 +13340,25 @@ type ObjectTypeField<
11210
13340
  Field extends "Input" | "Output" | "CanonicalInput",
11211
13341
  > = Field extends "CanonicalInput" ? CanonicalInputOf<T> : T[Field];
11212
13342
 
11213
- type RequiredObjectKeys<Props extends ObjectProps> = Exclude<
13343
+ type RequiredObjectKeys<Props extends ObjectValueProps> = Exclude<
11214
13344
  keyof Props,
11215
13345
  OptionalObjectKeys<Props>
11216
13346
  >;
11217
13347
 
11218
- type OptionalObjectKeys<Props extends ObjectProps> = {
13348
+ type OptionalObjectKeys<Props extends ObjectValueProps> = {
11219
13349
  readonly [Key in keyof Props]: Props[Key] extends OptionalProperty<TypeNode>
11220
13350
  ? Key
11221
13351
  : never;
11222
13352
  }[keyof Props];
11223
13353
 
11224
- type ObjectPropertyType<Property extends ObjectProperty> =
13354
+ type ObjectPropertyType<Property extends ObjectValueProps[string]> =
11225
13355
  Property extends OptionalProperty<infer T> ? T : Property;
11226
13356
 
11227
- type ObjectFromParentPropertyErrors<Props extends ObjectProps> = {
13357
+ type ObjectFromParentPropertyErrors<Props extends ObjectValueProps> = {
11228
13358
  readonly [Key in keyof Props]: TypeFromError<ObjectPropertyType<Props[Key]>>;
11229
13359
  };
11230
13360
 
11231
- type StrictRootObjectType<Props extends ObjectProps> = Type<
13361
+ type StrictRootObjectType<Props extends ObjectValueProps> = Type<
11232
13362
  "Object",
11233
13363
  StrictObjectShape<Props, "Input">,
11234
13364
  StrictObjectShape<Props, "Output">,
@@ -11243,7 +13373,7 @@ type StrictRootObjectType<Props extends ObjectProps> = Type<
11243
13373
  };
11244
13374
 
11245
13375
  type RootObjectWithRecordType<
11246
- Props extends ObjectProps,
13376
+ Props extends ObjectValueProps,
11247
13377
  Rest extends ObjectRecordTypeNode,
11248
13378
  > = Type<
11249
13379
  "Object",
@@ -11258,16 +13388,16 @@ type RootObjectWithRecordType<
11258
13388
  > &
11259
13389
  ObjectWithRecordReflection<Props, Rest>;
11260
13390
 
11261
- type StrictObjectParents<Props extends ObjectProps> = ObjectPropertyType<
13391
+ type StrictObjectParents<Props extends ObjectValueProps> = ObjectPropertyType<
11262
13392
  Props[keyof Props]
11263
13393
  >["parent"];
11264
13394
 
11265
13395
  type ObjectWithRecordParents<
11266
- Props extends ObjectProps,
13396
+ Props extends ObjectValueProps,
11267
13397
  Rest extends ObjectRecordTypeNode,
11268
13398
  > = ObjectPropertyType<Props[keyof Props]>["parent"] | Rest["parent"];
11269
13399
 
11270
- type RootObjectProps<Props extends ObjectProps> = {
13400
+ type RootObjectProps<Props extends ObjectValueProps> = {
11271
13401
  readonly [Key in keyof Props]: Props[Key] extends OptionalProperty<infer T>
11272
13402
  ? OptionalProperty<RootType<T>>
11273
13403
  : Props[Key] extends TypeNode
@@ -11281,20 +13411,20 @@ type RootObjectRecord<Rest extends ObjectRecordTypeNode> = RecordType<
11281
13411
  >;
11282
13412
 
11283
13413
  interface ObjectWithRecordReflection<
11284
- Props extends ObjectProps,
13414
+ Props extends ObjectValueProps,
11285
13415
  Rest extends ObjectRecordTypeNode,
11286
13416
  > {
11287
13417
  readonly props: Readonly<Props>;
11288
13418
  readonly record: Rest;
11289
13419
  }
11290
13420
 
11291
- type ObjectDeclaredErrors<Props extends ObjectProps> = {
13421
+ type ObjectDeclaredErrors<Props extends ObjectValueProps> = {
11292
13422
  readonly [Key in RequiredObjectKeys<Props>]: Props[Key][typeof errorsSymbol];
11293
13423
  } & {
11294
13424
  readonly [Key in OptionalObjectKeys<Props>]?: Props[Key][typeof errorsSymbol];
11295
13425
  };
11296
13426
 
11297
- type StrictObjectFromUnknownPropertyErrors<Props extends ObjectProps> = {
13427
+ type StrictObjectFromUnknownPropertyErrors<Props extends ObjectValueProps> = {
11298
13428
  readonly [Key in RequiredObjectKeys<Props>]:
11299
13429
  | Props[Key][typeof errorsSymbol]
11300
13430
  | ObjectMissingPropertyError
@@ -11474,16 +13604,31 @@ type ObjectRestFromParentError<Rest extends ObjectRecordTypeNode | undefined> =
11474
13604
  : never;
11475
13605
 
11476
13606
  type RuntimeObjectProperty =
11477
- RuntimeTypeNode | OptionalProperty<RuntimeTypeNode>;
13607
+ | RuntimeTypeNode
13608
+ | OptionalProperty<RuntimeTypeNode>
13609
+ | WithDefaultProperty<RuntimeTypeNode, unknown, "replace" | "preserve">;
11478
13610
 
11479
13611
  const isOptionalProperty = (
11480
13612
  property: RuntimeObjectProperty,
11481
13613
  ): property is OptionalProperty<RuntimeTypeNode> =>
11482
13614
  optionalPropertySymbol in property;
11483
13615
 
13616
+ const isDefaultProperty = (
13617
+ property: RuntimeObjectProperty,
13618
+ ): property is WithDefaultProperty<
13619
+ RuntimeTypeNode,
13620
+ unknown,
13621
+ "replace" | "preserve"
13622
+ > => defaultPropertySymbol in property;
13623
+
11484
13624
  const objectPropertyToType = (
11485
13625
  property: RuntimeObjectProperty,
11486
- ): RuntimeTypeNode => (isOptionalProperty(property) ? property.type : property);
13626
+ ): RuntimeTypeNode =>
13627
+ isDefaultProperty(property)
13628
+ ? property[defaultPropertySymbol].output
13629
+ : isOptionalProperty(property)
13630
+ ? property.type
13631
+ : property;
11487
13632
 
11488
13633
  interface RuntimeObjectPropertyErrors {
11489
13634
  [key: string]: TypeError;
@@ -11501,7 +13646,8 @@ const createRecordPropertyError = <Error extends TypeError>(
11501
13646
  * Object {@link Type} with every property optional.
11502
13647
  *
11503
13648
  * No property is required, but every present property must still satisfy its
11504
- * Type.
13649
+ * Type. For {@link withDefault} properties, this disables the missing-property
13650
+ * default; defaults for present `null` or `undefined` values still apply.
11505
13651
  *
11506
13652
  * ### Example
11507
13653
  *
@@ -11535,9 +13681,11 @@ export const partial = <const Props extends ObjectProps>(
11535
13681
 
11536
13682
  for (const key of globalThis.Object.keys(source)) {
11537
13683
  const property = source[key] as RuntimeObjectProperty;
11538
- partialProps[key] = isOptionalProperty(property)
11539
- ? property
11540
- : createOptionalProperty(property);
13684
+ partialProps[key] = isDefaultProperty(property)
13685
+ ? property[defaultPropertySymbol].partial(property.type)
13686
+ : isOptionalProperty(property)
13687
+ ? property
13688
+ : createOptionalProperty(property);
11541
13689
  }
11542
13690
 
11543
13691
  return createObjectType(partialProps) as unknown as ObjectType<
@@ -11551,11 +13699,17 @@ export const partial = <const Props extends ObjectProps>(
11551
13699
  * @group Objects
11552
13700
  */
11553
13701
  export type PartialObjectProps<Props extends ObjectProps> = {
11554
- readonly [Key in keyof Props]: Props[Key] extends OptionalProperty<TypeNode>
11555
- ? Props[Key]
11556
- : Props[Key] extends TypeNode
11557
- ? OptionalProperty<Props[Key]>
11558
- : never;
13702
+ readonly [Key in keyof Props]: Props[Key] extends WithDefaultProperty<
13703
+ infer T,
13704
+ infer Value,
13705
+ infer Strategy
13706
+ >
13707
+ ? OptionalProperty<WithDefaultType<T, Value, Strategy>>
13708
+ : Props[Key] extends OptionalProperty<TypeNode>
13709
+ ? Props[Key]
13710
+ : Props[Key] extends TypeNode
13711
+ ? OptionalProperty<Props[Key]>
13712
+ : never;
11559
13713
  };
11560
13714
 
11561
13715
  /**
@@ -11602,7 +13756,7 @@ export const nullableToOptional = <const Props extends ObjectProps>(
11602
13756
  for (const key of globalThis.Object.keys(source)) {
11603
13757
  const property = source[key] as RuntimeObjectProperty;
11604
13758
 
11605
- if (isOptionalProperty(property)) {
13759
+ if (isOptionalProperty(property) || isDefaultProperty(property)) {
11606
13760
  optionalProps[key] = property;
11607
13761
  continue;
11608
13762
  }
@@ -11661,7 +13815,10 @@ export const omit = <
11661
13815
  const Keys extends ReadonlyArray<keyof Props>,
11662
13816
  Rest extends ObjectRecordTypeNode | undefined = undefined,
11663
13817
  >(
11664
- objectType: ObjectType<Props, Rest>,
13818
+ objectType: {
13819
+ readonly props: Props;
13820
+ readonly record?: Rest;
13821
+ } & NoInfer<ObjectType<Props, Rest>>,
11665
13822
  ...keys: Keys &
11666
13823
  ([ValidateOmitKeys<Keys>] extends [never]
11667
13824
  ? unknown
@@ -11705,6 +13862,198 @@ type OmitKeyConcreteTypeError = CompileTimeError<
11705
13862
  "Each omitted key must be one concrete property key."
11706
13863
  >;
11707
13864
 
13865
+ /**
13866
+ * The {@link Type} returned by {@link objectKeys}.
13867
+ *
13868
+ * @group Objects
13869
+ */
13870
+ export interface ObjectKeysType<
13871
+ Key extends TypeNode,
13872
+ T extends TypeNode & { readonly props: ObjectProps },
13873
+ > extends Type<
13874
+ "ObjectKeys",
13875
+ unknown,
13876
+ T["Output"],
13877
+ ObjectKeysError<T> | TransformOutputError<"ObjectKeys", InferErrors<T>>,
13878
+ typeof Unknown,
13879
+ ObjectKeysError<T> | TransformOutputError<"ObjectKeys", InferErrors<T>>,
13880
+ never,
13881
+ Readonly<Record<string, T["CanonicalInput"][keyof T["CanonicalInput"]]>>,
13882
+ false
13883
+ > {
13884
+ readonly key: Key;
13885
+ readonly output: T;
13886
+ readonly [reflectedTypesSymbol]?: Key | T;
13887
+ }
13888
+
13889
+ /**
13890
+ * Renames a strict {@link object} Type's encoded keys using a string codec.
13891
+ *
13892
+ * The object declaration uses semantic keys. Each key must be a valid Output of
13893
+ * the key Type. Its canonical encoding becomes the external property name.
13894
+ * Decoding accepts those exact names; aliases and unknown properties are
13895
+ * rejected. Values, optionality, and semantic Output stay with the object Type.
13896
+ * Only the outer keys change. Nested field codecs keep their own behavior.
13897
+ *
13898
+ * Construction rejects invalid schema keys, duplicate encodings, and key codecs
13899
+ * that do not decode their encodings back to the declared keys. These are
13900
+ * schema mistakes; invalid external values return normal Type errors. Error
13901
+ * paths use the external names, including missing required properties.
13902
+ *
13903
+ * ### Example
13904
+ *
13905
+ * ```ts
13906
+ * import {
13907
+ * assertEqual,
13908
+ * assertErr,
13909
+ * assertOk,
13910
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
13911
+ * object,
13912
+ * objectKeys,
13913
+ * optional,
13914
+ * PortFromString,
13915
+ * prefixed,
13916
+ * } from "@evolu/common";
13917
+ *
13918
+ * const Key = prefixed("APP_")(
13919
+ * CamelCaseIdentifierFromConstantCaseIdentifier,
13920
+ * );
13921
+ * const Settings = objectKeys(Key)(
13922
+ * object({ port: optional(PortFromString) }),
13923
+ * );
13924
+ *
13925
+ * const result = Settings.fromUnknown({ APP_PORT: "04000" });
13926
+ * assertOk(result, { port: 4000 });
13927
+ *
13928
+ * assertEqual(Settings.to(result.value), { APP_PORT: "4000" });
13929
+ *
13930
+ * assertErr(Settings.fromUnknown({ APP_POTR: "4000" }));
13931
+ * ```
13932
+ *
13933
+ * @group Objects
13934
+ */
13935
+ export const objectKeys =
13936
+ <
13937
+ Key extends ConcreteTypeNode & {
13938
+ readonly Output: string;
13939
+ readonly CanonicalInput: string;
13940
+ },
13941
+ >(
13942
+ key: Key & ValidateOutput<Key>,
13943
+ ) =>
13944
+ <
13945
+ T extends ConcreteTypeNode & {
13946
+ readonly name: "Object";
13947
+ readonly props: ObjectProps;
13948
+ readonly record?: never;
13949
+ },
13950
+ >(
13951
+ type: T & ValidateOutput<T>,
13952
+ ): ObjectKeysType<Key, T> => {
13953
+ const runtimeKey = key as unknown as RuntimeTypeNode;
13954
+ const runtimeOutput = type as unknown as RuntimeTypeNode;
13955
+ const props = createMutableRecord<string, ObjectProperty>();
13956
+ const outputKeyByInputKey = new Map<string, string>();
13957
+ const inputKeyByOutputKey = new Map<string, string>();
13958
+
13959
+ for (const outputKey of globalThis.Object.keys(type.props)) {
13960
+ assert(
13961
+ runtimeKey.is(outputKey),
13962
+ `Invalid object schema key ${safelyStringifyUnknownValue(outputKey)} for ${key.name}.`,
13963
+ );
13964
+ const inputKey = runtimeKey.to(outputKey as never) as string;
13965
+ assert(
13966
+ !outputKeyByInputKey.has(inputKey),
13967
+ `Duplicate encoded object key ${safelyStringifyUnknownValue(inputKey)}.`,
13968
+ );
13969
+ const decoded = runtimeKey.fromUnknown(inputKey, firstValidationOptions);
13970
+ assert(
13971
+ decoded.ok && decoded.value === outputKey,
13972
+ "An object key codec must decode its encoding to the declared key.",
13973
+ );
13974
+ props[inputKey] = type.props[outputKey];
13975
+ outputKeyByInputKey.set(inputKey, outputKey);
13976
+ inputKeyByOutputKey.set(outputKey, inputKey);
13977
+ }
13978
+
13979
+ const input = createObjectType(props) as unknown as RuntimeTypeNode;
13980
+ const fromUnknown = (
13981
+ value: unknown,
13982
+ options = firstValidationOptions,
13983
+ ): Result<unknown, TypeError> => {
13984
+ const result = input.fromUnknown(value, options);
13985
+ if (!result.ok) return err({ type: "ObjectKeys", error: result.error });
13986
+ const output = createMutableRecord();
13987
+ for (const [inputKey, value] of globalThis.Object.entries(
13988
+ result.value as Record<string, unknown>,
13989
+ )) {
13990
+ output[outputKeyByInputKey.get(inputKey)!] = value;
13991
+ }
13992
+ return ok(output);
13993
+ };
13994
+ const to: RuntimeEncoder = (value: never) => {
13995
+ const encoded = runtimeOutput[encoderSymbol](value) as Record<
13996
+ string,
13997
+ unknown
13998
+ >;
13999
+ const input = createMutableRecord();
14000
+ for (const [key, value] of globalThis.Object.entries(encoded)) {
14001
+ input[inputKeyByOutputKey.get(key)!] = value;
14002
+ }
14003
+ return input;
14004
+ };
14005
+ const validateOutput: RuntimeOutputValidation = (value, options) => {
14006
+ const result = runtimeOutput[outputValidationSymbol](value, options);
14007
+ return result.ok
14008
+ ? result
14009
+ : err({ type: "ObjectKeys", outputError: result.error });
14010
+ };
14011
+ const getTypeIssues: RuntimeGetTypeIssues = (error, mode) => {
14012
+ if ("outputError" in error) {
14013
+ return runtimeOutput[getRuntimeTypeIssuesSymbol](
14014
+ error.outputError as TypeError,
14015
+ mode,
14016
+ );
14017
+ }
14018
+ return input[getRuntimeTypeIssuesSymbol](
14019
+ (error as ObjectKeysError<T>).error,
14020
+ mode,
14021
+ );
14022
+ };
14023
+ return createTypeNode<ObjectKeysType<Key, T>>(
14024
+ "ObjectKeys",
14025
+ Unknown,
14026
+ fromUnknown,
14027
+ runtimeOutput.is,
14028
+ validateOutput,
14029
+ createFromOperation(fromUnknown),
14030
+ to,
14031
+ getTypeIssues,
14032
+ { key, output: type },
14033
+ );
14034
+ };
14035
+
14036
+ /**
14037
+ * An error from the externally named object decoded by {@link objectKeys}.
14038
+ *
14039
+ * @group Objects
14040
+ */
14041
+ export interface ObjectKeysError<
14042
+ T extends TypeNode & { readonly props: ObjectProps },
14043
+ >
14044
+ extends TypeError<"ObjectKeys">, TransparentTypeError {
14045
+ readonly error: ObjectError<
14046
+ Readonly<
14047
+ Record<
14048
+ string,
14049
+ | T["props"][keyof T["props"]][typeof errorsSymbol]
14050
+ | ObjectMissingPropertyError
14051
+ | ObjectExcessPropertyError
14052
+ >
14053
+ >
14054
+ >;
14055
+ }
14056
+
11708
14057
  /**
11709
14058
  * Creates a {@link Type} for {@link Result} values.
11710
14059
  *
@@ -11821,7 +14170,9 @@ export type UnknownResult = typeof UnknownResult.Output;
11821
14170
  * type: "Loaded",
11822
14171
  * value: "Evolu",
11823
14172
  * });
14173
+ *
11824
14174
  * assertFalse(Loading.is({ type: "Loading", progress: 1 }));
14175
+ *
11825
14176
  * assertType<
11826
14177
  * typeof Loading.Output extends { readonly type: "Loading" }
11827
14178
  * ? true
@@ -11880,6 +14231,7 @@ export function typed<
11880
14231
  * } from "@evolu/common";
11881
14232
  *
11882
14233
  * const Open = typed("Open", { label: String }, record(String, String));
14234
+ *
11883
14235
  * const result = Open.fromUnknown({
11884
14236
  * type: "Open",
11885
14237
  * label: "Ready",
@@ -12088,6 +14440,7 @@ type TypedTypePropertyError = CompileTimeError<
12088
14440
  * };
12089
14441
  *
12090
14442
  * assertEqual(describeNext({ ok: true, value: "item" }), "Value: item");
14443
+ *
12091
14444
  * assertEqual(
12092
14445
  * describeNext({
12093
14446
  * ok: false,
@@ -12095,6 +14448,7 @@ type TypedTypePropertyError = CompileTimeError<
12095
14448
  * }),
12096
14449
  * "Done: complete",
12097
14450
  * );
14451
+ *
12098
14452
  * assertEqual(
12099
14453
  * describeNext({
12100
14454
  * ok: false,
@@ -14480,7 +16834,10 @@ export const json = <T extends ConcreteTypeNode, Name extends TypeName>(
14480
16834
  const BrandedJson = {
14481
16835
  ...typeNode,
14482
16836
  from,
14483
- orThrow: mapRuntimeResult(fromInput, getOrThrow),
16837
+ orThrow: createRuntimeOrThrow(
16838
+ fromInput,
16839
+ (typeNode as unknown as RuntimeTypeNode).formatError,
16840
+ ),
14484
16841
  orNull: mapRuntimeResult(fromInput, getOrNull),
14485
16842
  } as BrandType<typeof Json, Name, JsonTypeError>;
14486
16843