@evolu/common 8.3.0 → 8.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/Type.js CHANGED
@@ -8,17 +8,84 @@
8
8
  * valid `Output` into a `CanonicalInput`. Types can validate, refine,
9
9
  * transform, and compose without losing the contracts TypeScript can express.
10
10
  *
11
+ * Decoding failures are explicit {@link Result} values, and their structured
12
+ * errors preserve the exact error types each Type can return.
13
+ *
14
+ * Evolu Type is designed to make correct code the easiest code to write:
15
+ *
16
+ * - Predefined constraints add a {@link Brand} to their Output.
17
+ * - Invalid declarations produce readable {@link CompileTimeError} types when the
18
+ * compiler can detect them.
19
+ * - Evolu Type uses runtime {@link assert | assertions} to detect developer errors
20
+ * that TypeScript cannot express, such as excess properties and sparse
21
+ * arrays.
22
+ * - Typed `from` boundaries allow connecting value producers to domain fields
23
+ * through their exact TypeScript types, so incompatible contract changes are
24
+ * compile-time errors rather than runtime validation errors.
25
+ * - Lawful codecs compose without creating unencodable values: every valid Output
26
+ * has a canonical Input representation and round-trips to the same semantic
27
+ * value.
28
+ * - Type-safe localization infers the required error formatters from selected
29
+ * Types, so missing validation messages are compile-time errors.
30
+ *
31
+ * Correctness is especially important for local-first data: application authors
32
+ * cannot inspect or repair a user's data.
33
+ *
34
+ * Evolu Type is optimized for small real-world bundles: composed Types share
35
+ * runtime code, while unused validators and formatters are tree-shaken. It
36
+ * could be smaller with less descriptive assertion messages, but Evolu favors
37
+ * actionable diagnostics over micro-optimizing isolated Types.
38
+ *
39
+ * Predefined Types use the names of corresponding JavaScript built-ins. When a
40
+ * Type shadows one, access the JavaScript built-in through `globalThis`, such
41
+ * as `globalThis.String` or `globalThis.Date`.
42
+ *
43
+ * Evolu Type supports [Standard Schema](https://standardschema.dev/) and
44
+ * requires TypeScript 7+ with `exactOptionalPropertyTypes` enabled.
45
+ *
46
+ * ## Examples
47
+ *
48
+ * Define a domain object with a custom `Age` Type, then validate unknown input:
49
+ *
11
50
  * ```ts
12
51
  * import {
52
+ * Number,
13
53
  * NonEmptyTrimmedString100,
14
- * PositiveInt,
54
+ * brand,
55
+ * finite,
56
+ * int,
57
+ * lessThan,
58
+ * nonNaN,
59
+ * nonNegative,
15
60
  * object,
61
+ * type Brand,
62
+ * type InferErrors,
16
63
  * type InferType,
17
64
  * } from "@evolu/common";
18
65
  *
66
+ * // Age and its parent Types are predefined by Evolu. They are reconstructed
67
+ * // here to reveal every constraint behind a seemingly simple domain value.
68
+ * const NonNaNNumber = nonNaN(Number);
69
+ * const FiniteNumber = finite(NonNaNNumber);
70
+ * const Int = int(FiniteNumber);
71
+ * const NonNegativeInt = nonNegative(Int);
72
+ *
73
+ * const Age = brand("Age", lessThan(200)(NonNegativeInt));
74
+ * type Age = typeof Age.Output;
75
+ *
76
+ * expectTypeOf<Age>().toEqualTypeOf<
77
+ * number &
78
+ * Brand<"NonNaN"> &
79
+ * Brand<"Finite"> &
80
+ * Brand<"Int"> &
81
+ * Brand<"NonNegative"> &
82
+ * Brand<"LessThan200"> &
83
+ * Brand<"Age">
84
+ * >();
85
+ *
19
86
  * const User = object({
20
87
  * name: NonEmptyTrimmedString100,
21
- * age: PositiveInt,
88
+ * age: Age,
22
89
  * });
23
90
  * interface User extends InferType<typeof User> {}
24
91
  *
@@ -27,74 +94,114 @@
27
94
  *
28
95
  * expectOk(user, { name: "Ada", age: 37 });
29
96
  * expectTypeOf(user.value).toExtend<User>();
97
+ *
98
+ * const invalidUser = User.fromUnknown({ name: "Ada", age: 37.5 });
99
+ *
100
+ * expectErr(invalidUser, {
101
+ * type: "Object",
102
+ * reason: {
103
+ * kind: "Properties",
104
+ * errors: {
105
+ * age: { type: "Int", value: 37.5 },
106
+ * },
107
+ * },
108
+ * });
109
+ *
110
+ * // InferErrors includes every structured error User.fromUnknown can return.
111
+ * expectTypeOf(invalidUser.error).toEqualTypeOf<
112
+ * InferErrors<typeof User>
113
+ * >();
30
114
  * ```
31
115
  *
32
- * Decoding failures are explicit {@link Result} values. Error formatters are
33
- * separate from validation, so structured errors remain exhaustively typed and
34
- * can be localized without changing the Type.
116
+ * A Type can format its structured errors into user-facing messages:
35
117
  *
36
- * Evolu Type is designed to make correct code the easiest code to write:
118
+ * ```ts
119
+ * import { Age } from "@evolu/common";
37
120
  *
38
- * - {@link Brand} carries every refinement constraint into TypeScript.
39
- * - Invalid declarations produce readable {@link CompileTimeError} types when the
40
- * compiler can detect them; runtime assertions enforce construction contracts
41
- * it cannot prove.
42
- * - Typed `from` boundaries allow connecting value producers to domain fields
43
- * through their exact TypeScript types, so incompatible contract changes are
44
- * compile-time errors rather than runtime validation errors.
45
- * - Lawful codecs compose without creating unencodable values: every valid
46
- * Output has a canonical Input representation and round-trips to the same
47
- * semantic value.
48
- * - Type-safe localization infers the required error formatters from selected
49
- * Types, so missing validation messages are compile-time errors.
121
+ * const age = Age.fromUnknown(37.5);
50
122
  *
51
- * Correctness is especially important for local-first data: application authors
52
- * cannot inspect or repair a user's data on a server. Type declarations reject
53
- * invalid data at system boundaries, then preserve those guarantees wherever
54
- * the data travels.
55
- *
56
- * Evolu Type supports [Standard Schema](https://standardschema.dev/) while
57
- * preserving each Type's exact Input and Output. Its implementation is
58
- * optimized for small bundles, but keeps actionable assertion messages as a
59
- * deliberate developer-experience tradeoff.
60
- *
61
- * Evolu Type requires TypeScript 7 or newer and `exactOptionalPropertyTypes`.
62
- * Predefined Types use the names of corresponding JavaScript built-ins. When
63
- * one shadows a built-in, access the built-in through `globalThis`, such as
64
- * `globalThis.String` or `globalThis.Date`.
65
- *
66
- * ## Boundaries
67
- *
68
- * `fromUnknown` validates untyped input through the complete pipeline. `from`
69
- * and its `.parent` operations assert their declared boundary, then return only
70
- * errors from the remaining stages. `orThrow` and `orNull` reuse the deepest
71
- * `from` operation accepting `Input`, while `to` asserts its Output boundary. A
72
- * failed assertion is a developer error; its cause preserves the exact
73
- * structured Output validation error.
74
- *
75
- * Prefer the most precise typed boundary available. A value is not unknown
76
- * merely because it originated outside the application: forms, components, and
77
- * other producers often expose a `string` or a branded value that can connect
78
- * directly to a matching `from` boundary. Reserve `fromUnknown` for values
79
- * whose TypeScript type is genuinely `unknown`. `is` means exact membership in
80
- * the Output domain, not merely that an encoded Input can be decoded.
81
- *
82
- * Typed boundaries also connect producer and domain constraints at compile
83
- * time. Suppose a form and its domain field both use
84
- * {@link NonEmptyTrimmedString100}. If the form is later relaxed to
85
- * {@link NonEmptyTrimmedString1000}, the domain Type will still reject longer
86
- * values, so invalid data cannot be stored. But a boundary accepting only
87
- * `unknown` or `string` cannot reveal that the producer contract changed. The
88
- * application still compiles, and users discover the incompatibility only when
89
- * a valid form value fails to save.
90
- *
91
- * Passing the precise branded value to `from` makes that incompatibility a
92
- * compile-time error. The developer must preserve the original form limit or
93
- * introduce a new field for the wider domain instead of shipping a broken form.
94
- *
95
- * A weaker producer is sometimes intentional. In that case, a matching
96
- * `.parent` boundary validates only the constraints the producer does not
97
- * already guarantee, while preserving its existing guarantees in the type.
123
+ * expectErr(age, { type: "Int", value: 37.5 });
124
+ * expect(Age.formatError(age.error)).toBe(
125
+ * "The value 37.5 must be a safe integer.",
126
+ * );
127
+ * ```
128
+ *
129
+ * Use {@link localizeTypes} to derive Types with localized messages without
130
+ * changing validation behavior.
131
+ *
132
+ * One of Evolu Type's strongest features is typed `from` boundaries. A value
133
+ * producer, such as a form input, carries the precise constraints it
134
+ * guarantees, and TypeScript checks them against the consuming domain field.
135
+ * Unlike validation from `unknown` or `string`, this checks the contract
136
+ * between the producer and consumer, not merely whether the current value
137
+ * passes:
138
+ *
139
+ * ```ts
140
+ * import {
141
+ * NonEmptyTrimmedString100,
142
+ * NonEmptyTrimmedString1000,
143
+ * object,
144
+ * trim,
145
+ * type MaxLengthError,
146
+ * type MinLengthError,
147
+ * type Result,
148
+ * type TrimmedString,
149
+ * } from "@evolu/common";
150
+ *
151
+ * const Todo = object({ title: NonEmptyTrimmedString100 });
152
+ *
153
+ * // This is type-checked: Todo.from expects NonEmptyTrimmedString100.
154
+ * const title = NonEmptyTrimmedString100.orThrow("Buy milk");
155
+ * expectOk(Todo.from({ title }), { title });
156
+ *
157
+ * // Imagine the UI input component is changed to allow longer titles.
158
+ * // TypeScript rejects the mismatch, so users never see a save error
159
+ * // for a title the UI accepts but the domain cannot save.
160
+ * const longerTitle = NonEmptyTrimmedString1000.orThrow("Buy milk");
161
+ * // @ts-expect-error MaxLength1000 does not guarantee MaxLength100.
162
+ * Todo.from({ title: longerTitle });
163
+ *
164
+ * // Imagine a UI input component that returns TrimmedString.
165
+ * // from.parent.parent connects it to the domain field and validates the
166
+ * // remaining constraints.
167
+ * const titleFromTrimmingInput: TrimmedString = trim(" Buy milk ");
168
+ * const validatedTitle = Todo.props.title.from.parent.parent(
169
+ * titleFromTrimmingInput,
170
+ * );
171
+ *
172
+ * // No "not a string" or "not trimmed" errors: the input guarantees both.
173
+ * expectTypeOf(validatedTitle).toEqualTypeOf<
174
+ * Result<
175
+ * NonEmptyTrimmedString100,
176
+ * MaxLengthError<100> | MinLengthError<1>
177
+ * >
178
+ * >();
179
+ * expectOk(validatedTitle, "Buy milk");
180
+ * ```
181
+ *
182
+ * Evolu includes dozens of predefined Types and Type factories. Use Types such
183
+ * as {@link Age}, {@link PositiveInt}, {@link DateIso},
184
+ * {@link NonEmptyTrimmedString100}, {@link Base64Url}, and {@link Json} directly.
185
+ * Build domain Types with factories such as {@link brand}, {@link typed},
186
+ * {@link minLength}, {@link maxLength}, {@link array}, {@link object},
187
+ * {@link union}, {@link templateLiteral}, {@link transform},
188
+ * {@link discriminatedUnion}, and {@link json}.
189
+ *
190
+ * ## Guarantees
191
+ *
192
+ * Evolu Type validates values; it does not defend against adversarial
193
+ * JavaScript such as malicious Proxies, mutation during validation, throwing
194
+ * traps, forged built-ins, or code deliberately bypassing TypeScript with `any`
195
+ * or casts.
196
+ *
197
+ * Evolu Type trusts application code and audited dependencies. Untrusted code
198
+ * can cause harm far beyond validation and must not run in the application.
199
+ * Defending against it would add complexity without creating a meaningful
200
+ * security boundary.
201
+ *
202
+ * Runtime assertions still detect accidental developer errors that TypeScript
203
+ * cannot express. They are correctness checks, not defenses against malicious
204
+ * code.
98
205
  *
99
206
  * ## FAQ
100
207
  *
@@ -244,24 +351,18 @@
244
351
  *
245
352
  * ### How should values from another realm be handled?
246
353
  *
247
- * Code trust and data validation are separate decisions. Values returned by
248
- * trusted legacy code or another realm can still be uncertain and should be
249
- * validated. Realm-neutral Types accept an otherwise legitimate representation
250
- * without requiring conversion merely because its built-ins belong to another
251
- * realm.
354
+ * Values returned by legacy code or another realm can still be uncertain and
355
+ * should be validated. Realm-neutral Types accept an otherwise legitimate
356
+ * representation without requiring conversion merely because its JavaScript
357
+ * built-ins belong to another realm.
252
358
  *
253
359
  * When an application trusts both the producer and its return contract, expose
254
- * that contract as an accurate TypeScript type and use the typed value directly.
255
- * If the boundary returns `unknown`, validate it instead of bypassing the
256
- * boundary with a cast. Use a specialized Type or explicit transformation when
257
- * the producer uses a different representation that needs adaptation or
360
+ * that contract as an accurate TypeScript type and use the typed value
361
+ * directly. If the boundary returns `unknown`, validate it instead of bypassing
362
+ * the boundary with a cast. Use a specialized Type or explicit transformation
363
+ * when the producer uses a different representation that needs adaptation or
258
364
  * normalization.
259
365
  *
260
- * All executing JavaScript remains trusted. Deliberately forged built-ins,
261
- * hostile Proxies, throwing traps, or sabotaged executable behavior can throw;
262
- * Evolu Type does not selectively contain them or claim to be a security
263
- * boundary for untrusted code.
264
- *
265
366
  * ### Why doesn't Evolu Type extract data from rich objects?
266
367
  *
267
368
  * Some validation libraries parse an object's data projection. An imaginary
@@ -418,8 +519,8 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
418
519
  * Pass the Types used together in one localization scope and formatter maps
419
520
  * keyed by locale. TypeScript infers every formatter required by the selected
420
521
  * Types, including errors from nested structural Types and recursive Lazy
421
- * Types. Every locale must provide the complete inferred formatter set;
422
- * missing and unrelated formatters are compile-time errors.
522
+ * Types. Every locale must provide the complete inferred formatter set; missing
523
+ * and unrelated formatters are compile-time errors.
423
524
  *
424
525
  * The result preserves the locale names, selected Type names, and exact
425
526
  * TypeScript types. A localized Type validates exactly like its source Type;
@@ -471,8 +572,8 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
471
572
  *
472
573
  * ### Supported locales
473
574
  *
474
- * English is built in; use {@link Type} directly for its default formatters.
475
- * The following additional locales are available:
575
+ * English is built in; use {@link Type} directly for its default formatters. The
576
+ * following additional locales are available:
476
577
  *
477
578
  * - Arabic (`ar`)
478
579
  * - Bengali (`bn`)
@@ -1941,8 +2042,8 @@ const base64UrlStringToUint8Array = (value) => {
1941
2042
  /**
1942
2043
  * Base64Url text without padding.
1943
2044
  *
1944
- * Encode bytes with {@link uint8ArrayToBase64Url} and decode them with
1945
- * {@link base64UrlToUint8Array}.
2045
+ * Convert bytes to Base64Url with {@link uint8ArrayToBase64Url} and convert
2046
+ * Base64Url to bytes with {@link base64UrlToUint8Array}.
1946
2047
  *
1947
2048
  * @group String
1948
2049
  */
@@ -1953,7 +2054,7 @@ export const Base64Url = /*#__PURE__*/ brand("Base64Url", String, (value) => {
1953
2054
  : err({ type: "Base64Url", value });
1954
2055
  }, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid Base64Url string.`);
1955
2056
  /**
1956
- * Encodes bytes as {@link Base64Url}.
2057
+ * Converts bytes to {@link Base64Url}.
1957
2058
  *
1958
2059
  * ### Example
1959
2060
  *
@@ -1969,7 +2070,7 @@ export const Base64Url = /*#__PURE__*/ brand("Base64Url", String, (value) => {
1969
2070
  */
1970
2071
  export const uint8ArrayToBase64Url = (bytes) => uint8ArrayToBase64UrlString(bytes);
1971
2072
  /**
1972
- * Decodes {@link Base64Url} as bytes.
2073
+ * Converts {@link Base64Url} to bytes.
1973
2074
  *
1974
2075
  * ### Example
1975
2076
  *
@@ -2225,7 +2326,9 @@ export const Int64String = /*#__PURE__*/ brand("Int64String", NonEmptyTrimmedStr
2225
2326
  * const result = Int64FromInt64String.fromUnknown("9223372036854775807");
2226
2327
  *
2227
2328
  * expectOk(result, 9223372036854775807n);
2228
- * expect(Int64FromInt64String.to(result.value)).toBe("9223372036854775807");
2329
+ * expect(Int64FromInt64String.to(result.value)).toBe(
2330
+ * "9223372036854775807",
2331
+ * );
2229
2332
  * ```
2230
2333
  *
2231
2334
  * @group Number
@@ -4791,7 +4894,7 @@ export const JsonObject = /*#__PURE__*/ record(String, JsonValue);
4791
4894
  * A {@link String} Brand proving that its exact text parses to {@link JsonValue}.
4792
4895
  *
4793
4896
  * The Brand preserves whitespace, property order, and number spelling. Convert
4794
- * it totally to {@link JsonValue} through {@link JsonValueFromJson} or
4897
+ * it to {@link JsonValue} through {@link JsonValueFromJson} or
4795
4898
  * {@link jsonToJsonValue}.
4796
4899
  *
4797
4900
  * @group JSON
@@ -4801,7 +4904,7 @@ export const Json = /*#__PURE__*/ brand("Json", String, (value) => {
4801
4904
  return result.ok ? ok() : result;
4802
4905
  }, (error) => `The value ${safelyStringifyUnknownValue(error.value)} cannot be parsed into a JsonValue.`);
4803
4906
  /**
4804
- * Totally parses proven {@link Json} text into an exact {@link JsonValue}.
4907
+ * Converts proven {@link Json} text to an exact {@link JsonValue}.
4805
4908
  *
4806
4909
  * ### Example
4807
4910
  *
@@ -4817,7 +4920,7 @@ export const Json = /*#__PURE__*/ brand("Json", String, (value) => {
4817
4920
  */
4818
4921
  export const jsonToJsonValue = (value) => parseJson(value);
4819
4922
  /**
4820
- * Totally encodes an exact {@link JsonValue} as canonical {@link Json} text.
4923
+ * Converts an exact {@link JsonValue} to canonical {@link Json} text.
4821
4924
  *
4822
4925
  * ### Example
4823
4926
  *
@@ -4856,67 +4959,48 @@ export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Js
4856
4959
  to: stringifyJsonValue,
4857
4960
  });
4858
4961
  /**
4859
- * Branded {@link Json} Type and total conversions for another Type.
4860
- *
4861
- * Use this factory when a domain value must be stored as JSON text while its
4862
- * exact Type remains visible to TypeScript, such as a JSON column in an Evolu
4863
- * Schema. The returned tuple contains the branded Json Type, an encoder from
4864
- * the supplied Type's Output to Json, and a decoder from Json back to that
4865
- * Output.
4866
- *
4867
- * The supplied Type's `CanonicalInput` must be JSON-compatible. The encoder
4868
- * first uses the Type's canonical `to` operation, then encodes that
4869
- * representation as canonical Json. Runtime representation constraints
4870
- * TypeScript cannot prove, such as dense Arrays and enumerable data properties,
4871
- * are asserted as developer errors.
4872
- *
4873
- * The branded Json Type is the validation boundary for unknown JSON text. It
4874
- * grants its {@link Brand} only when the text is valid Json and decoding it
4875
- * through the supplied Type succeeds. The supplied Type is responsible for
4876
- * preserving semantic Outputs across canonical JSON encoding and decoding. This
4877
- * law cannot be checked generically because Types do not define semantic
4878
- * equality. Before granting the Brand, the encoder asserts the weaker runtime
4879
- * guarantee that the final Json successfully decodes through the supplied Type.
4880
- * Failed decodability therefore throws as a developer error.
4881
- *
4882
- * Consequently, the two typed conversions return their values directly without
4883
- * exposing a validation {@link Result}: an Output satisfying the JSON
4884
- * representation contract of a correctly declared Type can always be encoded,
4885
- * and the branded Json proves decoding will succeed. Decoding still runs the
4886
- * Type pipeline because transformations may need to construct different Output
4887
- * values.
4962
+ * Branded {@link Json} Type and conversions for another {@link Type}.
4963
+ *
4964
+ * Use this when a value must be stored as JSON text, such as in a JSON column
4965
+ * in an Evolu Schema. It returns a branded Json Type and functions for
4966
+ * converting the supplied Type's Output to and from that branded JSON
4967
+ * representation.
4888
4968
  *
4889
4969
  * ### Example
4890
4970
  *
4891
4971
  * ```ts
4892
4972
  * import {
4893
4973
  * Age,
4974
+ * NonEmptyTrimmedString100,
4894
4975
  * json,
4895
4976
  * object,
4896
- * String,
4897
4977
  * type Brand,
4898
- * type InferType,
4899
- * type Json,
4900
4978
  * } from "@evolu/common";
4901
4979
  *
4902
- * const Person = object({ name: String, age: Age });
4903
- * interface Person extends InferType<typeof Person> {}
4980
+ * const User = object({
4981
+ * name: NonEmptyTrimmedString100,
4982
+ * age: Age,
4983
+ * });
4904
4984
  *
4905
- * const [PersonJson, personToPersonJson, personJsonToPerson] = json(
4906
- * Person,
4907
- * "PersonJson",
4985
+ * const [UserJson, userToUserJson, userJsonToUser] = json(
4986
+ * User,
4987
+ * "UserJson",
4908
4988
  * );
4909
- * type PersonJson = typeof PersonJson.Output;
4910
- *
4911
- * expectTypeOf<PersonJson>().toEqualTypeOf<Json & Brand<"PersonJson">>();
4912
4989
  *
4913
- * const person = Person.orThrow({ name: "Ada", age: 42 });
4914
- * const personJson = personToPersonJson(person);
4915
- * const decodedPerson = personJsonToPerson(personJson);
4990
+ * const user = User.orThrow({ name: "Ada", age: 37 });
4991
+ * const userJson = userToUserJson(user);
4916
4992
  *
4917
- * expect(decodedPerson).toEqual(person);
4993
+ * expectTypeOf(userJson).toEqualTypeOf<
4994
+ * string & Brand<"Json"> & Brand<"UserJson">
4995
+ * >();
4996
+ * expect(userJson).toBe('{"name":"Ada","age":37}');
4997
+ * expect(userJsonToUser(userJson)).toEqual(user);
4918
4998
  * ```
4919
4999
  *
5000
+ * The supplied Type must have a JSON-compatible `CanonicalInput`. The branded
5001
+ * Json Type accepts only valid JSON text whose parsed value can be decoded by
5002
+ * the supplied Type.
5003
+ *
4920
5004
  * @group JSON
4921
5005
  */
4922
5006
  export const json = (type, name, ..._validation) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/common",
3
- "version": "8.3.0",
3
+ "version": "8.3.1",
4
4
  "description": "TypeScript library and local-first platform",
5
5
  "keywords": [
6
6
  "evolu",