@evolu/common 8.1.0 → 8.2.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.
package/dist/src/Type.js CHANGED
@@ -90,7 +90,7 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
90
90
  throw new Error(`Expected ${name}.`, { cause: error });
91
91
  };
92
92
  /**
93
- * Creates localized copies of selected {@link Type | Types} for every locale.
93
+ * Localized copies of selected {@link Type} declarations for every locale.
94
94
  *
95
95
  * Each locale supplies one formatter for every Type that can own a formatted
96
96
  * error. Structural Types use their own formatter for structural failures and
@@ -341,6 +341,8 @@ const encoderSymbol =
341
341
  /*#__PURE__*/ globalThis.Symbol();
342
342
  const fromSymbol =
343
343
  /*#__PURE__*/ globalThis.Symbol();
344
+ const templateLiteralSyntaxSymbol =
345
+ /*#__PURE__*/ globalThis.Symbol();
344
346
  const mapRuntimeResult = (operation, map) => (value, options = firstValidationOptions) => map(operation(value, options), options);
345
347
  // `map` must return a fresh operation because this function can attach `.parent`.
346
348
  const mapRuntimeOperations = (operation, map) => {
@@ -600,7 +602,7 @@ export const Uint8Array = /*#__PURE__*/ objectTag("Uint8Array");
600
602
  */
601
603
  export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
602
604
  /**
603
- * Creates a {@link Type} for instances of one constructor.
605
+ * Instance {@link Type} for one constructor.
604
606
  *
605
607
  * Membership uses the intrinsic prototype chain, so subclasses are accepted,
606
608
  * equivalent constructors from other realms are rejected, and custom
@@ -644,6 +646,26 @@ export const instanceOf = (constructor) => {
644
646
  * primitive through `from.parent`. The expected value must have one exact
645
647
  * literal type. Validation uses `===`, so `-0` matches `0`.
646
648
  *
649
+ * In {@link templateLiteralParser}, use a string Literal Type when the literal
650
+ * should be decoded into the Output Tuple. Use a raw string when it should only
651
+ * frame the canonical string.
652
+ *
653
+ * ### Example
654
+ *
655
+ * ```ts
656
+ * import { literal } from "@evolu/common";
657
+ *
658
+ * const Ready = literal("ready");
659
+ *
660
+ * expectTypeOf<typeof Ready.Output>().toEqualTypeOf<"ready">();
661
+ * expectOk(Ready.fromUnknown("ready"), "ready");
662
+ * expectErr(Ready.fromUnknown("pending"), {
663
+ * type: "Literal",
664
+ * expected: "ready",
665
+ * value: "pending",
666
+ * });
667
+ * ```
668
+ *
647
669
  * @group Unions
648
670
  */
649
671
  export const literal = (expected) => {
@@ -663,7 +685,10 @@ export const literal = (expected) => {
663
685
  const formatError = (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`;
664
686
  return globalThis.Object.assign(parent
665
687
  ? createChildType("Literal", parent, validate, formatError)
666
- : createRootType("Literal", validate, formatError), { expected: literalExpected });
688
+ : createRootType("Literal", validate, formatError), {
689
+ expected: literalExpected,
690
+ [templateLiteralSyntaxSymbol]: true,
691
+ });
667
692
  };
668
693
  /** @group Unions */
669
694
  export const Undefined = /*#__PURE__*/ literal(undefined);
@@ -692,7 +717,7 @@ export function union(...typesOrLiterals) {
692
717
  assertNonNullable(member);
693
718
  return member[encoderSymbol](value);
694
719
  };
695
- return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members });
720
+ return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members, [templateLiteralSyntaxSymbol]: true });
696
721
  }
697
722
  const formatUnionError = () => "A value does not match any union member.";
698
723
  const createUnionValidation = (members, validateMember) => (value, options = firstValidationOptions) => {
@@ -714,7 +739,7 @@ const createUnionValidation = (members, validateMember) => (value, options = fir
714
739
  });
715
740
  };
716
741
  /**
717
- * Shorthand for passing a {@link Type} and `undefined` to {@link union}.
742
+ * Union {@link Type} containing the supplied Type and `undefined`.
718
743
  *
719
744
  * This does not make an object property optional. It changes only the values
720
745
  * accepted when the property is present.
@@ -723,13 +748,13 @@ const createUnionValidation = (members, validateMember) => (value, options = fir
723
748
  */
724
749
  export const undefinedOr = (type) => union(type, Undefined);
725
750
  /**
726
- * Shorthand for passing a {@link Type} and `null` to {@link union}.
751
+ * Union {@link Type} containing the supplied Type and `null`.
727
752
  *
728
753
  * @group Unions
729
754
  */
730
755
  export const nullOr = (type) => union(type, Null);
731
756
  /**
732
- * Shorthand for passing a {@link Type}, `null`, and `undefined` to {@link union}.
757
+ * Union {@link Type} containing the supplied Type, `null`, and `undefined`.
733
758
  *
734
759
  * @group Unions
735
760
  */
@@ -737,6 +762,407 @@ export const nullishOr = (type) => union(type, Null, Undefined);
737
762
  const isRuntimeUnionTypeNode = (type) => type.name === "Union" &&
738
763
  "members" in type &&
739
764
  globalThis.Array.isArray(type.members);
765
+ /**
766
+ * Template literal {@link Type} for validation and parsing.
767
+ *
768
+ * Parses and creates structured strings.
769
+ *
770
+ * Accepts the same template parts as {@link templateLiteral}: fixed string
771
+ * literals and Types canonically encoded as strings. Instead of keeping Output
772
+ * as a string, fixed literals define the framing and Output is a readonly Tuple
773
+ * of the decoded Type parts. `to` encodes that Tuple back into the canonical
774
+ * string represented by the parent Type. At least one Type part is required.
775
+ *
776
+ * When every capture uses identity encoding, the parent Output is the exact
777
+ * TypeScript template literal type. A transforming capture makes it nominal;
778
+ * create such strings with `to` or validate them with the parent Type.
779
+ *
780
+ * Deterministic framing is a core correctness guarantee. It preserves
781
+ * reversibility and keeps capture boundaries unambiguous. Different capture
782
+ * Tuples must never encode to the same string. The parser provides predictable
783
+ * parsing without pathological backtracking and decodes each capture once, so
784
+ * adversarial input cannot trigger exponential parser work. Fixed-width captures
785
+ * may be adjacent, but only one variable-width capture is allowed. Declarations
786
+ * that could join UTF-16 surrogate halves across parts are rejected during
787
+ * construction.
788
+ *
789
+ * Keep capture unions reasonably small to avoid excessive compiler work.
790
+ *
791
+ * TypeScript template literal types can describe a fixed number of digit
792
+ * positions, but not an arbitrarily long sequence of digits. Such grammars use
793
+ * branded Types such as {@link DecimalString}; `templateLiteralParser` preserves
794
+ * that exactness by requiring a validated branded capture when encoding.
795
+ *
796
+ * ### Example
797
+ *
798
+ * A template literal Type defines both a canonical string representation and
799
+ * the structured data decoded from it:
800
+ *
801
+ * ```ts
802
+ * import { templateLiteralParser, union } from "@evolu/common";
803
+ *
804
+ * const Language = union("en", "cs");
805
+ * const Region = union("US", "CZ");
806
+ *
807
+ * // Define a Type for "en-US" | "en-CZ" | "cs-US" | "cs-CZ".
808
+ * const SupportedLocale = templateLiteralParser(Language, "-", Region);
809
+ *
810
+ * // Output is the decoded language and region.
811
+ * type SupportedLocale = typeof SupportedLocale.Output;
812
+ * expectTypeOf<SupportedLocale>().toEqualTypeOf<
813
+ * readonly ["en" | "cs", "US" | "CZ"]
814
+ * >();
815
+ *
816
+ * // The parent Output is the canonical locale string.
817
+ * type SupportedLocaleLiteral = typeof SupportedLocale.parent.Output;
818
+ * expectTypeOf<SupportedLocaleLiteral>().toEqualTypeOf<
819
+ * "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
820
+ * >();
821
+ *
822
+ * // Parse an unknown string into structured data.
823
+ * const result = SupportedLocale.fromUnknown("cs-CZ");
824
+ * assert(result.ok);
825
+ * const locale = result.value;
826
+ * expectTypeOf(locale).toEqualTypeOf<SupportedLocale>();
827
+ * expect(locale).toEqual(["cs", "CZ"]);
828
+ * expectErr(SupportedLocale.fromUnknown("cs/CZ"), {
829
+ * type: "TemplateLiteral",
830
+ * value: "cs/CZ",
831
+ * });
832
+ *
833
+ * // Encode structured data into its canonical string.
834
+ * const localeLiteral = SupportedLocale.to(locale);
835
+ * expectTypeOf(localeLiteral).toEqualTypeOf<SupportedLocaleLiteral>();
836
+ * expect(localeLiteral).toBe("cs-CZ");
837
+ *
838
+ * // Validate a string configuration value.
839
+ * const configValue: unknown = "cs-CZ";
840
+ * assert(SupportedLocale.parent.is(configValue));
841
+ * expectTypeOf(configValue).toEqualTypeOf<SupportedLocaleLiteral>();
842
+ * expect(SupportedLocale.parent.is("fr-CZ")).toBe(false);
843
+ * ```
844
+ *
845
+ * `SupportedLocale` is structured data for application code.
846
+ * `SupportedLocaleLiteral` is its canonical representation for configuration
847
+ * and other APIs that require a string, such as URL parameters, environment
848
+ * variables, and storage keys.
849
+ *
850
+ * Use branded captures for strings that TypeScript template literal types
851
+ * cannot express exactly, such as arbitrary-length canonical decimals:
852
+ *
853
+ * ```ts
854
+ * import {
855
+ * NonNegativeDecimalString,
856
+ * templateLiteralParser,
857
+ * } from "@evolu/common";
858
+ *
859
+ * const DecimalText = templateLiteralParser(
860
+ * "decimal:",
861
+ * NonNegativeDecimalString,
862
+ * );
863
+ *
864
+ * // DecimalText.to requires a validated NonNegativeDecimalString.
865
+ * const zero = NonNegativeDecimalString.orThrow("0");
866
+ *
867
+ * expectOk(DecimalText.fromUnknown("decimal:0"), [zero]);
868
+ * expect(DecimalText.to([zero])).toBe("decimal:0");
869
+ * ```
870
+ *
871
+ * Capture Types (the Type arguments passed to `templateLiteralParser`) can use
872
+ * transformations to decode substrings into non-string data:
873
+ *
874
+ * ```ts
875
+ * import {
876
+ * Int64FromInt64String,
877
+ * templateLiteralParser,
878
+ * } from "@evolu/common";
879
+ *
880
+ * const ItemId = templateLiteralParser("item-", Int64FromInt64String);
881
+ * type ItemId = typeof ItemId.Output;
882
+ * type ItemIdLiteral = typeof ItemId.parent.Output;
883
+ *
884
+ * // Decode the string into structured data.
885
+ * const result = ItemId.fromUnknown("item-42");
886
+ * assert(result.ok);
887
+ * const itemId = result.value;
888
+ * expectTypeOf(itemId).toEqualTypeOf<ItemId>();
889
+ * expect(itemId).toEqual([42n]);
890
+ *
891
+ * // Encode the structured data into its canonical string.
892
+ * const itemIdLiteral = ItemId.to(itemId);
893
+ * expectTypeOf(itemIdLiteral).toEqualTypeOf<ItemIdLiteral>();
894
+ * expect(itemIdLiteral).toBe("item-42");
895
+ *
896
+ * // TypeScript cannot prove from the literal alone that "42" is a valid Int64 encoding.
897
+ * // @ts-expect-error Validate it with ItemId.parent or create it with ItemId.to.
898
+ * const invalidItemIdLiteral: ItemIdLiteral = "item-42";
899
+ * ```
900
+ *
901
+ * Fixed-width captures can be adjacent:
902
+ *
903
+ * ```ts
904
+ * import { templateLiteralParser, union } from "@evolu/common";
905
+ *
906
+ * const Digit = union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
907
+ * const TwoDigits = templateLiteralParser(Digit, Digit);
908
+ * type TwoDigits = typeof TwoDigits.Output;
909
+ * type TwoDigitsLiteral = typeof TwoDigits.parent.Output;
910
+ *
911
+ * const twoDigits: TwoDigits = ["4", "2"];
912
+ * const twoDigitsLiteral: TwoDigitsLiteral = "42";
913
+ * // @ts-expect-error TwoDigitsLiteral requires exactly two digits.
914
+ * const threeDigitsLiteral: TwoDigitsLiteral = "123";
915
+ *
916
+ * expectOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
917
+ * expect(TwoDigits.to(twoDigits)).toBe(twoDigitsLiteral);
918
+ * ```
919
+ *
920
+ * TypeScript rejects multiple variable-width captures because their encoded
921
+ * boundaries would be ambiguous:
922
+ *
923
+ * ```ts
924
+ * import { String, templateLiteralParser } from "@evolu/common";
925
+ *
926
+ * // @ts-expect-error At most one Type capture can have a variable-width string representation.
927
+ * templateLiteralParser(String, ":", String);
928
+ * ```
929
+ *
930
+ * This restriction keeps encoding reversible: different capture Tuples must
931
+ * never produce the same string. A delimiter alone is not enough because it can
932
+ * also occur inside a capture. Some formats could provide stronger guarantees,
933
+ * such as captures that exclude a delimiter; support for those can be added
934
+ * when concrete use cases justify the additional framing rules.
935
+ *
936
+ * @group Template literals
937
+ */
938
+ export const templateLiteralParser = (...parts) => createTemplateLiteralParserType(parts);
939
+ const createTemplateLiteralParserType = (templateParts) => {
940
+ const captureTypes = templateParts.filter((part) => typeof part !== "string");
941
+ const runtimeCaptureTypes = captureTypes;
942
+ const output = tuple(...captureTypes);
943
+ const runtimeOutput = output;
944
+ const reflection = {
945
+ output,
946
+ parts: templateParts,
947
+ [templateLiteralSyntaxSymbol]: true,
948
+ };
949
+ const parse = compileTemplateLiteralParser(templateParts);
950
+ const decodeString = (value, options = firstValidationOptions) => {
951
+ const parseResult = parse(value);
952
+ if (!parseResult.ok)
953
+ return parseResult;
954
+ const outputResult = validateTupleItems(parseResult.value, runtimeCaptureTypes, (capture, value, captureOptions) => capture.fromUnknown(value, captureOptions), options, false);
955
+ return (outputResult.ok
956
+ ? outputResult
957
+ : err({
958
+ type: "TemplateLiteral",
959
+ outputError: outputResult.error,
960
+ }));
961
+ };
962
+ const encodeCaptures = (captures) => {
963
+ const encodedCaptures = runtimeOutput[encoderSymbol](captures);
964
+ let value = "";
965
+ let captureIndex = 0;
966
+ for (const part of templateParts) {
967
+ value +=
968
+ typeof part === "string" ? part : encodedCaptures[captureIndex++];
969
+ }
970
+ return value;
971
+ };
972
+ const canonicalizeString = (value, options) => {
973
+ const result = decodeString(value, options);
974
+ return result.ok ? ok(encodeCaptures(result.value)) : result;
975
+ };
976
+ const validateCanonicalString = (value, options = firstValidationOptions) => {
977
+ const stringResult = String.fromUnknown(value, options);
978
+ if (!stringResult.ok)
979
+ return stringResult;
980
+ const result = canonicalizeString(stringResult.value, options);
981
+ if (!result.ok || result.value === stringResult.value)
982
+ return result;
983
+ return err({ type: "TemplateLiteral", value: stringResult.value });
984
+ };
985
+ const getTypeIssues = (error, mode) => {
986
+ if (error.type !== "TemplateLiteral") {
987
+ return String[getRuntimeTypeIssuesSymbol](error, mode);
988
+ }
989
+ if ("outputError" in error) {
990
+ return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
991
+ }
992
+ return singleRuntimeTypeIssue("TemplateLiteral", error, formatTemplateLiteralError);
993
+ };
994
+ const canonicalStringFromUnknown = (value, options = firstValidationOptions) => {
995
+ const stringResult = String.fromUnknown(value, options);
996
+ return stringResult.ok
997
+ ? canonicalizeString(stringResult.value, options)
998
+ : stringResult;
999
+ };
1000
+ const canonicalStringFrom = createFromOperation((value, options = firstValidationOptions) => canonicalizeString(value, options));
1001
+ const stringType = createTypeNode("TemplateLiteral", String, canonicalStringFromUnknown, (value) => validateCanonicalString(value, firstValidationOptions).ok, validateCanonicalString, canonicalStringFrom, identity, getTypeIssues, reflection);
1002
+ const fromUnknown = (value, options = firstValidationOptions) => {
1003
+ const stringResult = String.fromUnknown(value, options);
1004
+ if (!stringResult.ok)
1005
+ return stringResult;
1006
+ // The internal parser has one broad signature, but a statically frameless
1007
+ // declaration cannot return its framing error.
1008
+ return decodeString(stringResult.value, options);
1009
+ };
1010
+ const fromCanonicalString = (value, options = firstValidationOptions) => {
1011
+ String.from(value);
1012
+ const result = decodeString(value, options);
1013
+ if (!result.ok || encodeCaptures(result.value) !== value) {
1014
+ throw new Error("Expected TemplateLiteral.", {
1015
+ cause: result.ok
1016
+ ? {
1017
+ type: "TemplateLiteral",
1018
+ value,
1019
+ }
1020
+ : result.error,
1021
+ });
1022
+ }
1023
+ return result;
1024
+ };
1025
+ const fromString = (value, options = firstValidationOptions) => {
1026
+ String.from(value);
1027
+ return decodeString(value, options);
1028
+ };
1029
+ fromCanonicalString.parent = fromString;
1030
+ const from = createFromOperation(fromCanonicalString);
1031
+ const type = createTypeNode("TemplateLiteral", stringType, fromUnknown, runtimeOutput.is, runtimeOutput[outputValidationSymbol], from, encodeCaptures, getTypeIssues, reflection);
1032
+ type.from.parent =
1033
+ fromCanonicalString;
1034
+ return type;
1035
+ };
1036
+ /**
1037
+ * Template literal {@link Type} for validation.
1038
+ *
1039
+ * Creates a canonical string Type from fixed strings and string-encoded Types.
1040
+ *
1041
+ * Use this factory when Output should remain a string. Switch to
1042
+ * {@link templateLiteralParser} when the individual Type parts should be
1043
+ * decoded into a Tuple.
1044
+ *
1045
+ * ### Example
1046
+ *
1047
+ * ```ts
1048
+ * import { templateLiteral, union } from "@evolu/common";
1049
+ *
1050
+ * const Language = union("en", "cs");
1051
+ * const Region = union("US", "CZ");
1052
+ * const Locale = templateLiteral(Language, "-", Region);
1053
+ *
1054
+ * expectTypeOf<typeof Locale.Output>().toEqualTypeOf<
1055
+ * "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
1056
+ * >();
1057
+ * expectOk(Locale.fromUnknown("cs-CZ"), "cs-CZ");
1058
+ * expect(Locale.is("fr-CZ")).toBe(false);
1059
+ * ```
1060
+ *
1061
+ * @group Template literals
1062
+ */
1063
+ export const templateLiteral = (...parts) => createTemplateLiteralParserType(parts).parent;
1064
+ const formatTemplateLiteralError = (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`;
1065
+ const compileTemplateLiteralParser = (parts) => {
1066
+ let framing = emptyTemplateLiteralFraming;
1067
+ let fixedPartsWidth = 0;
1068
+ const compiledParts = parts.map((part) => {
1069
+ const partFraming = getTemplateLiteralPartFraming(part);
1070
+ framing = concatenateTemplateLiteralFraming(framing, partFraming);
1071
+ fixedPartsWidth += partFraming.width ?? 0;
1072
+ return [part, partFraming.width];
1073
+ });
1074
+ return (input) => {
1075
+ const inputCodePoints = globalThis.Array.from(input);
1076
+ const variableWidth = inputCodePoints.length - fixedPartsWidth;
1077
+ if (variableWidth < 0) {
1078
+ return err({ type: "TemplateLiteral", value: input });
1079
+ }
1080
+ const captures = [];
1081
+ let position = 0;
1082
+ for (const [part, width] of compiledParts) {
1083
+ const partWidth = width ?? variableWidth;
1084
+ const value = inputCodePoints
1085
+ .slice(position, position + partWidth)
1086
+ .join("");
1087
+ if (typeof part === "string") {
1088
+ if (value !== part) {
1089
+ return err({ type: "TemplateLiteral", value: input });
1090
+ }
1091
+ }
1092
+ else {
1093
+ captures.push(value);
1094
+ }
1095
+ position += partWidth;
1096
+ }
1097
+ return position === inputCodePoints.length
1098
+ ? ok(captures)
1099
+ : err({ type: "TemplateLiteral", value: input });
1100
+ };
1101
+ };
1102
+ const emptyTemplateLiteralFraming = {
1103
+ width: 0,
1104
+ canBeEmpty: true,
1105
+ canStartWithLowSurrogate: false,
1106
+ canEndWithHighSurrogate: false,
1107
+ };
1108
+ const unknownTemplateLiteralFraming = {
1109
+ width: null,
1110
+ canBeEmpty: true,
1111
+ canStartWithLowSurrogate: true,
1112
+ canEndWithHighSurrogate: true,
1113
+ };
1114
+ const concatenateTemplateLiteralFraming = (left, right) => {
1115
+ assert(!(left.canEndWithHighSurrogate && right.canStartWithLowSurrogate), "A TemplateLiteral cannot form a Unicode surrogate pair across part boundaries.");
1116
+ return {
1117
+ width: left.width !== null && right.width !== null
1118
+ ? left.width + right.width
1119
+ : null,
1120
+ canBeEmpty: left.canBeEmpty && right.canBeEmpty,
1121
+ canStartWithLowSurrogate: left.canStartWithLowSurrogate ||
1122
+ (left.canBeEmpty && right.canStartWithLowSurrogate),
1123
+ canEndWithHighSurrogate: right.canEndWithHighSurrogate ||
1124
+ (right.canBeEmpty && left.canEndWithHighSurrogate),
1125
+ };
1126
+ };
1127
+ const getStringTemplateLiteralFraming = (value) => {
1128
+ const firstCodeUnit = value.charCodeAt(0);
1129
+ const lastCodeUnit = value.charCodeAt(value.length - 1);
1130
+ return {
1131
+ width: globalThis.Array.from(value).length,
1132
+ canBeEmpty: value.length === 0,
1133
+ canStartWithLowSurrogate: firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff,
1134
+ canEndWithHighSurrogate: lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff,
1135
+ };
1136
+ };
1137
+ const getTemplateLiteralPartFraming = (part) => {
1138
+ if (typeof part === "string")
1139
+ return getStringTemplateLiteralFraming(part);
1140
+ const type = part;
1141
+ if (type[templateLiteralSyntaxSymbol] === true) {
1142
+ if (type.name === "Literal") {
1143
+ return getStringTemplateLiteralFraming(type.expected);
1144
+ }
1145
+ if (type.name === "Union") {
1146
+ const memberFramings = type.members.map(getTemplateLiteralPartFraming);
1147
+ const width = memberFramings[0].width;
1148
+ return {
1149
+ width: width !== null &&
1150
+ memberFramings.every((framing) => framing.width === width)
1151
+ ? width
1152
+ : null,
1153
+ canBeEmpty: memberFramings.some((framing) => framing.canBeEmpty),
1154
+ canStartWithLowSurrogate: memberFramings.some((framing) => framing.canStartWithLowSurrogate),
1155
+ canEndWithHighSurrogate: memberFramings.some((framing) => framing.canEndWithHighSurrogate),
1156
+ };
1157
+ }
1158
+ if (type.name === "TemplateLiteral") {
1159
+ return type.parts.reduce((framing, part) => concatenateTemplateLiteralFraming(framing, getTemplateLiteralPartFraming(part)), emptyTemplateLiteralFraming);
1160
+ }
1161
+ }
1162
+ if (type.parent === null)
1163
+ return unknownTemplateLiteralFraming;
1164
+ return getTemplateLiteralPartFraming(type.parent);
1165
+ };
740
1166
  export function brand(name, parent, validate, formatError) {
741
1167
  return createChildType(name, parent, validate
742
1168
  ? (value) => flatMapResult(validate(value), () => ok(value))
@@ -837,7 +1263,7 @@ export const capitalized = (parent) => brand("Capitalized", parent, (value) => {
837
1263
  */
838
1264
  export const CapitalizedString = /*#__PURE__*/ capitalized(String);
839
1265
  /**
840
- * Adds a {@link Brand} requiring a string without surrounding whitespace.
1266
+ * String {@link Brand} without surrounding whitespace.
841
1267
  *
842
1268
  * @group String
843
1269
  */
@@ -863,7 +1289,7 @@ export const TrimmedString = /*#__PURE__*/ trimmed(String);
863
1289
  */
864
1290
  export const trim = (value) => value.trim();
865
1291
  /**
866
- * Adds a {@link Brand} requiring a value to have at least `min` items.
1292
+ * Minimum-length {@link Brand} requiring a value to have at least `min` items.
867
1293
  *
868
1294
  * @group String
869
1295
  * @group Collection
@@ -890,7 +1316,7 @@ export const minLength = (min) => (parent) => {
890
1316
  */
891
1317
  export const NonEmptyTrimmedString = /*#__PURE__*/ minLength(1)(TrimmedString);
892
1318
  /**
893
- * Adds a {@link Brand} requiring a value to have at most `max` items.
1319
+ * Maximum-length {@link Brand} requiring a value to have at most `max` items.
894
1320
  *
895
1321
  * @group String
896
1322
  * @group Collection
@@ -914,7 +1340,7 @@ export const NonEmptyTrimmedString100 = /*#__PURE__*/ maxLength(100)(NonEmptyTri
914
1340
  */
915
1341
  export const NonEmptyTrimmedString1000 = /*#__PURE__*/ maxLength(1000)(NonEmptyTrimmedString);
916
1342
  /**
917
- * Adds a {@link Brand} requiring a value to have exactly `exact` items.
1343
+ * Exact-length {@link Brand} requiring a value to have exactly `exact` items.
918
1344
  *
919
1345
  * @group String
920
1346
  * @group Collection
@@ -926,7 +1352,7 @@ export const length = (exact) => (parent) => {
926
1352
  : err({ type: name, value, exact }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not have the required length of ${error.exact}.`);
927
1353
  };
928
1354
  /**
929
- * Creates a string {@link Brand} that must match a regular expression.
1355
+ * String {@link Brand} constrained by a regular expression.
930
1356
  *
931
1357
  * ### Example
932
1358
  *
@@ -1133,7 +1559,7 @@ export const createIdAsUuidv7 = (deps, ..._validation) => {
1133
1559
  return uint8ArrayToBase64Url(bytes);
1134
1560
  };
1135
1561
  /**
1136
- * A table-specific {@link Id} Type.
1562
+ * Table-specific {@link Id} Type.
1137
1563
  *
1138
1564
  * @group String
1139
1565
  */
@@ -1187,7 +1613,7 @@ export const Int64FromInt64String = /*#__PURE__*/ transform("Int64FromInt64Strin
1187
1613
  to: (value) => globalThis.String(value),
1188
1614
  });
1189
1615
  /**
1190
- * Adds a {@link Brand} requiring a number greater than or equal to zero.
1616
+ * Number {@link Brand} requiring a value greater than or equal to zero.
1191
1617
  *
1192
1618
  * @group Number
1193
1619
  */
@@ -1199,7 +1625,7 @@ export const nonNegative = (parent) => brand("NonNegative", parent, (value) => v
1199
1625
  */
1200
1626
  export const NonNegativeNumber = /*#__PURE__*/ nonNegative(Number);
1201
1627
  /**
1202
- * Adds a {@link Brand} requiring a number greater than zero.
1628
+ * Number {@link Brand} requiring a value greater than zero.
1203
1629
  *
1204
1630
  * @group Number
1205
1631
  */
@@ -1214,7 +1640,7 @@ export const positive = (parent) => brand("Positive", parent, (value) => value >
1214
1640
  */
1215
1641
  export const PositiveNumber = /*#__PURE__*/ positive(NonNegativeNumber);
1216
1642
  /**
1217
- * Adds a {@link Brand} requiring a number less than or equal to zero.
1643
+ * Number {@link Brand} requiring a value less than or equal to zero.
1218
1644
  *
1219
1645
  * @group Number
1220
1646
  */
@@ -1226,7 +1652,7 @@ export const nonPositive = (parent) => brand("NonPositive", parent, (value) => v
1226
1652
  */
1227
1653
  export const NonPositiveNumber = /*#__PURE__*/ nonPositive(Number);
1228
1654
  /**
1229
- * Adds a {@link Brand} requiring a number less than zero.
1655
+ * Number {@link Brand} requiring a value less than zero.
1230
1656
  *
1231
1657
  * @group Number
1232
1658
  */
@@ -1241,7 +1667,7 @@ export const negative = (parent) => brand("Negative", parent, (value) => value <
1241
1667
  */
1242
1668
  export const NegativeNumber = /*#__PURE__*/ negative(NonPositiveNumber);
1243
1669
  /**
1244
- * Adds a {@link Brand} requiring a number other than `NaN`.
1670
+ * Number {@link Brand} requiring a value other than `NaN`.
1245
1671
  *
1246
1672
  * @group Number
1247
1673
  */
@@ -1260,7 +1686,7 @@ export const nonNaN = (parent) => brand("NonNaN", parent, (value) => globalThis.
1260
1686
  */
1261
1687
  export const NonNaNNumber = /*#__PURE__*/ nonNaN(Number);
1262
1688
  /**
1263
- * Adds a {@link Brand} requiring a finite number.
1689
+ * Number {@link Brand} requiring a finite value.
1264
1690
  *
1265
1691
  * @group Number
1266
1692
  */
@@ -1366,7 +1792,7 @@ export const NonPositiveInt = /*#__PURE__*/ nonPositive(Int);
1366
1792
  */
1367
1793
  export const NegativeInt = /*#__PURE__*/ negative(NonPositiveInt);
1368
1794
  /**
1369
- * Adds a {@link Brand} requiring a number greater than `min`.
1795
+ * Number {@link Brand} requiring a value greater than `min`.
1370
1796
  *
1371
1797
  * @group Number
1372
1798
  */
@@ -1377,7 +1803,7 @@ export const greaterThan = (min) => (parent) => {
1377
1803
  : err({ type: name, value, min }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be greater than ${error.min}.`);
1378
1804
  };
1379
1805
  /**
1380
- * Adds a {@link Brand} requiring a number greater than or equal to `min`.
1806
+ * Number {@link Brand} requiring a value greater than or equal to `min`.
1381
1807
  *
1382
1808
  * @group Number
1383
1809
  */
@@ -1392,7 +1818,7 @@ export const greaterThanOrEqualTo = (min) => (parent) => {
1392
1818
  }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be greater than or equal to ${error.min}.`);
1393
1819
  };
1394
1820
  /**
1395
- * Adds a {@link Brand} requiring a number less than `max`.
1821
+ * Number {@link Brand} requiring a value less than `max`.
1396
1822
  *
1397
1823
  * @group Number
1398
1824
  */
@@ -1410,7 +1836,7 @@ export const lessThan = (max) => (parent) => {
1410
1836
  export const Age = /*#__PURE__*/ brand("Age",
1411
1837
  /*#__PURE__*/ lessThan(200)(NonNegativeInt));
1412
1838
  /**
1413
- * Adds a {@link Brand} requiring a number less than or equal to `max`.
1839
+ * Number {@link Brand} requiring a value less than or equal to `max`.
1414
1840
  *
1415
1841
  * @group Number
1416
1842
  */
@@ -1430,46 +1856,127 @@ export const lessThanOrEqualTo = (max) => (parent) => {
1430
1856
  export const Ratio = /*#__PURE__*/ brand("Ratio",
1431
1857
  /*#__PURE__*/ lessThanOrEqualTo(1)(NonNegativeFiniteNumber));
1432
1858
  /**
1433
- * Canonical string representation of a positive base-10 decimal value.
1859
+ * Canonical string representation of a signed base-10 decimal value.
1434
1860
  *
1435
1861
  * Use this Type when a decimal value must remain exact instead of being
1436
1862
  * converted to an IEEE-754 number. Equivalent values have one accepted
1437
- * representation, so leading zeroes, trailing fractional zeroes, signs, and
1438
- * exponent notation are rejected.
1863
+ * representation, so leading zeroes, trailing fractional zeroes, `-0`, plus
1864
+ * signs, and exponent notation are rejected.
1439
1865
  *
1440
1866
  * The decoded value remains a string. Arithmetic requires an explicit decimal
1441
1867
  * or fixed-point representation.
1442
1868
  *
1869
+ * TypeScript template literal types can describe a fixed number of digit
1870
+ * positions, but not the arbitrarily long integer and fractional parts accepted
1871
+ * here. `DecimalString` therefore uses a {@link Brand} so its TypeScript type
1872
+ * does not accept strings that have not been validated.
1873
+ *
1874
+ * Use these predefined Types or their corresponding factories to add sign
1875
+ * constraints to compatible decimal string Types:
1876
+ *
1877
+ * - {@link NonNegativeDecimalString} / {@link nonNegativeDecimalString}
1878
+ * - {@link PositiveDecimalString} / {@link positiveDecimalString}
1879
+ * - {@link NonPositiveDecimalString} / {@link nonPositiveDecimalString}
1880
+ * - {@link NegativeDecimalString} / {@link negativeDecimalString}
1881
+ *
1443
1882
  * ### Example
1444
1883
  *
1445
1884
  * ```ts
1446
- * import { PositiveDecimalString } from "@evolu/common";
1885
+ * import { DecimalString } from "@evolu/common";
1447
1886
  *
1448
- * expectOk(PositiveDecimalString.fromUnknown("0.3"), "0.3");
1449
- * expectOk(PositiveDecimalString.fromUnknown("25"), "25");
1450
- * expectOk(PositiveDecimalString.fromUnknown("10.01"), "10.01");
1887
+ * expectOk(DecimalString.fromUnknown("-10.25"), "-10.25");
1888
+ * expectOk(DecimalString.fromUnknown("0"), "0");
1889
+ * expectOk(DecimalString.fromUnknown("10.25"), "10.25");
1451
1890
  *
1452
- * expectErr(PositiveDecimalString.fromUnknown("0"), {
1453
- * type: "PositiveDecimalString",
1454
- * value: "0",
1455
- * });
1456
- * expectErr(PositiveDecimalString.fromUnknown("0.30"), {
1457
- * type: "PositiveDecimalString",
1458
- * value: "0.30",
1891
+ * expectErr(DecimalString.fromUnknown("10.250"), {
1892
+ * type: "DecimalString",
1893
+ * value: "10.250",
1459
1894
  * });
1460
1895
  * ```
1461
1896
  *
1462
1897
  * @group Number
1463
1898
  */
1464
- export const PositiveDecimalString = /*#__PURE__*/ brand("PositiveDecimalString", String, (value) => /^(?:[1-9]\d*|(?:0|[1-9]\d*)\.\d*[1-9])$/.test(value)
1899
+ export const DecimalString = /*#__PURE__*/ brand("DecimalString", String, (value) => /^(?:0|-?(?:[1-9]\d*|(?:0|[1-9]\d*)\.\d*[1-9]))$/.test(value)
1900
+ ? ok()
1901
+ : err({ type: "DecimalString", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a canonical decimal string.`);
1902
+ /**
1903
+ * {@link DecimalString} Brand requiring a value greater than or equal to zero.
1904
+ *
1905
+ * @group Number
1906
+ */
1907
+ export const nonNegativeDecimalString = (parent) => brand("NonNegativeDecimalString", parent, (value) => value[0] !== "-"
1908
+ ? ok()
1909
+ : err({
1910
+ type: "NonNegativeDecimalString",
1911
+ value,
1912
+ }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a non-negative decimal string.`);
1913
+ /**
1914
+ * Non-negative {@link DecimalString}.
1915
+ *
1916
+ * @group Number
1917
+ */
1918
+ export const NonNegativeDecimalString =
1919
+ /*#__PURE__*/ nonNegativeDecimalString(DecimalString);
1920
+ /**
1921
+ * {@link DecimalString} Brand requiring a value greater than zero.
1922
+ *
1923
+ * @group Number
1924
+ */
1925
+ export const positiveDecimalString = (parent) => brand("PositiveDecimalString", parent, (value) => value !== "0" && value[0] !== "-"
1465
1926
  ? ok()
1466
1927
  : err({
1467
1928
  type: "PositiveDecimalString",
1468
1929
  value,
1469
- }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a canonical positive decimal string.`);
1930
+ }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a positive decimal string.`);
1470
1931
  /**
1471
- * Adds a {@link Brand} requiring a number to be a multiple of an exact decimal
1472
- * `divisor`.
1932
+ * Positive {@link DecimalString}.
1933
+ *
1934
+ * Also satisfies {@link NonNegativeDecimalString}, so it can be used wherever a
1935
+ * non-negative decimal string is required.
1936
+ *
1937
+ * @group Number
1938
+ */
1939
+ export const PositiveDecimalString = /*#__PURE__*/ positiveDecimalString(NonNegativeDecimalString);
1940
+ /**
1941
+ * {@link DecimalString} Brand requiring a value less than or equal to zero.
1942
+ *
1943
+ * @group Number
1944
+ */
1945
+ export const nonPositiveDecimalString = (parent) => brand("NonPositiveDecimalString", parent, (value) => value === "0" || value[0] === "-"
1946
+ ? ok()
1947
+ : err({
1948
+ type: "NonPositiveDecimalString",
1949
+ value,
1950
+ }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a non-positive decimal string.`);
1951
+ /**
1952
+ * Non-positive {@link DecimalString}.
1953
+ *
1954
+ * @group Number
1955
+ */
1956
+ export const NonPositiveDecimalString =
1957
+ /*#__PURE__*/ nonPositiveDecimalString(DecimalString);
1958
+ /**
1959
+ * {@link DecimalString} Brand requiring a value less than zero.
1960
+ *
1961
+ * @group Number
1962
+ */
1963
+ export const negativeDecimalString = (parent) => brand("NegativeDecimalString", parent, (value) => value[0] === "-"
1964
+ ? ok()
1965
+ : err({
1966
+ type: "NegativeDecimalString",
1967
+ value,
1968
+ }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a negative decimal string.`);
1969
+ /**
1970
+ * Negative {@link DecimalString}.
1971
+ *
1972
+ * Also satisfies {@link NonPositiveDecimalString}, so it can be used wherever a
1973
+ * non-positive decimal string is required.
1974
+ *
1975
+ * @group Number
1976
+ */
1977
+ export const NegativeDecimalString = /*#__PURE__*/ negativeDecimalString(NonPositiveDecimalString);
1978
+ /**
1979
+ * Number {@link Brand} requiring an exact decimal multiple of `divisor`.
1473
1980
  *
1474
1981
  * The divisor must be one canonical positive decimal string literal because its
1475
1982
  * exact value is encoded in the resulting Brand name. The declaration is
@@ -1542,7 +2049,7 @@ const decimalStringToParts = (value) => {
1542
2049
  return { coefficient, exponent };
1543
2050
  };
1544
2051
  /**
1545
- * Adds a {@link Brand} requiring a number to be within an inclusive range.
2052
+ * Number {@link Brand} requiring a value within an inclusive range.
1546
2053
  *
1547
2054
  * @group Number
1548
2055
  */
@@ -1974,6 +2481,57 @@ const createTupleType = (typeElements) => {
1974
2481
  return createTypeNode("Tuple", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { elements: typeElements });
1975
2482
  };
1976
2483
  const validateTupleItems = (value, elements, validate, options, checkStructure) => validateIndexedArrayItems("Tuple", value, (value, elementOptions, index) => validate(elements[index], value, elementOptions, index), options, checkStructure);
2484
+ /**
2485
+ * Decimal digit from `"0"` to `"9"`.
2486
+ *
2487
+ * @group String
2488
+ */
2489
+ export const Digit = /*#__PURE__*/ union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
2490
+ /**
2491
+ * Decimal digit from `"1"` to `"9"`.
2492
+ *
2493
+ * @group String
2494
+ */
2495
+ export const Digit1To9 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6", "7", "8", "9");
2496
+ /**
2497
+ * Decimal string from `"1"` to `"6"`.
2498
+ *
2499
+ * @group String
2500
+ */
2501
+ export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
2502
+ /**
2503
+ * Decimal string from `"1"` to `"23"`.
2504
+ *
2505
+ * @group String
2506
+ */
2507
+ export const Digit1To23 = /*#__PURE__*/ union(Digit1To9,
2508
+ /*#__PURE__*/ templateLiteral("1", Digit),
2509
+ /*#__PURE__*/ templateLiteral("2",
2510
+ /*#__PURE__*/ union("0", "1", "2", "3")));
2511
+ /**
2512
+ * Decimal string from `"1"` to `"51"`.
2513
+ *
2514
+ * @group String
2515
+ */
2516
+ export const Digit1To51 = /*#__PURE__*/ union(Digit1To9,
2517
+ /*#__PURE__*/ templateLiteral(
2518
+ /*#__PURE__*/ union("1", "2", "3", "4"), Digit),
2519
+ /*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")));
2520
+ /**
2521
+ * Decimal string from `"1"` to `"99"`.
2522
+ *
2523
+ * @group String
2524
+ */
2525
+ export const Digit1To99 = /*#__PURE__*/ union(Digit1To9,
2526
+ /*#__PURE__*/ templateLiteral(Digit1To9, Digit));
2527
+ /**
2528
+ * Decimal string from `"1"` to `"59"`.
2529
+ *
2530
+ * @group String
2531
+ */
2532
+ export const Digit1To59 = /*#__PURE__*/ union(Digit1To9,
2533
+ /*#__PURE__*/ templateLiteral(
2534
+ /*#__PURE__*/ union("1", "2", "3", "4", "5"), Digit));
1977
2535
  const createObjectRuntimeTypeIssues = (defaultFormatter, props, recordType) => (error, mode) => {
1978
2536
  const objectError = error;
1979
2537
  if (objectError.reason.kind !== "Properties") {
@@ -2746,7 +3304,7 @@ const createRecordPropertyError = (issue) => ({
2746
3304
  reason: { kind: "Entries", issues: [issue] },
2747
3305
  });
2748
3306
  /**
2749
- * Creates an {@link object} Type with every property optional.
3307
+ * Object {@link Type} with every property optional.
2750
3308
  *
2751
3309
  * No property is required, but every present property must still satisfy its
2752
3310
  * Type.
@@ -2777,7 +3335,8 @@ export const partial = (props, ..._validation) => {
2777
3335
  return createObjectType(partialProps);
2778
3336
  };
2779
3337
  /**
2780
- * Makes every property whose Union Type includes {@link Null} optional.
3338
+ * Object {@link Type} making every property whose Union Type includes
3339
+ * {@link Null} optional.
2781
3340
  *
2782
3341
  * The property retains its original Union Type, so consumers may omit it, set
2783
3342
  * it to `null`, or provide any other member of that Union. Properties without
@@ -2822,7 +3381,7 @@ export const nullableToOptional = (props, ..._validation) => {
2822
3381
  return createObjectType(optionalProps);
2823
3382
  };
2824
3383
  /**
2825
- * Creates an {@link object} Type without the selected declared properties.
3384
+ * Object {@link Type} without the selected declared properties.
2826
3385
  *
2827
3386
  * @group Objects
2828
3387
  */
@@ -3433,7 +3992,7 @@ export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Js
3433
3992
  to: stringifyJsonValue,
3434
3993
  });
3435
3994
  /**
3436
- * Creates a branded {@link Json} Type and total conversions for another Type.
3995
+ * Branded {@link Json} Type and total conversions for another Type.
3437
3996
  *
3438
3997
  * Use this factory when a domain value must be stored as JSON text while its
3439
3998
  * exact Type remains visible to TypeScript, such as a JSON column in an Evolu