@evolu/common 8.0.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
  */
@@ -4676,6 +5545,16 @@ export type FiniteNumber = typeof FiniteNumber.Output;
4676
5545
  export const NonNegativeFiniteNumber = /*#__PURE__*/ nonNegative(FiniteNumber);
4677
5546
  export type NonNegativeFiniteNumber = typeof NonNegativeFiniteNumber.Output;
4678
5547
 
5548
+ /**
5549
+ * Positive {@link FiniteNumber}.
5550
+ *
5551
+ * @group Number
5552
+ */
5553
+ export const PositiveFiniteNumber = /*#__PURE__*/ positive(
5554
+ NonNegativeFiniteNumber,
5555
+ );
5556
+ export type PositiveFiniteNumber = typeof PositiveFiniteNumber.Output;
5557
+
4679
5558
  /**
4680
5559
  * Safe integer {@link Brand}.
4681
5560
  *
@@ -4731,13 +5610,6 @@ export type Int = typeof Int.Output;
4731
5610
  export const NonNegativeInt = /*#__PURE__*/ nonNegative(Int);
4732
5611
  export type NonNegativeInt = typeof NonNegativeInt.Output;
4733
5612
 
4734
- /**
4735
- * 0-100 as a literal, or any already-validated {@link NonNegativeInt}.
4736
- *
4737
- * @group Number
4738
- */
4739
- export type Int0To100OrNonNegativeInt = 0 | Int1To100 | NonNegativeInt;
4740
-
4741
5613
  /**
4742
5614
  * Minimum {@link NonNegativeInt} value.
4743
5615
  *
@@ -4756,13 +5628,6 @@ export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
4756
5628
  export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
4757
5629
  export type PositiveInt = typeof PositiveInt.Output;
4758
5630
 
4759
- /**
4760
- * 1-100 as a literal, or any already-validated {@link PositiveInt}.
4761
- *
4762
- * @group Number
4763
- */
4764
- export type Int1To100OrPositiveInt = Int1To100 | PositiveInt;
4765
-
4766
5631
  /**
4767
5632
  * Minimum {@link PositiveInt} value.
4768
5633
  *
@@ -4798,7 +5663,7 @@ export const NegativeInt = /*#__PURE__*/ negative(NonPositiveInt);
4798
5663
  export type NegativeInt = typeof NegativeInt.Output;
4799
5664
 
4800
5665
  /**
4801
- * Adds a {@link Brand} requiring a number greater than `min`.
5666
+ * Number {@link Brand} requiring a value greater than `min`.
4802
5667
  *
4803
5668
  * @group Number
4804
5669
  */
@@ -4830,7 +5695,7 @@ export interface GreaterThanError<
4830
5695
  }
4831
5696
 
4832
5697
  /**
4833
- * 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`.
4834
5699
  *
4835
5700
  * @group Number
4836
5701
  */
@@ -4870,7 +5735,7 @@ export interface GreaterThanOrEqualToError<
4870
5735
  }
4871
5736
 
4872
5737
  /**
4873
- * Adds a {@link Brand} requiring a number less than `max`.
5738
+ * Number {@link Brand} requiring a value less than `max`.
4874
5739
  *
4875
5740
  * @group Number
4876
5741
  */
@@ -4913,7 +5778,7 @@ export const Age = /*#__PURE__*/ brand(
4913
5778
  export type Age = typeof Age.Output;
4914
5779
 
4915
5780
  /**
4916
- * 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`.
4917
5782
  *
4918
5783
  * @group Number
4919
5784
  */
@@ -4962,104 +5827,245 @@ export const Ratio = /*#__PURE__*/ brand(
4962
5827
  export type Ratio = typeof Ratio.Output;
4963
5828
 
4964
5829
  /**
4965
- * Canonical string representation of a positive base-10 decimal.
5830
+ * Canonical string representation of a signed base-10 decimal value.
5831
+ *
5832
+ * Use this Type when a decimal value must remain exact instead of being
5833
+ * converted to an IEEE-754 number. Equivalent values have one accepted
5834
+ * representation, so leading zeroes, trailing fractional zeroes, `-0`, plus
5835
+ * signs, and exponent notation are rejected.
5836
+ *
5837
+ * The decoded value remains a string. Arithmetic requires an explicit decimal
5838
+ * or fixed-point representation.
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.
4966
5844
  *
4967
- * Each value has one spelling: leading zeroes, trailing fractional zeroes,
4968
- * signs, and exponent notation are rejected. This preserves exact decimal
4969
- * meaning without representing the value as an IEEE-754 number.
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}
4970
5852
  *
4971
5853
  * ### Example
4972
5854
  *
4973
5855
  * ```ts
4974
- * import { PositiveDecimalString } from "@evolu/common";
5856
+ * import { DecimalString } from "@evolu/common";
4975
5857
  *
4976
- * expectOk(PositiveDecimalString.fromUnknown("0.3"), "0.3");
4977
- * expectOk(PositiveDecimalString.fromUnknown("25"), "25");
5858
+ * expectOk(DecimalString.fromUnknown("-10.25"), "-10.25");
5859
+ * expectOk(DecimalString.fromUnknown("0"), "0");
5860
+ * expectOk(DecimalString.fromUnknown("10.25"), "10.25");
4978
5861
  *
4979
- * expectErr(PositiveDecimalString.fromUnknown("0.30"), {
4980
- * type: "PositiveDecimalString",
4981
- * value: "0.30",
5862
+ * expectErr(DecimalString.fromUnknown("10.250"), {
5863
+ * type: "DecimalString",
5864
+ * value: "10.250",
4982
5865
  * });
4983
5866
  * ```
4984
5867
  *
4985
5868
  * @group Number
4986
5869
  */
4987
- export const PositiveDecimalString = /*#__PURE__*/ brand(
4988
- "PositiveDecimalString",
5870
+ export const DecimalString = /*#__PURE__*/ brand(
5871
+ "DecimalString",
4989
5872
  String,
4990
5873
  (value) =>
4991
- /^(?:[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)
4992
5875
  ? ok()
4993
- : err<PositiveDecimalStringError>({
4994
- type: "PositiveDecimalString",
4995
- value,
4996
- }),
5876
+ : err<DecimalStringError>({ type: "DecimalString", value }),
4997
5877
  (error) =>
4998
- `The value ${safelyStringifyUnknownValue(error.value)} must be a canonical positive decimal string.`,
5878
+ `The value ${safelyStringifyUnknownValue(error.value)} must be a canonical decimal string.`,
4999
5879
  );
5000
- export type PositiveDecimalString = typeof PositiveDecimalString.Output;
5880
+ export type DecimalString = typeof DecimalString.Output;
5001
5881
 
5002
5882
  /** @group Number */
5003
- export interface PositiveDecimalStringError extends TypeError<"PositiveDecimalString"> {
5883
+ export interface DecimalStringError extends TypeError<"DecimalString"> {
5004
5884
  readonly value: string;
5005
5885
  }
5006
5886
 
5007
- type ValidateMultipleOfDivisor<Divisor extends string> =
5008
- IsUnion<Divisor> extends false
5009
- ? string extends Divisor
5010
- ? InvalidMultipleOfDivisor<Divisor>
5011
- : IsCanonicalPositiveDecimalString<Divisor> extends true
5012
- ? Divisor
5013
- : InvalidMultipleOfDivisor<Divisor>
5014
- : InvalidMultipleOfDivisor<Divisor>;
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
+ );
5015
5910
 
5016
- type IsCanonicalPositiveDecimalString<Value extends string> =
5017
- Value extends `${infer Integer}.${infer Fraction}`
5018
- ? IsCanonicalNonNegativeIntegerString<Integer> extends true
5019
- ? IsCanonicalPositiveFractionString<Fraction>
5020
- : false
5021
- : IsCanonicalPositiveIntegerString<Value>;
5911
+ /** @group Number */
5912
+ export interface NonNegativeDecimalStringError extends TypeError<"NonNegativeDecimalString"> {
5913
+ readonly value: string;
5914
+ }
5022
5915
 
5023
- type IsCanonicalNonNegativeIntegerString<Value extends string> =
5024
- Value extends "0" ? true : IsCanonicalPositiveIntegerString<Value>;
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;
5025
5924
 
5026
- type IsCanonicalPositiveIntegerString<Value extends string> =
5027
- Value extends `${Digit1To9}${infer Rest}` ? IsDecimalDigits<Rest> : false;
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
+ );
5028
5948
 
5029
- type IsCanonicalPositiveFractionString<Value extends string> =
5030
- Value extends `${infer Rest}${Digit1To9}` ? IsDecimalDigits<Rest> : false;
5949
+ /** @group Number */
5950
+ export interface PositiveDecimalStringError extends TypeError<"PositiveDecimalString"> {
5951
+ readonly value: string;
5952
+ }
5031
5953
 
5032
- type IsDecimalDigits<Value extends string> = Value extends ""
5033
- ? true
5034
- : Value extends `${Digit}${infer Rest}`
5035
- ? IsDecimalDigits<Rest>
5036
- : false;
5954
+ /**
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;
5037
5966
 
5038
- type InvalidMultipleOfDivisor<Divisor extends string> = Divisor &
5039
- Readonly<Record<MultipleOfDivisorError, never>>;
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
+ );
5040
5990
 
5041
- type MultipleOfDivisorError = CompileTimeError<
5042
- "MultipleOf",
5043
- 'Divisor must be one canonical positive decimal string literal such as "0.1".'
5044
- >;
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
+ }
5045
6033
 
5046
6034
  /**
5047
- * Adds a {@link Brand} requiring a number to be a multiple of an exact decimal
5048
- * `divisor`.
6035
+ * Negative {@link DecimalString}.
5049
6036
  *
5050
- * The literal divisor is validated against {@link PositiveDecimalString} and
5051
- * encoded in the resulting Brand name. A runtime string cannot define this Type
5052
- * because its exact value is not available to TypeScript for that name.
5053
- * Validation does not round: `"0.1"` accepts `0.3`, but rejects `0.1 + 0.2`
5054
- * because that expression evaluates to `0.30000000000000004`.
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`.
6049
+ *
6050
+ * The divisor must be one canonical positive decimal string literal because its
6051
+ * exact value is encoded in the resulting Brand name. The declaration is
6052
+ * validated at compile time and is never converted to an IEEE-754 number.
6053
+ *
6054
+ * Validation treats each finite Number as its canonical decimal representation
6055
+ * and checks exact base-10 divisibility. It does not use floating-point
6056
+ * remainder, apply a tolerance, or round to the divisor's precision.
5055
6057
  *
5056
6058
  * ### Example
5057
6059
  *
5058
6060
  * ```ts
5059
- * import { FiniteNumber, multipleOf } from "@evolu/common";
6061
+ * import { FiniteNumber, multipleOf, type Brand } from "@evolu/common";
5060
6062
  *
5061
6063
  * const Tenths = multipleOf("0.1")(FiniteNumber);
5062
6064
  *
6065
+ * expectTypeOf<typeof Tenths.Output>().toEqualTypeOf<
6066
+ * FiniteNumber & Brand<"MultipleOf0.1">
6067
+ * >();
6068
+ *
5063
6069
  * expectOk(Tenths.fromUnknown(0.3), 0.3);
5064
6070
  * expectErr(Tenths.fromUnknown(0.31), {
5065
6071
  * type: "MultipleOf0.1",
@@ -5073,11 +6079,6 @@ type MultipleOfDivisorError = CompileTimeError<
5073
6079
  export const multipleOf = <const Divisor extends string>(
5074
6080
  divisor: ValidateMultipleOfDivisor<Divisor>,
5075
6081
  ): BrandFactory<`MultipleOf${Divisor}`, number, MultipleOfError<Divisor>> => {
5076
- // TODO: Define the positive-decimal literal domain with templateLiteral so
5077
- // its TypeScript type is inferred from runtime Type code, then require that
5078
- // literal Type here instead of accepting a string guarded by assertType.
5079
- assertType(PositiveDecimalString, divisor);
5080
-
5081
6082
  const name = `MultipleOf${divisor}` as `MultipleOf${Divisor}`;
5082
6083
  const decimalDivisor = decimalStringToParts(divisor);
5083
6084
 
@@ -5090,22 +6091,18 @@ export const multipleOf = <const Divisor extends string>(
5090
6091
 
5091
6092
  if (globalThis.Number.isFinite(value)) {
5092
6093
  const decimalValue = decimalStringToParts(value.toString());
5093
- const exponentDifference =
5094
- decimalValue.exponent - decimalDivisor.exponent;
5095
-
5096
- if (exponentDifference >= 0) {
5097
- isMultiple =
5098
- (decimalValue.coefficient *
5099
- 10n ** globalThis.BigInt(exponentDifference)) %
5100
- decimalDivisor.coefficient ===
5101
- 0n;
5102
- } else {
5103
- isMultiple =
5104
- decimalValue.coefficient %
5105
- (decimalDivisor.coefficient *
5106
- 10n ** globalThis.BigInt(-exponentDifference)) ===
5107
- 0n;
5108
- }
6094
+ const exponent = Math.min(
6095
+ decimalValue.exponent,
6096
+ decimalDivisor.exponent,
6097
+ );
6098
+ const valueCoefficient =
6099
+ decimalValue.coefficient *
6100
+ 10n ** globalThis.BigInt(decimalValue.exponent - exponent);
6101
+ const divisorCoefficient =
6102
+ decimalDivisor.coefficient *
6103
+ 10n ** globalThis.BigInt(decimalDivisor.exponent - exponent);
6104
+
6105
+ isMultiple = valueCoefficient % divisorCoefficient === 0n;
5109
6106
  }
5110
6107
 
5111
6108
  return isMultiple
@@ -5129,6 +6126,45 @@ export interface MultipleOfError<
5129
6126
  readonly divisor: Divisor;
5130
6127
  }
5131
6128
 
6129
+ type ValidateMultipleOfDivisor<Divisor extends string> =
6130
+ IsUnion<Divisor> extends false
6131
+ ? string extends Divisor
6132
+ ? InvalidMultipleOfDivisor<Divisor>
6133
+ : IsCanonicalPositiveDecimalString<Divisor> extends true
6134
+ ? Divisor
6135
+ : InvalidMultipleOfDivisor<Divisor>
6136
+ : InvalidMultipleOfDivisor<Divisor>;
6137
+
6138
+ type IsCanonicalPositiveDecimalString<Value extends string> =
6139
+ Value extends `${infer Integer}.${infer Fraction}`
6140
+ ? IsCanonicalNonNegativeIntegerString<Integer> extends true
6141
+ ? IsCanonicalPositiveFractionString<Fraction>
6142
+ : false
6143
+ : IsCanonicalPositiveIntegerString<Value>;
6144
+
6145
+ type IsCanonicalNonNegativeIntegerString<Value extends string> =
6146
+ Value extends "0" ? true : IsCanonicalPositiveIntegerString<Value>;
6147
+
6148
+ type IsCanonicalPositiveIntegerString<Value extends string> =
6149
+ Value extends `${Digit1To9}${infer Rest}` ? IsDecimalDigits<Rest> : false;
6150
+
6151
+ type IsCanonicalPositiveFractionString<Value extends string> =
6152
+ Value extends `${infer Rest}${Digit1To9}` ? IsDecimalDigits<Rest> : false;
6153
+
6154
+ type IsDecimalDigits<Value extends string> = Value extends ""
6155
+ ? true
6156
+ : Value extends `${Digit}${infer Rest}`
6157
+ ? IsDecimalDigits<Rest>
6158
+ : false;
6159
+
6160
+ type InvalidMultipleOfDivisor<Divisor extends string> = Divisor &
6161
+ Readonly<Record<MultipleOfDivisorError, never>>;
6162
+
6163
+ type MultipleOfDivisorError = CompileTimeError<
6164
+ "MultipleOf",
6165
+ 'Divisor must be one canonical positive decimal string literal such as "0.1".'
6166
+ >;
6167
+
5132
6168
  interface DecimalParts {
5133
6169
  readonly coefficient: bigint;
5134
6170
  readonly exponent: number;
@@ -5159,7 +6195,7 @@ const decimalStringToParts = (value: string): DecimalParts => {
5159
6195
  };
5160
6196
 
5161
6197
  /**
5162
- * Adds a {@link Brand} requiring a number to be within an inclusive range.
6198
+ * Number {@link Brand} requiring a value within an inclusive range.
5163
6199
  *
5164
6200
  * @group Number
5165
6201
  */
@@ -6449,6 +7485,106 @@ const validateTupleItems = (
6449
7485
  checkStructure,
6450
7486
  );
6451
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
+
6452
7588
  const createObjectRuntimeTypeIssues =
6453
7589
  (
6454
7590
  defaultFormatter: TypeErrorFormatter<TypeError>,
@@ -8473,7 +9609,7 @@ const createRecordPropertyError = <Error extends TypeError>(
8473
9609
  });
8474
9610
 
8475
9611
  /**
8476
- * Creates an {@link object} Type with every property optional.
9612
+ * Object {@link Type} with every property optional.
8477
9613
  *
8478
9614
  * No property is required, but every present property must still satisfy its
8479
9615
  * Type.
@@ -8523,7 +9659,8 @@ export type PartialObjectProps<Props extends ObjectProps> = {
8523
9659
  };
8524
9660
 
8525
9661
  /**
8526
- * 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.
8527
9664
  *
8528
9665
  * The property retains its original Union Type, so consumers may omit it, set
8529
9666
  * it to `null`, or provide any other member of that Union. Properties without
@@ -8594,7 +9731,7 @@ export type NullableToOptionalProps<Props extends ObjectProps> = {
8594
9731
  };
8595
9732
 
8596
9733
  /**
8597
- * Creates an {@link object} Type without the selected declared properties.
9734
+ * Object {@link Type} without the selected declared properties.
8598
9735
  *
8599
9736
  * @group Objects
8600
9737
  */
@@ -8648,7 +9785,7 @@ type OmitKeyConcreteTypeError = CompileTimeError<
8648
9785
  >;
8649
9786
 
8650
9787
  /**
8651
- * Creates a {@link Type} for {@link Result} values.
9788
+ * {@link Result} {@link Type} for typed success and error values.
8652
9789
  *
8653
9790
  * Use this to validate Results crossing a storage, worker, API, or other
8654
9791
  * serialization boundary. The operation returns an outer validation Result. Its
@@ -8942,7 +10079,7 @@ type TypedTypePropertyError = CompileTimeError<
8942
10079
  >;
8943
10080
 
8944
10081
  /**
8945
- * 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.
8946
10083
  *
8947
10084
  * The three outcomes are `Ok<Value>`, `Err<Error>`, and `Err<Typed<"Done"> & {
8948
10085
  * done: Done }>`. This keeps normal completion distinct from failure while
@@ -10577,7 +11714,7 @@ export const JsonValueFromJson = /*#__PURE__*/ transform(
10577
11714
  );
10578
11715
 
10579
11716
  /**
10580
- * Creates a branded {@link Json} Type and total conversions for another Type.
11717
+ * Branded {@link Json} Type and total conversions for another Type.
10581
11718
  *
10582
11719
  * Use this factory when a domain value must be stored as JSON text while its
10583
11720
  * exact Type remains visible to TypeScript, such as a JSON column in an Evolu