@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/src/Type.ts CHANGED
@@ -36,10 +36,7 @@ import {
36
36
  instance,
37
37
  isInstance,
38
38
  type CompileTimeError,
39
- type Digit,
40
- type Digit1To9,
41
39
  type Instance,
42
- type Int1To100,
43
40
  type IsUnion,
44
41
  type Literal,
45
42
  type Simplify,
@@ -144,8 +141,8 @@ import {
144
141
  * normalized value, it uses a `null` prototype so every string key remains
145
142
  * ordinary data.
146
143
  *
147
- * Evolu Type expects TypeScript's `exactOptionalPropertyTypes` compiler option
148
- * to be enabled.
144
+ * Evolu Type requires TypeScript 7 or newer and expects the
145
+ * `exactOptionalPropertyTypes` compiler option to be enabled.
149
146
  *
150
147
  * Predefined Types intentionally use the names of corresponding JavaScript
151
148
  * built-ins because they represent those familiar value categories. If an
@@ -863,7 +860,7 @@ const assertTypeOutput = <Error extends TypeError>(
863
860
  };
864
861
 
865
862
  /**
866
- * Creates localized copies of selected {@link Type | Types} for every locale.
863
+ * Localized copies of selected {@link Type} declarations for every locale.
867
864
  *
868
865
  * Each locale supplies one formatter for every Type that can own a formatted
869
866
  * error. Structural Types use their own formatter for structural failures and
@@ -2128,10 +2125,13 @@ type RuntimeEncoder = RuntimeOperation<unknown>;
2128
2125
 
2129
2126
  declare const encoderSymbolType: unique symbol;
2130
2127
  declare const fromSymbolType: unique symbol;
2128
+ declare const templateLiteralSyntaxSymbolType: unique symbol;
2131
2129
  const encoderSymbol: typeof encoderSymbolType =
2132
2130
  /*#__PURE__*/ globalThis.Symbol() as typeof encoderSymbolType;
2133
2131
  const fromSymbol: typeof fromSymbolType =
2134
2132
  /*#__PURE__*/ globalThis.Symbol() as typeof fromSymbolType;
2133
+ const templateLiteralSyntaxSymbol: typeof templateLiteralSyntaxSymbolType =
2134
+ /*#__PURE__*/ globalThis.Symbol() as typeof templateLiteralSyntaxSymbolType;
2135
2135
 
2136
2136
  /**
2137
2137
  * Type-erased {@link Type} used to traverse and invoke heterogeneous Type nodes
@@ -2154,6 +2154,7 @@ type RuntimeTypeNode = Omit<TypeNode, typeof customFromSymbol> & {
2154
2154
  // assertion, avoiding repeated validation and preserving identity fast paths.
2155
2155
  readonly [encoderSymbolType]: RuntimeEncoder;
2156
2156
  readonly [getRuntimeTypeIssuesSymbolType]: RuntimeGetTypeIssues;
2157
+ readonly [templateLiteralSyntaxSymbolType]?: true;
2157
2158
  };
2158
2159
 
2159
2160
  const mapRuntimeResult =
@@ -2612,7 +2613,7 @@ interface ObjectTagOutputByName {
2612
2613
  }
2613
2614
 
2614
2615
  /**
2615
- * Creates a realm-neutral {@link Type} that trusts an object's reported tag.
2616
+ * Realm-neutral {@link Type} trusting an object's reported tag.
2616
2617
  *
2617
2618
  * Predefined built-in tags expose their native Output type under the assumption
2618
2619
  * that trusted code does not forge their tags. They do not verify native
@@ -2720,7 +2721,7 @@ export const Uint8Array = /*#__PURE__*/ objectTag("Uint8Array");
2720
2721
  export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
2721
2722
 
2722
2723
  /**
2723
- * Creates a {@link Type} for instances of one constructor.
2724
+ * Instance {@link Type} for one constructor.
2724
2725
  *
2725
2726
  * Membership uses the intrinsic prototype chain, so subclasses are accepted,
2726
2727
  * equivalent constructors from other realms are rejected, and custom
@@ -2831,6 +2832,26 @@ type InstanceConstructorCompileTimeError = CompileTimeError<
2831
2832
  * primitive through `from.parent`. The expected value must have one exact
2832
2833
  * literal type. Validation uses `===`, so `-0` matches `0`.
2833
2834
  *
2835
+ * In {@link templateLiteralParser}, use a string Literal Type when the literal
2836
+ * should be decoded into the Output Tuple. Use a raw string when it should only
2837
+ * frame the canonical string.
2838
+ *
2839
+ * ### Example
2840
+ *
2841
+ * ```ts
2842
+ * import { literal } from "@evolu/common";
2843
+ *
2844
+ * const Ready = literal("ready");
2845
+ *
2846
+ * expectTypeOf<typeof Ready.Output>().toEqualTypeOf<"ready">();
2847
+ * expectOk(Ready.fromUnknown("ready"), "ready");
2848
+ * expectErr(Ready.fromUnknown("pending"), {
2849
+ * type: "Literal",
2850
+ * expected: "ready",
2851
+ * value: "pending",
2852
+ * });
2853
+ * ```
2854
+ *
2834
2855
  * @group Unions
2835
2856
  */
2836
2857
  export const literal = <const Expected extends Literal>(
@@ -2867,7 +2888,10 @@ export const literal = <const Expected extends Literal>(
2867
2888
  formatError,
2868
2889
  )
2869
2890
  : createRootType("Literal", validate, formatError),
2870
- { expected: literalExpected },
2891
+ {
2892
+ expected: literalExpected,
2893
+ [templateLiteralSyntaxSymbol]: true,
2894
+ },
2871
2895
  ) as unknown as LiteralType<Expected>;
2872
2896
  };
2873
2897
 
@@ -2886,6 +2910,7 @@ export interface LiteralType<Expected extends Literal> extends Type<
2886
2910
  >,
2887
2911
  IdentityEncodingForParent<LiteralParent<Expected>>
2888
2912
  > {
2913
+ readonly [templateLiteralSyntaxSymbol]: true;
2889
2914
  readonly expected: Expected;
2890
2915
  }
2891
2916
 
@@ -3072,7 +3097,7 @@ export function union(
3072
3097
  from,
3073
3098
  to,
3074
3099
  getTypeIssues,
3075
- { members },
3100
+ { members, [templateLiteralSyntaxSymbol]: true },
3076
3101
  );
3077
3102
  }
3078
3103
 
@@ -3116,7 +3141,7 @@ const createUnionValidation =
3116
3141
  };
3117
3142
 
3118
3143
  /**
3119
- * Shorthand for passing a {@link Type} and `undefined` to {@link union}.
3144
+ * Union {@link Type} containing the supplied Type and `undefined`.
3120
3145
  *
3121
3146
  * This does not make an object property optional. It changes only the values
3122
3147
  * accepted when the property is present.
@@ -3128,7 +3153,7 @@ export const undefinedOr = <ValueType extends TypeNode>(
3128
3153
  ): UnionType<readonly [ValueType, typeof Undefined]> => union(type, Undefined);
3129
3154
 
3130
3155
  /**
3131
- * Shorthand for passing a {@link Type} and `null` to {@link union}.
3156
+ * Union {@link Type} containing the supplied Type and `null`.
3132
3157
  *
3133
3158
  * @group Unions
3134
3159
  */
@@ -3137,7 +3162,7 @@ export const nullOr = <ValueType extends TypeNode>(
3137
3162
  ): UnionType<readonly [ValueType, typeof Null]> => union(type, Null);
3138
3163
 
3139
3164
  /**
3140
- * Shorthand for passing a {@link Type}, `null`, and `undefined` to {@link union}.
3165
+ * Union {@link Type} containing the supplied Type, `null`, and `undefined`.
3141
3166
  *
3142
3167
  * @group Unions
3143
3168
  */
@@ -3194,6 +3219,7 @@ export interface UnionType<
3194
3219
  CanonicalInputOf<Members[number]>,
3195
3220
  AllTypesUseIdentityEncoding<Members[number]>
3196
3221
  > {
3222
+ readonly [templateLiteralSyntaxSymbol]: true;
3197
3223
  readonly [reflectedTypesSymbol]?: Members[number];
3198
3224
  readonly members: Members;
3199
3225
  }
@@ -3326,6 +3352,849 @@ interface UnionErrorValue<
3326
3352
  readonly errors: NonEmptyReadonlyArray<MemberError>;
3327
3353
  }
3328
3354
 
3355
+ /**
3356
+ * Template literal {@link Type} for validation and parsing.
3357
+ *
3358
+ * Parses and creates structured strings.
3359
+ *
3360
+ * Accepts the same template parts as {@link templateLiteral}: fixed string
3361
+ * literals and Types canonically encoded as strings. Instead of keeping Output
3362
+ * as a string, fixed literals define the framing and Output is a readonly Tuple
3363
+ * of the decoded Type parts. `to` encodes that Tuple back into the canonical
3364
+ * string represented by the parent Type. At least one Type part is required.
3365
+ *
3366
+ * When every capture uses identity encoding, the parent Output is the exact
3367
+ * TypeScript template literal type. A transforming capture makes it nominal;
3368
+ * create such strings with `to` or validate them with the parent Type.
3369
+ *
3370
+ * Deterministic framing is a core correctness guarantee. It preserves
3371
+ * reversibility and keeps capture boundaries unambiguous. Different capture
3372
+ * Tuples must never encode to the same string. The parser provides predictable
3373
+ * parsing without pathological backtracking and decodes each capture once, so
3374
+ * adversarial input cannot trigger exponential parser work. Fixed-width captures
3375
+ * may be adjacent, but only one variable-width capture is allowed. Declarations
3376
+ * that could join UTF-16 surrogate halves across parts are rejected during
3377
+ * construction.
3378
+ *
3379
+ * Keep capture unions reasonably small to avoid excessive compiler work.
3380
+ *
3381
+ * TypeScript template literal types can describe a fixed number of digit
3382
+ * positions, but not an arbitrarily long sequence of digits. Such grammars use
3383
+ * branded Types such as {@link DecimalString}; `templateLiteralParser` preserves
3384
+ * that exactness by requiring a validated branded capture when encoding.
3385
+ *
3386
+ * ### Example
3387
+ *
3388
+ * A template literal Type defines both a canonical string representation and
3389
+ * the structured data decoded from it:
3390
+ *
3391
+ * ```ts
3392
+ * import { templateLiteralParser, union } from "@evolu/common";
3393
+ *
3394
+ * const Language = union("en", "cs");
3395
+ * const Region = union("US", "CZ");
3396
+ *
3397
+ * // Define a Type for "en-US" | "en-CZ" | "cs-US" | "cs-CZ".
3398
+ * const SupportedLocale = templateLiteralParser(Language, "-", Region);
3399
+ *
3400
+ * // Output is the decoded language and region.
3401
+ * type SupportedLocale = typeof SupportedLocale.Output;
3402
+ * expectTypeOf<SupportedLocale>().toEqualTypeOf<
3403
+ * readonly ["en" | "cs", "US" | "CZ"]
3404
+ * >();
3405
+ *
3406
+ * // The parent Output is the canonical locale string.
3407
+ * type SupportedLocaleLiteral = typeof SupportedLocale.parent.Output;
3408
+ * expectTypeOf<SupportedLocaleLiteral>().toEqualTypeOf<
3409
+ * "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
3410
+ * >();
3411
+ *
3412
+ * // Parse an unknown string into structured data.
3413
+ * const result = SupportedLocale.fromUnknown("cs-CZ");
3414
+ * assert(result.ok);
3415
+ * const locale = result.value;
3416
+ * expectTypeOf(locale).toEqualTypeOf<SupportedLocale>();
3417
+ * expect(locale).toEqual(["cs", "CZ"]);
3418
+ * expectErr(SupportedLocale.fromUnknown("cs/CZ"), {
3419
+ * type: "TemplateLiteral",
3420
+ * value: "cs/CZ",
3421
+ * });
3422
+ *
3423
+ * // Encode structured data into its canonical string.
3424
+ * const localeLiteral = SupportedLocale.to(locale);
3425
+ * expectTypeOf(localeLiteral).toEqualTypeOf<SupportedLocaleLiteral>();
3426
+ * expect(localeLiteral).toBe("cs-CZ");
3427
+ *
3428
+ * // Validate a string configuration value.
3429
+ * const configValue: unknown = "cs-CZ";
3430
+ * assert(SupportedLocale.parent.is(configValue));
3431
+ * expectTypeOf(configValue).toEqualTypeOf<SupportedLocaleLiteral>();
3432
+ * expect(SupportedLocale.parent.is("fr-CZ")).toBe(false);
3433
+ * ```
3434
+ *
3435
+ * `SupportedLocale` is structured data for application code.
3436
+ * `SupportedLocaleLiteral` is its canonical representation for configuration
3437
+ * and other APIs that require a string, such as URL parameters, environment
3438
+ * variables, and storage keys.
3439
+ *
3440
+ * Use branded captures for strings that TypeScript template literal types
3441
+ * cannot express exactly, such as arbitrary-length canonical decimals:
3442
+ *
3443
+ * ```ts
3444
+ * import {
3445
+ * NonNegativeDecimalString,
3446
+ * templateLiteralParser,
3447
+ * } from "@evolu/common";
3448
+ *
3449
+ * const DecimalText = templateLiteralParser(
3450
+ * "decimal:",
3451
+ * NonNegativeDecimalString,
3452
+ * );
3453
+ *
3454
+ * // DecimalText.to requires a validated NonNegativeDecimalString.
3455
+ * const zero = NonNegativeDecimalString.orThrow("0");
3456
+ *
3457
+ * expectOk(DecimalText.fromUnknown("decimal:0"), [zero]);
3458
+ * expect(DecimalText.to([zero])).toBe("decimal:0");
3459
+ * ```
3460
+ *
3461
+ * Capture Types (the Type arguments passed to `templateLiteralParser`) can use
3462
+ * transformations to decode substrings into non-string data:
3463
+ *
3464
+ * ```ts
3465
+ * import {
3466
+ * Int64FromInt64String,
3467
+ * templateLiteralParser,
3468
+ * } from "@evolu/common";
3469
+ *
3470
+ * const ItemId = templateLiteralParser("item-", Int64FromInt64String);
3471
+ * type ItemId = typeof ItemId.Output;
3472
+ * type ItemIdLiteral = typeof ItemId.parent.Output;
3473
+ *
3474
+ * // Decode the string into structured data.
3475
+ * const result = ItemId.fromUnknown("item-42");
3476
+ * assert(result.ok);
3477
+ * const itemId = result.value;
3478
+ * expectTypeOf(itemId).toEqualTypeOf<ItemId>();
3479
+ * expect(itemId).toEqual([42n]);
3480
+ *
3481
+ * // Encode the structured data into its canonical string.
3482
+ * const itemIdLiteral = ItemId.to(itemId);
3483
+ * expectTypeOf(itemIdLiteral).toEqualTypeOf<ItemIdLiteral>();
3484
+ * expect(itemIdLiteral).toBe("item-42");
3485
+ *
3486
+ * // TypeScript cannot prove from the literal alone that "42" is a valid Int64 encoding.
3487
+ * // @ts-expect-error Validate it with ItemId.parent or create it with ItemId.to.
3488
+ * const invalidItemIdLiteral: ItemIdLiteral = "item-42";
3489
+ * ```
3490
+ *
3491
+ * Fixed-width captures can be adjacent:
3492
+ *
3493
+ * ```ts
3494
+ * import { templateLiteralParser, union } from "@evolu/common";
3495
+ *
3496
+ * const Digit = union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
3497
+ * const TwoDigits = templateLiteralParser(Digit, Digit);
3498
+ * type TwoDigits = typeof TwoDigits.Output;
3499
+ * type TwoDigitsLiteral = typeof TwoDigits.parent.Output;
3500
+ *
3501
+ * const twoDigits: TwoDigits = ["4", "2"];
3502
+ * const twoDigitsLiteral: TwoDigitsLiteral = "42";
3503
+ * // @ts-expect-error TwoDigitsLiteral requires exactly two digits.
3504
+ * const threeDigitsLiteral: TwoDigitsLiteral = "123";
3505
+ *
3506
+ * expectOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
3507
+ * expect(TwoDigits.to(twoDigits)).toBe(twoDigitsLiteral);
3508
+ * ```
3509
+ *
3510
+ * TypeScript rejects multiple variable-width captures because their encoded
3511
+ * boundaries would be ambiguous:
3512
+ *
3513
+ * ```ts
3514
+ * import { String, templateLiteralParser } from "@evolu/common";
3515
+ *
3516
+ * // @ts-expect-error At most one Type capture can have a variable-width string representation.
3517
+ * templateLiteralParser(String, ":", String);
3518
+ * ```
3519
+ *
3520
+ * This restriction keeps encoding reversible: different capture Tuples must
3521
+ * never produce the same string. A delimiter alone is not enough because it can
3522
+ * also occur inside a capture. Some formats could provide stronger guarantees,
3523
+ * such as captures that exclude a delimiter; support for those can be added
3524
+ * when concrete use cases justify the additional framing rules.
3525
+ *
3526
+ * @group Template literals
3527
+ */
3528
+ export const templateLiteralParser = <const Parts extends TemplateLiteralParts>(
3529
+ ...parts: {
3530
+ readonly [Index in keyof Parts]: ValidateTemplateLiteralPart<Parts[Index]>;
3531
+ } & TemplateLiteralValidation<Parts>
3532
+ ): TemplateLiteralParserType<Parts> =>
3533
+ createTemplateLiteralParserType(parts as unknown as Parts);
3534
+
3535
+ const createTemplateLiteralParserType = <
3536
+ const Parts extends TemplateLiteralParts,
3537
+ >(
3538
+ templateParts: Parts,
3539
+ ): TemplateLiteralParserType<Parts> => {
3540
+ const captureTypes = templateParts.filter(
3541
+ (part): part is TypeNode => typeof part !== "string",
3542
+ ) as unknown as TemplateLiteralCaptureTypes<Parts>;
3543
+ const runtimeCaptureTypes =
3544
+ captureTypes as unknown as NonEmptyReadonlyArray<RuntimeTypeNode>;
3545
+ const output = tuple(...captureTypes);
3546
+ const runtimeOutput = output as TemplateLiteralCaptureTuple<Parts> &
3547
+ RuntimeTypeNode;
3548
+ const reflection = {
3549
+ output,
3550
+ parts: templateParts,
3551
+ [templateLiteralSyntaxSymbol]: true as const,
3552
+ };
3553
+ const parse = compileTemplateLiteralParser(templateParts);
3554
+ const decodeString = (
3555
+ value: string,
3556
+ options: ValidationOptions = firstValidationOptions,
3557
+ ): Result<
3558
+ TemplateLiteralCaptureTuple<Parts>["Output"],
3559
+ TemplateLiteralRuntimeParseError<Parts>
3560
+ > => {
3561
+ const parseResult = parse(value);
3562
+ if (!parseResult.ok) return parseResult;
3563
+
3564
+ const outputResult = validateTupleItems(
3565
+ parseResult.value,
3566
+ runtimeCaptureTypes,
3567
+ (capture, value, captureOptions) =>
3568
+ capture.fromUnknown(value, captureOptions),
3569
+ options,
3570
+ false,
3571
+ );
3572
+ return (outputResult.ok
3573
+ ? outputResult
3574
+ : err({
3575
+ type: "TemplateLiteral",
3576
+ outputError: outputResult.error,
3577
+ })) as Result<
3578
+ TemplateLiteralCaptureTuple<Parts>["Output"],
3579
+ TemplateLiteralRuntimeParseError<Parts>
3580
+ >;
3581
+ };
3582
+ const encodeCaptures = (
3583
+ captures: TemplateLiteralCaptureTuple<Parts>["Output"],
3584
+ ): TemplateLiteralStringOutput<Parts> => {
3585
+ const encodedCaptures = runtimeOutput[encoderSymbol](
3586
+ captures as never,
3587
+ ) as ReadonlyArray<string>;
3588
+ let value = "";
3589
+ let captureIndex = 0;
3590
+
3591
+ for (const part of templateParts) {
3592
+ value +=
3593
+ typeof part === "string" ? part : encodedCaptures[captureIndex++];
3594
+ }
3595
+ return value as TemplateLiteralStringOutput<Parts>;
3596
+ };
3597
+ const canonicalizeString = (
3598
+ value: string,
3599
+ options: ValidationOptions,
3600
+ ): Result<
3601
+ TemplateLiteralStringOutput<Parts>,
3602
+ TemplateLiteralRuntimeParseError<Parts>
3603
+ > => {
3604
+ const result = decodeString(value, options);
3605
+ return result.ok ? ok(encodeCaptures(result.value)) : result;
3606
+ };
3607
+ const validateCanonicalString = (
3608
+ value: unknown,
3609
+ options: ValidationOptions = firstValidationOptions,
3610
+ ): Result<
3611
+ TemplateLiteralStringOutput<Parts>,
3612
+ TypeOfError<"String"> | TemplateLiteralRuntimeParseError<Parts>
3613
+ > => {
3614
+ const stringResult = String.fromUnknown(value, options);
3615
+ if (!stringResult.ok) return stringResult;
3616
+
3617
+ const result = canonicalizeString(stringResult.value, options);
3618
+ if (!result.ok || result.value === stringResult.value) return result;
3619
+
3620
+ return err({ type: "TemplateLiteral", value: stringResult.value });
3621
+ };
3622
+ const getTypeIssues: RuntimeGetTypeIssues = (error, mode) => {
3623
+ if (error.type !== "TemplateLiteral") {
3624
+ return (String as unknown as RuntimeTypeNode)[getRuntimeTypeIssuesSymbol](
3625
+ error,
3626
+ mode,
3627
+ );
3628
+ }
3629
+ if ("outputError" in error) {
3630
+ return runtimeOutput[getRuntimeTypeIssuesSymbol](
3631
+ error.outputError as TypeError,
3632
+ mode,
3633
+ );
3634
+ }
3635
+ return singleRuntimeTypeIssue(
3636
+ "TemplateLiteral",
3637
+ error,
3638
+ formatTemplateLiteralError as TypeErrorFormatter<TypeError>,
3639
+ );
3640
+ };
3641
+ const canonicalStringFromUnknown = (
3642
+ value: unknown,
3643
+ options: ValidationOptions = firstValidationOptions,
3644
+ ): Result<
3645
+ TemplateLiteralStringOutput<Parts>,
3646
+ TypeOfError<"String"> | TemplateLiteralRuntimeParseError<Parts>
3647
+ > => {
3648
+ const stringResult = String.fromUnknown(value, options);
3649
+ return stringResult.ok
3650
+ ? canonicalizeString(stringResult.value, options)
3651
+ : stringResult;
3652
+ };
3653
+ const canonicalStringFrom = createFromOperation(
3654
+ (value, options = firstValidationOptions) =>
3655
+ canonicalizeString(value, options),
3656
+ );
3657
+ const stringType = createTypeNode<TemplateLiteralType<Parts>>(
3658
+ "TemplateLiteral",
3659
+ String,
3660
+ canonicalStringFromUnknown,
3661
+ (value) => validateCanonicalString(value, firstValidationOptions).ok,
3662
+ validateCanonicalString,
3663
+ canonicalStringFrom,
3664
+ identity,
3665
+ getTypeIssues,
3666
+ reflection,
3667
+ ) as TemplateLiteralType<Parts> & RuntimeTypeNode;
3668
+ const fromUnknown = (
3669
+ value: unknown,
3670
+ options: ValidationOptions = firstValidationOptions,
3671
+ ): Result<
3672
+ TemplateLiteralCaptureTuple<Parts>["Output"],
3673
+ InferErrors<TemplateLiteralType<Parts>>
3674
+ > => {
3675
+ const stringResult = String.fromUnknown(value, options);
3676
+ if (!stringResult.ok) return stringResult;
3677
+
3678
+ // The internal parser has one broad signature, but a statically frameless
3679
+ // declaration cannot return its framing error.
3680
+ return decodeString(stringResult.value, options) as Result<
3681
+ TemplateLiteralCaptureTuple<Parts>["Output"],
3682
+ InferErrors<TemplateLiteralType<Parts>>
3683
+ >;
3684
+ };
3685
+ const fromCanonicalString: RuntimeOperation<Result<unknown, TypeError>> = (
3686
+ value: never,
3687
+ options = firstValidationOptions,
3688
+ ) => {
3689
+ String.from(value);
3690
+ const result = decodeString(value, options);
3691
+ if (!result.ok || encodeCaptures(result.value) !== value) {
3692
+ throw new Error("Expected TemplateLiteral.", {
3693
+ cause: result.ok
3694
+ ? ({
3695
+ type: "TemplateLiteral",
3696
+ value,
3697
+ } satisfies TemplateLiteralError)
3698
+ : result.error,
3699
+ });
3700
+ }
3701
+ return result;
3702
+ };
3703
+ const fromString: RuntimeOperation<Result<unknown, TypeError>> = (
3704
+ value: never,
3705
+ options = firstValidationOptions,
3706
+ ) => {
3707
+ String.from(value);
3708
+ return decodeString(value, options);
3709
+ };
3710
+ fromCanonicalString.parent = fromString;
3711
+ const from = createFromOperation(fromCanonicalString);
3712
+ const type = createTypeNode<TemplateLiteralParserType<Parts>>(
3713
+ "TemplateLiteral",
3714
+ stringType,
3715
+ fromUnknown,
3716
+ runtimeOutput.is,
3717
+ runtimeOutput[outputValidationSymbol],
3718
+ from,
3719
+ encodeCaptures,
3720
+ getTypeIssues,
3721
+ reflection,
3722
+ );
3723
+ (type.from as RuntimeOperation<Result<unknown, TypeError>>).parent =
3724
+ fromCanonicalString;
3725
+
3726
+ return type;
3727
+ };
3728
+
3729
+ /** @group Template literals */
3730
+ export interface TemplateLiteralParserType<
3731
+ Parts extends TemplateLiteralParts,
3732
+ > extends Type<
3733
+ "TemplateLiteral",
3734
+ string,
3735
+ TemplateLiteralCaptureTuple<Parts>["Output"],
3736
+ never,
3737
+ TemplateLiteralType<Parts>,
3738
+ InferErrors<TemplateLiteralType<Parts>>,
3739
+ never,
3740
+ TemplateLiteralStringOutput<Parts>,
3741
+ false
3742
+ > {
3743
+ readonly [templateLiteralSyntaxSymbol]: true;
3744
+ readonly [reflectedTypesSymbol]?: TemplateLiteralCaptureTuple<Parts>;
3745
+ readonly output: TemplateLiteralCaptureTuple<Parts>;
3746
+ readonly parts: Parts;
3747
+ }
3748
+
3749
+ /** @group Template literals */
3750
+ export interface TemplateLiteralType<
3751
+ Parts extends TemplateLiteralParts,
3752
+ > extends Type<
3753
+ "TemplateLiteral",
3754
+ string,
3755
+ TemplateLiteralStringOutput<Parts>,
3756
+ TemplateLiteralParseError<Parts>,
3757
+ typeof String,
3758
+ TypeOfError<"String"> | TemplateLiteralParseError<Parts>,
3759
+ never,
3760
+ TemplateLiteralStringOutput<Parts>,
3761
+ true
3762
+ > {
3763
+ readonly [templateLiteralSyntaxSymbol]: true;
3764
+ readonly [reflectedTypesSymbol]?: TemplateLiteralCaptureTuple<Parts>;
3765
+ readonly output: TemplateLiteralCaptureTuple<Parts>;
3766
+ readonly parts: Parts;
3767
+ }
3768
+
3769
+ /**
3770
+ * Template literal {@link Type} for validation.
3771
+ *
3772
+ * Creates a canonical string Type from fixed strings and string-encoded Types.
3773
+ *
3774
+ * Use this factory when Output should remain a string. Switch to
3775
+ * {@link templateLiteralParser} when the individual Type parts should be
3776
+ * decoded into a Tuple.
3777
+ *
3778
+ * ### Example
3779
+ *
3780
+ * ```ts
3781
+ * import { templateLiteral, union } from "@evolu/common";
3782
+ *
3783
+ * const Language = union("en", "cs");
3784
+ * const Region = union("US", "CZ");
3785
+ * const Locale = templateLiteral(Language, "-", Region);
3786
+ *
3787
+ * expectTypeOf<typeof Locale.Output>().toEqualTypeOf<
3788
+ * "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
3789
+ * >();
3790
+ * expectOk(Locale.fromUnknown("cs-CZ"), "cs-CZ");
3791
+ * expect(Locale.is("fr-CZ")).toBe(false);
3792
+ * ```
3793
+ *
3794
+ * @group Template literals
3795
+ */
3796
+ export const templateLiteral = <const Parts extends TemplateLiteralParts>(
3797
+ ...parts: {
3798
+ readonly [Index in keyof Parts]: ValidateTemplateLiteralPart<Parts[Index]>;
3799
+ } & TemplateLiteralValidation<Parts>
3800
+ ): TemplateLiteralType<Parts> =>
3801
+ createTemplateLiteralParserType(parts as unknown as Parts).parent;
3802
+
3803
+ type TemplateLiteralParseError<Parts extends TemplateLiteralParts> =
3804
+ TransformError<
3805
+ "TemplateLiteral",
3806
+ TemplateLiteralIsFrameless<Parts> extends true
3807
+ ? never
3808
+ : TemplateLiteralError,
3809
+ TemplateLiteralCaptureTupleError<Parts>
3810
+ >;
3811
+
3812
+ type TemplateLiteralRuntimeParseError<Parts extends TemplateLiteralParts> =
3813
+ TransformError<
3814
+ "TemplateLiteral",
3815
+ TemplateLiteralError,
3816
+ TemplateLiteralCaptureTupleError<Parts>
3817
+ >;
3818
+
3819
+ type TemplateLiteralCaptureTupleError<Parts extends TemplateLiteralParts> =
3820
+ TupleElementsError<
3821
+ TemplateLiteralCaptureFromStringError<
3822
+ TemplateLiteralCaptureTypes<Parts>[number]
3823
+ >
3824
+ >;
3825
+
3826
+ type TemplateLiteralCaptureFromStringError<T extends TypeNode> =
3827
+ T extends TypeNode
3828
+ ? string extends RootType<T>["Output"]
3829
+ ? TypeFromError<T>
3830
+ : InferErrors<T>
3831
+ : never;
3832
+
3833
+ /** @group Template literals */
3834
+ export interface TemplateLiteralError extends TypeError<"TemplateLiteral"> {
3835
+ readonly value: string;
3836
+ }
3837
+
3838
+ const formatTemplateLiteralError: TypeErrorFormatter<TemplateLiteralError> =
3839
+ (error) =>
3840
+ `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`;
3841
+
3842
+ declare const templateLiteralStringBrandSymbol: unique symbol;
3843
+
3844
+ interface TemplateLiteralStringBrand<Parts extends TemplateLiteralParts> {
3845
+ readonly [templateLiteralStringBrandSymbol]: Parts;
3846
+ }
3847
+
3848
+ type TemplateLiteralPart = string | TypeNode;
3849
+
3850
+ type TemplateLiteralParts = NonEmptyReadonlyArray<TemplateLiteralPart>;
3851
+
3852
+ type TemplateLiteralValidation<Parts extends TemplateLiteralParts> =
3853
+ number extends Parts["length"]
3854
+ ? readonly [ValidationFailure<TemplateLiteralPartsTupleError>]
3855
+ : IsUnion<Parts["length"]> extends true
3856
+ ? readonly [ValidationFailure<TemplateLiteralPartsTupleError>]
3857
+ : [Extract<Parts[number], TypeNode>] extends [never]
3858
+ ? readonly [ValidationFailure<TemplateLiteralCaptureRequiredError>]
3859
+ : TemplateLiteralHasAmbiguousCaptures<Parts> extends true
3860
+ ? readonly [ValidationFailure<TemplateLiteralAmbiguousCapturesError>]
3861
+ : unknown;
3862
+
3863
+ type TemplateLiteralPartsTupleError = CompileTimeError<
3864
+ "TemplateLiteral",
3865
+ "Parts must use one concrete finite non-empty tuple."
3866
+ >;
3867
+
3868
+ type TemplateLiteralCaptureRequiredError = CompileTimeError<
3869
+ "TemplateLiteral",
3870
+ "At least one part must be a Type capture."
3871
+ >;
3872
+
3873
+ type TemplateLiteralAmbiguousCapturesError = CompileTimeError<
3874
+ "TemplateLiteral",
3875
+ "At most one Type capture can have a variable-width string representation."
3876
+ >;
3877
+
3878
+ type TemplateLiteralCaptureTypes<
3879
+ Parts extends ReadonlyArray<TemplateLiteralPart>,
3880
+ Captures extends ReadonlyArray<TypeNode> = readonly [],
3881
+ > = Parts extends readonly [infer Head, ...infer Tail]
3882
+ ? TemplateLiteralCaptureTypes<
3883
+ Extract<Tail, ReadonlyArray<TemplateLiteralPart>>,
3884
+ Head extends TypeNode ? readonly [...Captures, Head] : Captures
3885
+ >
3886
+ : Extract<Captures, NonEmptyReadonlyArray<TypeNode>>;
3887
+
3888
+ type TemplateLiteralCaptureTuple<Parts extends TemplateLiteralParts> =
3889
+ TupleType<TemplateLiteralCaptureTypes<Parts>>;
3890
+
3891
+ type TemplateLiteralCanonicalInput<
3892
+ Parts extends ReadonlyArray<TemplateLiteralPart>,
3893
+ Input extends string = "",
3894
+ > = Parts extends readonly [infer Head, ...infer Tail]
3895
+ ? TemplateLiteralCanonicalInput<
3896
+ Extract<Tail, ReadonlyArray<TemplateLiteralPart>>,
3897
+ `${Input}${TemplateLiteralPartCanonicalInput<Extract<Head, TemplateLiteralPart>>}`
3898
+ >
3899
+ : Input;
3900
+
3901
+ type TemplateLiteralStringOutput<Parts extends TemplateLiteralParts> =
3902
+ AllTypesUseIdentityEncoding<Extract<Parts[number], TypeNode>> extends true
3903
+ ? TemplateLiteralCanonicalInput<Parts>
3904
+ : TemplateLiteralCanonicalInput<Parts> & TemplateLiteralStringBrand<Parts>;
3905
+
3906
+ type TemplateLiteralPartCanonicalInput<Part extends TemplateLiteralPart> =
3907
+ Part extends string
3908
+ ? Part
3909
+ : Part extends TypeNode
3910
+ ? Extract<CanonicalInputOf<Part>, string>
3911
+ : never;
3912
+
3913
+ type ValidateTemplateLiteralPart<Part extends TemplateLiteralPart> =
3914
+ IsUnion<Part> extends false
3915
+ ? Part extends string
3916
+ ? ValidateLiteral<Part>
3917
+ : Part extends ConcreteTypeNode
3918
+ ? IsTemplateLiteralPartType<Part> extends true
3919
+ ? Part
3920
+ : TemplateLiteralPartCompileTimeError
3921
+ : TemplateLiteralPartCompileTimeError
3922
+ : TemplateLiteralPartCompileTimeError;
3923
+
3924
+ type IsTemplateLiteralPartType<T extends TypeNode> = [
3925
+ CanonicalInputOf<T>,
3926
+ ] extends [never]
3927
+ ? false
3928
+ : [CanonicalInputOf<T>] extends [string]
3929
+ ? true
3930
+ : false;
3931
+
3932
+ type TemplateLiteralPartCompileTimeError = CompileTimeError<
3933
+ "TemplateLiteral",
3934
+ "Part must be a raw string literal or a Type canonically encoded as a string."
3935
+ >;
3936
+
3937
+ type TemplateLiteralHasAmbiguousCaptures<
3938
+ Parts extends ReadonlyArray<TemplateLiteralPart>,
3939
+ VariableCaptures extends ReadonlyArray<unknown> = readonly [],
3940
+ > = Parts extends readonly [infer Head, ...infer Tail]
3941
+ ? Head extends TypeNode
3942
+ ? [TemplateLiteralTypeWidth<Head>] extends [null]
3943
+ ? VariableCaptures extends readonly [unknown]
3944
+ ? true
3945
+ : TemplateLiteralHasAmbiguousCaptures<
3946
+ Extract<Tail, ReadonlyArray<TemplateLiteralPart>>,
3947
+ readonly [unknown]
3948
+ >
3949
+ : TemplateLiteralHasAmbiguousCaptures<
3950
+ Extract<Tail, ReadonlyArray<TemplateLiteralPart>>,
3951
+ VariableCaptures
3952
+ >
3953
+ : TemplateLiteralHasAmbiguousCaptures<
3954
+ Extract<Tail, ReadonlyArray<TemplateLiteralPart>>,
3955
+ VariableCaptures
3956
+ >
3957
+ : false;
3958
+
3959
+ type TemplateLiteralTypeWidth<T extends TypeNode> =
3960
+ T extends LiteralType<infer Expected extends string>
3961
+ ? TemplateLiteralStringWidth<Expected>
3962
+ : T extends UnionType<infer Members>
3963
+ ? NormalizeTemplateLiteralWidth<TemplateLiteralTypeWidth<Members[number]>>
3964
+ : T extends TemplateLiteralParserType<infer Parts>
3965
+ ? TemplateLiteralPartsWidth<Parts>
3966
+ : T extends TemplateLiteralType<infer Parts>
3967
+ ? TemplateLiteralPartsWidth<Parts>
3968
+ : T["parent"] extends infer Parent extends TypeNode
3969
+ ? TemplateLiteralTypeWidth<Parent>
3970
+ : null;
3971
+
3972
+ type NormalizeTemplateLiteralWidth<Width> =
3973
+ IsUnion<Width> extends true
3974
+ ? null
3975
+ : Width extends ReadonlyArray<unknown>
3976
+ ? Width
3977
+ : null;
3978
+
3979
+ type TemplateLiteralPartsWidth<
3980
+ Parts extends ReadonlyArray<TemplateLiteralPart>,
3981
+ Width extends ReadonlyArray<unknown> = readonly [],
3982
+ > = Parts extends readonly [infer Head, ...infer Tail]
3983
+ ? TemplateLiteralPartWidth<
3984
+ Extract<Head, TemplateLiteralPart>
3985
+ > extends infer PartWidth
3986
+ ? [PartWidth] extends [ReadonlyArray<unknown>]
3987
+ ? TemplateLiteralPartsWidth<
3988
+ Extract<Tail, ReadonlyArray<TemplateLiteralPart>>,
3989
+ readonly [...Width, ...PartWidth]
3990
+ >
3991
+ : null
3992
+ : never
3993
+ : Width;
3994
+
3995
+ type TemplateLiteralPartWidth<Part extends TemplateLiteralPart> =
3996
+ Part extends string
3997
+ ? TemplateLiteralStringWidth<Part>
3998
+ : Part extends TypeNode
3999
+ ? TemplateLiteralTypeWidth<Part>
4000
+ : never;
4001
+
4002
+ type TemplateLiteralIsFrameless<
4003
+ Parts extends ReadonlyArray<TemplateLiteralPart>,
4004
+ HasVariableCapture extends boolean = false,
4005
+ > = Parts extends readonly [infer Head, ...infer Tail]
4006
+ ? TemplateLiteralPartWidth<
4007
+ Extract<Head, TemplateLiteralPart>
4008
+ > extends infer Width
4009
+ ? [Width] extends [null]
4010
+ ? HasVariableCapture extends true
4011
+ ? false
4012
+ : TemplateLiteralIsFrameless<
4013
+ Extract<Tail, ReadonlyArray<TemplateLiteralPart>>,
4014
+ true
4015
+ >
4016
+ : [Width] extends [readonly []]
4017
+ ? TemplateLiteralIsFrameless<
4018
+ Extract<Tail, ReadonlyArray<TemplateLiteralPart>>,
4019
+ HasVariableCapture
4020
+ >
4021
+ : false
4022
+ : false
4023
+ : HasVariableCapture;
4024
+
4025
+ type TemplateLiteralStringWidth<
4026
+ Value extends string,
4027
+ Width extends ReadonlyArray<unknown> = readonly [],
4028
+ > = string extends Value
4029
+ ? null
4030
+ : Value extends ""
4031
+ ? Width
4032
+ : Value extends `${infer _CodePoint}${infer Tail}`
4033
+ ? TemplateLiteralStringWidth<Tail, readonly [...Width, unknown]>
4034
+ : null;
4035
+
4036
+ interface TemplateLiteralFraming {
4037
+ readonly width: number | null;
4038
+ readonly canBeEmpty: boolean;
4039
+ readonly canStartWithLowSurrogate: boolean;
4040
+ readonly canEndWithHighSurrogate: boolean;
4041
+ }
4042
+
4043
+ const compileTemplateLiteralParser = <Parts extends TemplateLiteralParts>(
4044
+ parts: Parts,
4045
+ ): ((
4046
+ input: string,
4047
+ ) => Result<
4048
+ TemplateLiteralCaptureTuple<Parts>["Input"],
4049
+ TemplateLiteralError
4050
+ >) => {
4051
+ let framing = emptyTemplateLiteralFraming;
4052
+ let fixedPartsWidth = 0;
4053
+ const compiledParts = parts.map((part) => {
4054
+ const partFraming = getTemplateLiteralPartFraming(part);
4055
+ framing = concatenateTemplateLiteralFraming(framing, partFraming);
4056
+ fixedPartsWidth += partFraming.width ?? 0;
4057
+ return [part, partFraming.width] as const;
4058
+ });
4059
+
4060
+ return (input) => {
4061
+ const inputCodePoints = globalThis.Array.from(input);
4062
+ const variableWidth = inputCodePoints.length - fixedPartsWidth;
4063
+ if (variableWidth < 0) {
4064
+ return err({ type: "TemplateLiteral", value: input });
4065
+ }
4066
+
4067
+ const captures: Array<string> = [];
4068
+ let position = 0;
4069
+
4070
+ for (const [part, width] of compiledParts) {
4071
+ const partWidth = width ?? variableWidth;
4072
+ const value = inputCodePoints
4073
+ .slice(position, position + partWidth)
4074
+ .join("");
4075
+
4076
+ if (typeof part === "string") {
4077
+ if (value !== part) {
4078
+ return err({ type: "TemplateLiteral", value: input });
4079
+ }
4080
+ } else {
4081
+ captures.push(value);
4082
+ }
4083
+ position += partWidth;
4084
+ }
4085
+
4086
+ return position === inputCodePoints.length
4087
+ ? ok(
4088
+ captures as unknown as TemplateLiteralCaptureTuple<Parts>["Input"],
4089
+ )
4090
+ : err({ type: "TemplateLiteral", value: input });
4091
+ };
4092
+ };
4093
+
4094
+ const emptyTemplateLiteralFraming: TemplateLiteralFraming = {
4095
+ width: 0,
4096
+ canBeEmpty: true,
4097
+ canStartWithLowSurrogate: false,
4098
+ canEndWithHighSurrogate: false,
4099
+ };
4100
+
4101
+ const unknownTemplateLiteralFraming: TemplateLiteralFraming = {
4102
+ width: null,
4103
+ canBeEmpty: true,
4104
+ canStartWithLowSurrogate: true,
4105
+ canEndWithHighSurrogate: true,
4106
+ };
4107
+
4108
+ const concatenateTemplateLiteralFraming = (
4109
+ left: TemplateLiteralFraming,
4110
+ right: TemplateLiteralFraming,
4111
+ ): TemplateLiteralFraming => {
4112
+ assert(
4113
+ !(left.canEndWithHighSurrogate && right.canStartWithLowSurrogate),
4114
+ "A TemplateLiteral cannot form a Unicode surrogate pair across part boundaries.",
4115
+ );
4116
+
4117
+ return {
4118
+ width:
4119
+ left.width !== null && right.width !== null
4120
+ ? left.width + right.width
4121
+ : null,
4122
+ canBeEmpty: left.canBeEmpty && right.canBeEmpty,
4123
+ canStartWithLowSurrogate:
4124
+ left.canStartWithLowSurrogate ||
4125
+ (left.canBeEmpty && right.canStartWithLowSurrogate),
4126
+ canEndWithHighSurrogate:
4127
+ right.canEndWithHighSurrogate ||
4128
+ (right.canBeEmpty && left.canEndWithHighSurrogate),
4129
+ };
4130
+ };
4131
+
4132
+ const getStringTemplateLiteralFraming = (
4133
+ value: string,
4134
+ ): TemplateLiteralFraming => {
4135
+ const firstCodeUnit = value.charCodeAt(0);
4136
+ const lastCodeUnit = value.charCodeAt(value.length - 1);
4137
+
4138
+ return {
4139
+ width: globalThis.Array.from(value).length,
4140
+ canBeEmpty: value.length === 0,
4141
+ canStartWithLowSurrogate:
4142
+ firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff,
4143
+ canEndWithHighSurrogate: lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff,
4144
+ };
4145
+ };
4146
+
4147
+ const getTemplateLiteralPartFraming = (
4148
+ part: TemplateLiteralPart,
4149
+ ): TemplateLiteralFraming => {
4150
+ if (typeof part === "string") return getStringTemplateLiteralFraming(part);
4151
+
4152
+ const type = part as RuntimeTypeNode;
4153
+ if (type[templateLiteralSyntaxSymbol] === true) {
4154
+ if (type.name === "Literal") {
4155
+ return getStringTemplateLiteralFraming(
4156
+ (type as unknown as LiteralType<string>).expected,
4157
+ );
4158
+ }
4159
+ if (type.name === "Union") {
4160
+ const memberFramings = (
4161
+ type as unknown as RuntimeUnionTypeNode
4162
+ ).members.map(getTemplateLiteralPartFraming);
4163
+ const width = memberFramings[0].width;
4164
+
4165
+ return {
4166
+ width:
4167
+ width !== null &&
4168
+ memberFramings.every((framing) => framing.width === width)
4169
+ ? width
4170
+ : null,
4171
+ canBeEmpty: memberFramings.some((framing) => framing.canBeEmpty),
4172
+ canStartWithLowSurrogate: memberFramings.some(
4173
+ (framing) => framing.canStartWithLowSurrogate,
4174
+ ),
4175
+ canEndWithHighSurrogate: memberFramings.some(
4176
+ (framing) => framing.canEndWithHighSurrogate,
4177
+ ),
4178
+ };
4179
+ }
4180
+ if (type.name === "TemplateLiteral") {
4181
+ return (
4182
+ type as unknown as { readonly parts: TemplateLiteralParts }
4183
+ ).parts.reduce(
4184
+ (framing, part) =>
4185
+ concatenateTemplateLiteralFraming(
4186
+ framing,
4187
+ getTemplateLiteralPartFraming(part),
4188
+ ),
4189
+ emptyTemplateLiteralFraming,
4190
+ );
4191
+ }
4192
+ }
4193
+ if (type.parent === null) return unknownTemplateLiteralFraming;
4194
+
4195
+ return getTemplateLiteralPartFraming(type.parent);
4196
+ };
4197
+
3329
4198
  /**
3330
4199
  * Branded {@link Type}.
3331
4200
  *
@@ -3809,7 +4678,7 @@ export const CapitalizedString = /*#__PURE__*/ capitalized(String);
3809
4678
  export type CapitalizedString = typeof CapitalizedString.Output;
3810
4679
 
3811
4680
  /**
3812
- * Adds a {@link Brand} requiring a string without surrounding whitespace.
4681
+ * String {@link Brand} without surrounding whitespace.
3813
4682
  *
3814
4683
  * @group String
3815
4684
  */
@@ -3855,7 +4724,7 @@ export const trim = (value: string): TrimmedString =>
3855
4724
  value.trim() as TrimmedString;
3856
4725
 
3857
4726
  /**
3858
- * Adds a {@link Brand} requiring a value to have at least `min` items.
4727
+ * Minimum-length {@link Brand} requiring a value to have at least `min` items.
3859
4728
  *
3860
4729
  * @group String
3861
4730
  * @group Collection
@@ -3905,7 +4774,7 @@ export const NonEmptyTrimmedString = /*#__PURE__*/ minLength(1)(TrimmedString);
3905
4774
  export type NonEmptyTrimmedString = typeof NonEmptyTrimmedString.Output;
3906
4775
 
3907
4776
  /**
3908
- * Adds a {@link Brand} requiring a value to have at most `max` items.
4777
+ * Maximum-length {@link Brand} requiring a value to have at most `max` items.
3909
4778
  *
3910
4779
  * @group String
3911
4780
  * @group Collection
@@ -3958,7 +4827,7 @@ export const NonEmptyTrimmedString1000 = /*#__PURE__*/ maxLength(1000)(
3958
4827
  export type NonEmptyTrimmedString1000 = typeof NonEmptyTrimmedString1000.Output;
3959
4828
 
3960
4829
  /**
3961
- * Adds a {@link Brand} requiring a value to have exactly `exact` items.
4830
+ * Exact-length {@link Brand} requiring a value to have exactly `exact` items.
3962
4831
  *
3963
4832
  * @group String
3964
4833
  * @group Collection
@@ -3991,7 +4860,7 @@ export interface LengthError<
3991
4860
  }
3992
4861
 
3993
4862
  /**
3994
- * Creates a string {@link Brand} that must match a regular expression.
4863
+ * String {@link Brand} constrained by a regular expression.
3995
4864
  *
3996
4865
  * ### Example
3997
4866
  *
@@ -4318,7 +5187,7 @@ export const createIdAsUuidv7 = <B extends string = never>(
4318
5187
  };
4319
5188
 
4320
5189
  /**
4321
- * A table-specific {@link Id} Type.
5190
+ * Table-specific {@link Id} Type.
4322
5191
  *
4323
5192
  * @group String
4324
5193
  */
@@ -4475,7 +5344,7 @@ export const Int64FromInt64String = /*#__PURE__*/ transform(
4475
5344
  );
4476
5345
 
4477
5346
  /**
4478
- * Adds a {@link Brand} requiring a number greater than or equal to zero.
5347
+ * Number {@link Brand} requiring a value greater than or equal to zero.
4479
5348
  *
4480
5349
  * @group Number
4481
5350
  */
@@ -4507,7 +5376,7 @@ export const NonNegativeNumber = /*#__PURE__*/ nonNegative(Number);
4507
5376
  export type NonNegativeNumber = typeof NonNegativeNumber.Output;
4508
5377
 
4509
5378
  /**
4510
- * Adds a {@link Brand} requiring a number greater than zero.
5379
+ * Number {@link Brand} requiring a value greater than zero.
4511
5380
  *
4512
5381
  * @group Number
4513
5382
  */
@@ -4540,7 +5409,7 @@ export const PositiveNumber = /*#__PURE__*/ positive(NonNegativeNumber);
4540
5409
  export type PositiveNumber = typeof PositiveNumber.Output;
4541
5410
 
4542
5411
  /**
4543
- * Adds a {@link Brand} requiring a number less than or equal to zero.
5412
+ * Number {@link Brand} requiring a value less than or equal to zero.
4544
5413
  *
4545
5414
  * @group Number
4546
5415
  */
@@ -4572,7 +5441,7 @@ export const NonPositiveNumber = /*#__PURE__*/ nonPositive(Number);
4572
5441
  export type NonPositiveNumber = typeof NonPositiveNumber.Output;
4573
5442
 
4574
5443
  /**
4575
- * Adds a {@link Brand} requiring a number less than zero.
5444
+ * Number {@link Brand} requiring a value less than zero.
4576
5445
  *
4577
5446
  * @group Number
4578
5447
  */
@@ -4605,7 +5474,7 @@ export const NegativeNumber = /*#__PURE__*/ negative(NonPositiveNumber);
4605
5474
  export type NegativeNumber = typeof NegativeNumber.Output;
4606
5475
 
4607
5476
  /**
4608
- * Adds a {@link Brand} requiring a number other than `NaN`.
5477
+ * Number {@link Brand} requiring a value other than `NaN`.
4609
5478
  *
4610
5479
  * @group Number
4611
5480
  */
@@ -4639,7 +5508,7 @@ export const NonNaNNumber = /*#__PURE__*/ nonNaN(Number);
4639
5508
  export type NonNaNNumber = typeof NonNaNNumber.Output;
4640
5509
 
4641
5510
  /**
4642
- * Adds a {@link Brand} requiring a finite number.
5511
+ * Number {@link Brand} requiring a finite value.
4643
5512
  *
4644
5513
  * @group Number
4645
5514
  */
@@ -4741,13 +5610,6 @@ export type Int = typeof Int.Output;
4741
5610
  export const NonNegativeInt = /*#__PURE__*/ nonNegative(Int);
4742
5611
  export type NonNegativeInt = typeof NonNegativeInt.Output;
4743
5612
 
4744
- /**
4745
- * 0-100 as a literal, or any already-validated {@link NonNegativeInt}.
4746
- *
4747
- * @group Number
4748
- */
4749
- export type Int0To100OrNonNegativeInt = 0 | Int1To100 | NonNegativeInt;
4750
-
4751
5613
  /**
4752
5614
  * Minimum {@link NonNegativeInt} value.
4753
5615
  *
@@ -4766,13 +5628,6 @@ export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
4766
5628
  export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
4767
5629
  export type PositiveInt = typeof PositiveInt.Output;
4768
5630
 
4769
- /**
4770
- * 1-100 as a literal, or any already-validated {@link PositiveInt}.
4771
- *
4772
- * @group Number
4773
- */
4774
- export type Int1To100OrPositiveInt = Int1To100 | PositiveInt;
4775
-
4776
5631
  /**
4777
5632
  * Minimum {@link PositiveInt} value.
4778
5633
  *
@@ -4808,7 +5663,7 @@ export const NegativeInt = /*#__PURE__*/ negative(NonPositiveInt);
4808
5663
  export type NegativeInt = typeof NegativeInt.Output;
4809
5664
 
4810
5665
  /**
4811
- * Adds a {@link Brand} requiring a number greater than `min`.
5666
+ * Number {@link Brand} requiring a value greater than `min`.
4812
5667
  *
4813
5668
  * @group Number
4814
5669
  */
@@ -4840,7 +5695,7 @@ export interface GreaterThanError<
4840
5695
  }
4841
5696
 
4842
5697
  /**
4843
- * Adds a {@link Brand} requiring a number greater than or equal to `min`.
5698
+ * Number {@link Brand} requiring a value greater than or equal to `min`.
4844
5699
  *
4845
5700
  * @group Number
4846
5701
  */
@@ -4880,7 +5735,7 @@ export interface GreaterThanOrEqualToError<
4880
5735
  }
4881
5736
 
4882
5737
  /**
4883
- * Adds a {@link Brand} requiring a number less than `max`.
5738
+ * Number {@link Brand} requiring a value less than `max`.
4884
5739
  *
4885
5740
  * @group Number
4886
5741
  */
@@ -4923,7 +5778,7 @@ export const Age = /*#__PURE__*/ brand(
4923
5778
  export type Age = typeof Age.Output;
4924
5779
 
4925
5780
  /**
4926
- * Adds a {@link Brand} requiring a number less than or equal to `max`.
5781
+ * Number {@link Brand} requiring a value less than or equal to `max`.
4927
5782
  *
4928
5783
  * @group Number
4929
5784
  */
@@ -4972,51 +5827,124 @@ export const Ratio = /*#__PURE__*/ brand(
4972
5827
  export type Ratio = typeof Ratio.Output;
4973
5828
 
4974
5829
  /**
4975
- * Canonical string representation of a positive base-10 decimal value.
5830
+ * Canonical string representation of a signed base-10 decimal value.
4976
5831
  *
4977
5832
  * Use this Type when a decimal value must remain exact instead of being
4978
5833
  * converted to an IEEE-754 number. Equivalent values have one accepted
4979
- * representation, so leading zeroes, trailing fractional zeroes, signs, and
4980
- * exponent notation are rejected.
5834
+ * representation, so leading zeroes, trailing fractional zeroes, `-0`, plus
5835
+ * signs, and exponent notation are rejected.
4981
5836
  *
4982
5837
  * The decoded value remains a string. Arithmetic requires an explicit decimal
4983
5838
  * or fixed-point representation.
4984
5839
  *
5840
+ * TypeScript template literal types can describe a fixed number of digit
5841
+ * positions, but not the arbitrarily long integer and fractional parts accepted
5842
+ * here. `DecimalString` therefore uses a {@link Brand} so its TypeScript type
5843
+ * does not accept strings that have not been validated.
5844
+ *
5845
+ * Use these predefined Types or their corresponding factories to add sign
5846
+ * constraints to compatible decimal string Types:
5847
+ *
5848
+ * - {@link NonNegativeDecimalString} / {@link nonNegativeDecimalString}
5849
+ * - {@link PositiveDecimalString} / {@link positiveDecimalString}
5850
+ * - {@link NonPositiveDecimalString} / {@link nonPositiveDecimalString}
5851
+ * - {@link NegativeDecimalString} / {@link negativeDecimalString}
5852
+ *
4985
5853
  * ### Example
4986
5854
  *
4987
5855
  * ```ts
4988
- * import { PositiveDecimalString } from "@evolu/common";
5856
+ * import { DecimalString } from "@evolu/common";
4989
5857
  *
4990
- * expectOk(PositiveDecimalString.fromUnknown("0.3"), "0.3");
4991
- * expectOk(PositiveDecimalString.fromUnknown("25"), "25");
4992
- * expectOk(PositiveDecimalString.fromUnknown("10.01"), "10.01");
5858
+ * expectOk(DecimalString.fromUnknown("-10.25"), "-10.25");
5859
+ * expectOk(DecimalString.fromUnknown("0"), "0");
5860
+ * expectOk(DecimalString.fromUnknown("10.25"), "10.25");
4993
5861
  *
4994
- * expectErr(PositiveDecimalString.fromUnknown("0"), {
4995
- * type: "PositiveDecimalString",
4996
- * value: "0",
4997
- * });
4998
- * expectErr(PositiveDecimalString.fromUnknown("0.30"), {
4999
- * type: "PositiveDecimalString",
5000
- * value: "0.30",
5862
+ * expectErr(DecimalString.fromUnknown("10.250"), {
5863
+ * type: "DecimalString",
5864
+ * value: "10.250",
5001
5865
  * });
5002
5866
  * ```
5003
5867
  *
5004
5868
  * @group Number
5005
5869
  */
5006
- export const PositiveDecimalString = /*#__PURE__*/ brand(
5007
- "PositiveDecimalString",
5870
+ export const DecimalString = /*#__PURE__*/ brand(
5871
+ "DecimalString",
5008
5872
  String,
5009
5873
  (value) =>
5010
- /^(?:[1-9]\d*|(?:0|[1-9]\d*)\.\d*[1-9])$/.test(value)
5874
+ /^(?:0|-?(?:[1-9]\d*|(?:0|[1-9]\d*)\.\d*[1-9]))$/.test(value)
5011
5875
  ? ok()
5012
- : err<PositiveDecimalStringError>({
5013
- type: "PositiveDecimalString",
5014
- value,
5015
- }),
5876
+ : err<DecimalStringError>({ type: "DecimalString", value }),
5016
5877
  (error) =>
5017
- `The value ${safelyStringifyUnknownValue(error.value)} must be a canonical positive decimal string.`,
5878
+ `The value ${safelyStringifyUnknownValue(error.value)} must be a canonical decimal string.`,
5018
5879
  );
5019
- export type PositiveDecimalString = typeof PositiveDecimalString.Output;
5880
+ export type DecimalString = typeof DecimalString.Output;
5881
+
5882
+ /** @group Number */
5883
+ export interface DecimalStringError extends TypeError<"DecimalString"> {
5884
+ readonly value: string;
5885
+ }
5886
+
5887
+ /**
5888
+ * {@link DecimalString} Brand requiring a value greater than or equal to zero.
5889
+ *
5890
+ * @group Number
5891
+ */
5892
+ export const nonNegativeDecimalString: BrandFactory<
5893
+ "NonNegativeDecimalString",
5894
+ DecimalString,
5895
+ NonNegativeDecimalStringError
5896
+ > = (parent) =>
5897
+ brand(
5898
+ "NonNegativeDecimalString",
5899
+ parent,
5900
+ (value) =>
5901
+ value[0] !== "-"
5902
+ ? ok()
5903
+ : err<NonNegativeDecimalStringError>({
5904
+ type: "NonNegativeDecimalString",
5905
+ value,
5906
+ }),
5907
+ (error) =>
5908
+ `The value ${safelyStringifyUnknownValue(error.value)} must be a non-negative decimal string.`,
5909
+ );
5910
+
5911
+ /** @group Number */
5912
+ export interface NonNegativeDecimalStringError extends TypeError<"NonNegativeDecimalString"> {
5913
+ readonly value: string;
5914
+ }
5915
+
5916
+ /**
5917
+ * Non-negative {@link DecimalString}.
5918
+ *
5919
+ * @group Number
5920
+ */
5921
+ export const NonNegativeDecimalString =
5922
+ /*#__PURE__*/ nonNegativeDecimalString(DecimalString);
5923
+ export type NonNegativeDecimalString = typeof NonNegativeDecimalString.Output;
5924
+
5925
+ /**
5926
+ * {@link DecimalString} Brand requiring a value greater than zero.
5927
+ *
5928
+ * @group Number
5929
+ */
5930
+ export const positiveDecimalString: BrandFactory<
5931
+ "PositiveDecimalString",
5932
+ DecimalString,
5933
+ PositiveDecimalStringError
5934
+ > = (parent) =>
5935
+ brand(
5936
+ "PositiveDecimalString",
5937
+ parent,
5938
+ (value) =>
5939
+ value !== "0" && value[0] !== "-"
5940
+ ? ok()
5941
+ : err<PositiveDecimalStringError>({
5942
+ type: "PositiveDecimalString",
5943
+ value,
5944
+ }),
5945
+ (error) =>
5946
+ `The value ${safelyStringifyUnknownValue(error.value)} must be a positive decimal string.`,
5947
+ );
5020
5948
 
5021
5949
  /** @group Number */
5022
5950
  export interface PositiveDecimalStringError extends TypeError<"PositiveDecimalString"> {
@@ -5024,8 +5952,100 @@ export interface PositiveDecimalStringError extends TypeError<"PositiveDecimalSt
5024
5952
  }
5025
5953
 
5026
5954
  /**
5027
- * Adds a {@link Brand} requiring a number to be a multiple of an exact decimal
5028
- * `divisor`.
5955
+ * Positive {@link DecimalString}.
5956
+ *
5957
+ * Also satisfies {@link NonNegativeDecimalString}, so it can be used wherever a
5958
+ * non-negative decimal string is required.
5959
+ *
5960
+ * @group Number
5961
+ */
5962
+ export const PositiveDecimalString = /*#__PURE__*/ positiveDecimalString(
5963
+ NonNegativeDecimalString,
5964
+ );
5965
+ export type PositiveDecimalString = typeof PositiveDecimalString.Output;
5966
+
5967
+ /**
5968
+ * {@link DecimalString} Brand requiring a value less than or equal to zero.
5969
+ *
5970
+ * @group Number
5971
+ */
5972
+ export const nonPositiveDecimalString: BrandFactory<
5973
+ "NonPositiveDecimalString",
5974
+ DecimalString,
5975
+ NonPositiveDecimalStringError
5976
+ > = (parent) =>
5977
+ brand(
5978
+ "NonPositiveDecimalString",
5979
+ parent,
5980
+ (value) =>
5981
+ value === "0" || value[0] === "-"
5982
+ ? ok()
5983
+ : err<NonPositiveDecimalStringError>({
5984
+ type: "NonPositiveDecimalString",
5985
+ value,
5986
+ }),
5987
+ (error) =>
5988
+ `The value ${safelyStringifyUnknownValue(error.value)} must be a non-positive decimal string.`,
5989
+ );
5990
+
5991
+ /** @group Number */
5992
+ export interface NonPositiveDecimalStringError extends TypeError<"NonPositiveDecimalString"> {
5993
+ readonly value: string;
5994
+ }
5995
+
5996
+ /**
5997
+ * Non-positive {@link DecimalString}.
5998
+ *
5999
+ * @group Number
6000
+ */
6001
+ export const NonPositiveDecimalString =
6002
+ /*#__PURE__*/ nonPositiveDecimalString(DecimalString);
6003
+ export type NonPositiveDecimalString = typeof NonPositiveDecimalString.Output;
6004
+
6005
+ /**
6006
+ * {@link DecimalString} Brand requiring a value less than zero.
6007
+ *
6008
+ * @group Number
6009
+ */
6010
+ export const negativeDecimalString: BrandFactory<
6011
+ "NegativeDecimalString",
6012
+ DecimalString,
6013
+ NegativeDecimalStringError
6014
+ > = (parent) =>
6015
+ brand(
6016
+ "NegativeDecimalString",
6017
+ parent,
6018
+ (value) =>
6019
+ value[0] === "-"
6020
+ ? ok()
6021
+ : err<NegativeDecimalStringError>({
6022
+ type: "NegativeDecimalString",
6023
+ value,
6024
+ }),
6025
+ (error) =>
6026
+ `The value ${safelyStringifyUnknownValue(error.value)} must be a negative decimal string.`,
6027
+ );
6028
+
6029
+ /** @group Number */
6030
+ export interface NegativeDecimalStringError extends TypeError<"NegativeDecimalString"> {
6031
+ readonly value: string;
6032
+ }
6033
+
6034
+ /**
6035
+ * Negative {@link DecimalString}.
6036
+ *
6037
+ * Also satisfies {@link NonPositiveDecimalString}, so it can be used wherever a
6038
+ * non-positive decimal string is required.
6039
+ *
6040
+ * @group Number
6041
+ */
6042
+ export const NegativeDecimalString = /*#__PURE__*/ negativeDecimalString(
6043
+ NonPositiveDecimalString,
6044
+ );
6045
+ export type NegativeDecimalString = typeof NegativeDecimalString.Output;
6046
+
6047
+ /**
6048
+ * Number {@link Brand} requiring an exact decimal multiple of `divisor`.
5029
6049
  *
5030
6050
  * The divisor must be one canonical positive decimal string literal because its
5031
6051
  * exact value is encoded in the resulting Brand name. The declaration is
@@ -5175,7 +6195,7 @@ const decimalStringToParts = (value: string): DecimalParts => {
5175
6195
  };
5176
6196
 
5177
6197
  /**
5178
- * Adds a {@link Brand} requiring a number to be within an inclusive range.
6198
+ * Number {@link Brand} requiring a value within an inclusive range.
5179
6199
  *
5180
6200
  * @group Number
5181
6201
  */
@@ -6465,6 +7485,106 @@ const validateTupleItems = (
6465
7485
  checkStructure,
6466
7486
  );
6467
7487
 
7488
+ /**
7489
+ * Decimal digit from `"0"` to `"9"`.
7490
+ *
7491
+ * @group String
7492
+ */
7493
+ export const Digit = /*#__PURE__*/ union(
7494
+ "0",
7495
+ "1",
7496
+ "2",
7497
+ "3",
7498
+ "4",
7499
+ "5",
7500
+ "6",
7501
+ "7",
7502
+ "8",
7503
+ "9",
7504
+ );
7505
+ export type Digit = typeof Digit.Output;
7506
+
7507
+ /**
7508
+ * Decimal digit from `"1"` to `"9"`.
7509
+ *
7510
+ * @group String
7511
+ */
7512
+ export const Digit1To9 = /*#__PURE__*/ union(
7513
+ "1",
7514
+ "2",
7515
+ "3",
7516
+ "4",
7517
+ "5",
7518
+ "6",
7519
+ "7",
7520
+ "8",
7521
+ "9",
7522
+ );
7523
+ export type Digit1To9 = typeof Digit1To9.Output;
7524
+
7525
+ /**
7526
+ * Decimal string from `"1"` to `"6"`.
7527
+ *
7528
+ * @group String
7529
+ */
7530
+ export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
7531
+ export type Digit1To6 = typeof Digit1To6.Output;
7532
+
7533
+ /**
7534
+ * Decimal string from `"1"` to `"23"`.
7535
+ *
7536
+ * @group String
7537
+ */
7538
+ export const Digit1To23 = /*#__PURE__*/ union(
7539
+ Digit1To9,
7540
+ /*#__PURE__*/ templateLiteral("1", Digit),
7541
+ /*#__PURE__*/ templateLiteral(
7542
+ "2",
7543
+ /*#__PURE__*/ union("0", "1", "2", "3"),
7544
+ ),
7545
+ );
7546
+ export type Digit1To23 = typeof Digit1To23.Output;
7547
+
7548
+ /**
7549
+ * Decimal string from `"1"` to `"51"`.
7550
+ *
7551
+ * @group String
7552
+ */
7553
+ export const Digit1To51 = /*#__PURE__*/ union(
7554
+ Digit1To9,
7555
+ /*#__PURE__*/ templateLiteral(
7556
+ /*#__PURE__*/ union("1", "2", "3", "4"),
7557
+ Digit,
7558
+ ),
7559
+ /*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")),
7560
+ );
7561
+ export type Digit1To51 = typeof Digit1To51.Output;
7562
+
7563
+ /**
7564
+ * Decimal string from `"1"` to `"99"`.
7565
+ *
7566
+ * @group String
7567
+ */
7568
+ export const Digit1To99 = /*#__PURE__*/ union(
7569
+ Digit1To9,
7570
+ /*#__PURE__*/ templateLiteral(Digit1To9, Digit),
7571
+ );
7572
+ export type Digit1To99 = typeof Digit1To99.Output;
7573
+
7574
+ /**
7575
+ * Decimal string from `"1"` to `"59"`.
7576
+ *
7577
+ * @group String
7578
+ */
7579
+ export const Digit1To59 = /*#__PURE__*/ union(
7580
+ Digit1To9,
7581
+ /*#__PURE__*/ templateLiteral(
7582
+ /*#__PURE__*/ union("1", "2", "3", "4", "5"),
7583
+ Digit,
7584
+ ),
7585
+ );
7586
+ export type Digit1To59 = typeof Digit1To59.Output;
7587
+
6468
7588
  const createObjectRuntimeTypeIssues =
6469
7589
  (
6470
7590
  defaultFormatter: TypeErrorFormatter<TypeError>,
@@ -8489,7 +9609,7 @@ const createRecordPropertyError = <Error extends TypeError>(
8489
9609
  });
8490
9610
 
8491
9611
  /**
8492
- * Creates an {@link object} Type with every property optional.
9612
+ * Object {@link Type} with every property optional.
8493
9613
  *
8494
9614
  * No property is required, but every present property must still satisfy its
8495
9615
  * Type.
@@ -8539,7 +9659,8 @@ export type PartialObjectProps<Props extends ObjectProps> = {
8539
9659
  };
8540
9660
 
8541
9661
  /**
8542
- * Makes every property whose Union Type includes {@link Null} optional.
9662
+ * Object {@link Type} making every property whose Union Type includes
9663
+ * {@link Null} optional.
8543
9664
  *
8544
9665
  * The property retains its original Union Type, so consumers may omit it, set
8545
9666
  * it to `null`, or provide any other member of that Union. Properties without
@@ -8610,7 +9731,7 @@ export type NullableToOptionalProps<Props extends ObjectProps> = {
8610
9731
  };
8611
9732
 
8612
9733
  /**
8613
- * Creates an {@link object} Type without the selected declared properties.
9734
+ * Object {@link Type} without the selected declared properties.
8614
9735
  *
8615
9736
  * @group Objects
8616
9737
  */
@@ -8664,7 +9785,7 @@ type OmitKeyConcreteTypeError = CompileTimeError<
8664
9785
  >;
8665
9786
 
8666
9787
  /**
8667
- * Creates a {@link Type} for {@link Result} values.
9788
+ * {@link Result} {@link Type} for typed success and error values.
8668
9789
  *
8669
9790
  * Use this to validate Results crossing a storage, worker, API, or other
8670
9791
  * serialization boundary. The operation returns an outer validation Result. Its
@@ -8958,7 +10079,7 @@ type TypedTypePropertyError = CompileTimeError<
8958
10079
  >;
8959
10080
 
8960
10081
  /**
8961
- * Creates a {@link Type} for a producer's value, error, or done {@link Result}.
10082
+ * Producer-result {@link Type} for value, error, or done outcomes.
8962
10083
  *
8963
10084
  * The three outcomes are `Ok<Value>`, `Err<Error>`, and `Err<Typed<"Done"> & {
8964
10085
  * done: Done }>`. This keeps normal completion distinct from failure while
@@ -10593,7 +11714,7 @@ export const JsonValueFromJson = /*#__PURE__*/ transform(
10593
11714
  );
10594
11715
 
10595
11716
  /**
10596
- * Creates a branded {@link Json} Type and total conversions for another Type.
11717
+ * Branded {@link Json} Type and total conversions for another Type.
10597
11718
  *
10598
11719
  * Use this factory when a domain value must be stored as JSON text while its
10599
11720
  * exact Type remains visible to TypeScript, such as a JSON column in an Evolu