@evolu/common 8.3.0 → 8.3.2
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.d.ts +65 -87
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +226 -136
- package/package.json +5 -5
- package/src/Type.ts +252 -166
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
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* import { Age } from "@evolu/common";
|
|
37
120
|
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
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
|
|
255
|
-
* If the boundary returns `unknown`, validate it instead of bypassing
|
|
256
|
-
* boundary with a cast. Use a specialized Type or explicit transformation
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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(
|
|
2329
|
+
* expect(Int64FromInt64String.to(result.value)).toBe(
|
|
2330
|
+
* "9223372036854775807",
|
|
2331
|
+
* );
|
|
2229
2332
|
* ```
|
|
2230
2333
|
*
|
|
2231
2334
|
* @group Number
|
|
@@ -3375,7 +3478,7 @@ const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
|
3375
3478
|
*
|
|
3376
3479
|
* @group Base
|
|
3377
3480
|
*/
|
|
3378
|
-
|
|
3481
|
+
const _Object = /*#__PURE__*/ createRootType("Object", (value, options = firstValidationOptions) => {
|
|
3379
3482
|
if (value === null || typeof value !== "object") {
|
|
3380
3483
|
return err({
|
|
3381
3484
|
type: "Object",
|
|
@@ -3469,6 +3572,12 @@ export const Object = /*#__PURE__*/ createRootType("Object", (value, options = f
|
|
|
3469
3572
|
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
3470
3573
|
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
3471
3574
|
})));
|
|
3575
|
+
// Avoid a local `Object` binding because Babel's CommonJS transform injects
|
|
3576
|
+
// `Object.defineProperty` before it is initialized:
|
|
3577
|
+
// https://github.com/babel/babel/issues/16943
|
|
3578
|
+
// https://github.com/react/metro/issues/1331
|
|
3579
|
+
// https://github.com/expo/expo/issues/31167
|
|
3580
|
+
export { _Object as Object };
|
|
3472
3581
|
const isPlainObject = (value) => {
|
|
3473
3582
|
const prototype = globalThis.Object.getPrototypeOf(value);
|
|
3474
3583
|
return (prototype === null || globalThis.Object.getPrototypeOf(prototype) === null);
|
|
@@ -4791,7 +4900,7 @@ export const JsonObject = /*#__PURE__*/ record(String, JsonValue);
|
|
|
4791
4900
|
* A {@link String} Brand proving that its exact text parses to {@link JsonValue}.
|
|
4792
4901
|
*
|
|
4793
4902
|
* The Brand preserves whitespace, property order, and number spelling. Convert
|
|
4794
|
-
* it
|
|
4903
|
+
* it to {@link JsonValue} through {@link JsonValueFromJson} or
|
|
4795
4904
|
* {@link jsonToJsonValue}.
|
|
4796
4905
|
*
|
|
4797
4906
|
* @group JSON
|
|
@@ -4801,7 +4910,7 @@ export const Json = /*#__PURE__*/ brand("Json", String, (value) => {
|
|
|
4801
4910
|
return result.ok ? ok() : result;
|
|
4802
4911
|
}, (error) => `The value ${safelyStringifyUnknownValue(error.value)} cannot be parsed into a JsonValue.`);
|
|
4803
4912
|
/**
|
|
4804
|
-
*
|
|
4913
|
+
* Converts proven {@link Json} text to an exact {@link JsonValue}.
|
|
4805
4914
|
*
|
|
4806
4915
|
* ### Example
|
|
4807
4916
|
*
|
|
@@ -4817,7 +4926,7 @@ export const Json = /*#__PURE__*/ brand("Json", String, (value) => {
|
|
|
4817
4926
|
*/
|
|
4818
4927
|
export const jsonToJsonValue = (value) => parseJson(value);
|
|
4819
4928
|
/**
|
|
4820
|
-
*
|
|
4929
|
+
* Converts an exact {@link JsonValue} to canonical {@link Json} text.
|
|
4821
4930
|
*
|
|
4822
4931
|
* ### Example
|
|
4823
4932
|
*
|
|
@@ -4856,67 +4965,48 @@ export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Js
|
|
|
4856
4965
|
to: stringifyJsonValue,
|
|
4857
4966
|
});
|
|
4858
4967
|
/**
|
|
4859
|
-
* Branded {@link Json} Type and
|
|
4860
|
-
*
|
|
4861
|
-
* Use this
|
|
4862
|
-
*
|
|
4863
|
-
*
|
|
4864
|
-
*
|
|
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.
|
|
4968
|
+
* Branded {@link Json} Type and conversions for another {@link Type}.
|
|
4969
|
+
*
|
|
4970
|
+
* Use this when a value must be stored as JSON text, such as in a JSON column
|
|
4971
|
+
* in an Evolu Schema. It returns a branded Json Type and functions for
|
|
4972
|
+
* converting the supplied Type's Output to and from that branded JSON
|
|
4973
|
+
* representation.
|
|
4888
4974
|
*
|
|
4889
4975
|
* ### Example
|
|
4890
4976
|
*
|
|
4891
4977
|
* ```ts
|
|
4892
4978
|
* import {
|
|
4893
4979
|
* Age,
|
|
4980
|
+
* NonEmptyTrimmedString100,
|
|
4894
4981
|
* json,
|
|
4895
4982
|
* object,
|
|
4896
|
-
* String,
|
|
4897
4983
|
* type Brand,
|
|
4898
|
-
* type InferType,
|
|
4899
|
-
* type Json,
|
|
4900
4984
|
* } from "@evolu/common";
|
|
4901
4985
|
*
|
|
4902
|
-
* const
|
|
4903
|
-
*
|
|
4986
|
+
* const User = object({
|
|
4987
|
+
* name: NonEmptyTrimmedString100,
|
|
4988
|
+
* age: Age,
|
|
4989
|
+
* });
|
|
4904
4990
|
*
|
|
4905
|
-
* const [
|
|
4906
|
-
*
|
|
4907
|
-
* "
|
|
4991
|
+
* const [UserJson, userToUserJson, userJsonToUser] = json(
|
|
4992
|
+
* User,
|
|
4993
|
+
* "UserJson",
|
|
4908
4994
|
* );
|
|
4909
|
-
* type PersonJson = typeof PersonJson.Output;
|
|
4910
|
-
*
|
|
4911
|
-
* expectTypeOf<PersonJson>().toEqualTypeOf<Json & Brand<"PersonJson">>();
|
|
4912
4995
|
*
|
|
4913
|
-
* const
|
|
4914
|
-
* const
|
|
4915
|
-
* const decodedPerson = personJsonToPerson(personJson);
|
|
4996
|
+
* const user = User.orThrow({ name: "Ada", age: 37 });
|
|
4997
|
+
* const userJson = userToUserJson(user);
|
|
4916
4998
|
*
|
|
4917
|
-
*
|
|
4999
|
+
* expectTypeOf(userJson).toEqualTypeOf<
|
|
5000
|
+
* string & Brand<"Json"> & Brand<"UserJson">
|
|
5001
|
+
* >();
|
|
5002
|
+
* expect(userJson).toBe('{"name":"Ada","age":37}');
|
|
5003
|
+
* expect(userJsonToUser(userJson)).toEqual(user);
|
|
4918
5004
|
* ```
|
|
4919
5005
|
*
|
|
5006
|
+
* The supplied Type must have a JSON-compatible `CanonicalInput`. The branded
|
|
5007
|
+
* Json Type accepts only valid JSON text whose parsed value can be decoded by
|
|
5008
|
+
* the supplied Type.
|
|
5009
|
+
*
|
|
4920
5010
|
* @group JSON
|
|
4921
5011
|
*/
|
|
4922
5012
|
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.
|
|
3
|
+
"version": "8.3.2",
|
|
4
4
|
"description": "TypeScript library and local-first platform",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"evolu",
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
"README.md"
|
|
67
67
|
],
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@noble/ciphers": "^2.
|
|
70
|
-
"@noble/hashes": "^2.0
|
|
71
|
-
"@scure/bip39": "^2.0
|
|
69
|
+
"@noble/ciphers": "^2.3.0",
|
|
70
|
+
"@noble/hashes": "^2.3.0",
|
|
71
|
+
"@scure/bip39": "^2.3.0",
|
|
72
72
|
"@standard-schema/spec": "^1.1.0",
|
|
73
|
-
"kysely": "^0.29.
|
|
73
|
+
"kysely": "^0.29.5",
|
|
74
74
|
"msgpackr": "^2.0.5",
|
|
75
75
|
"random": "^5.4.1"
|
|
76
76
|
},
|