@scalar/validation 0.3.2 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @scalar/validation
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#9262](https://github.com/scalar/scalar/pull/9262): feat(validation): support recursive schemas in validate and coerce
8
+
9
+ Add recursive schema support for `validate` and `coerce`: both functions now track visited object–schema pairs so cyclic values (self-referential nodes, mutual `lazy` graphs, cyclic records) terminate instead of overflowing the stack. Nested `intersection` members are validated and coerced recursively, and union branch scoring prefers property-less object schemas over primitives for empty objects.
10
+
11
+ Improve `Static` inference for circular schemas via `LazyStatic` and tuple-folding union statics; `union` and `intersection` use lightweight `UnionMember` / `IntersectionMember` constraints so `lazy(() => self)` call sites type-check without hitting depth limits. `coerce` returns `SafeStatic<S>` (precise `Static<S>` for concrete schemas, `any` when `S` is the full `Schema` union). Export `UnionMember` and `IntersectionMember` from the package entry point.
12
+
13
+ ### Patch Changes
14
+
15
+ - [#9262](https://github.com/scalar/scalar/pull/9262): fix(validation): detect cycles when scoring union branches
16
+
17
+ `scoreUnion` (used by `coerce` to pick the best matching `union` branch) now tracks the `(value, schema)` pairs that are live on its call stack and returns a neutral positive score on re-entry. A recursive lazy schema such as `lazy(() => union([object({ child: optional(lazy(() => T)) }), …]))` evaluated against a self-referential value previously caused `scoreUnion` to recurse through `lazy → union → object → property → lazy → …` indefinitely and overflow the stack, contradicting the rest of the cycle-handling work. The marker is cleared in `finally` so sibling union branches that share a schema reference are scored independently.
18
+
19
+ - [#9262](https://github.com/scalar/scalar/pull/9262): fix(validation): do not leak cycle-detection cache across union branches
20
+
21
+ Scope the in-progress `(value, schema)` cache used by `validate` to the live call stack instead of treating it as run-wide memoization. The marker for each pair is now cleared before the call returns, so a shared schema reference that failed in one `union` branch (for example the common `base` in `union([intersection([base, objA]), intersection([base, objB])])`) is re-validated in the next branch rather than short-circuiting to `true` from a stale entry. Cycle detection on self-referential and mutually recursive lazy graphs is unaffected because the marker is still present during recursive descent into the same value.
22
+
23
+ ## 0.5.0
24
+
25
+ ### Minor Changes
26
+
27
+ - [#9211](https://github.com/scalar/scalar/pull/9211): feat: support default values for coersion
28
+
29
+ ## 0.4.0
30
+
31
+ ### Minor Changes
32
+
33
+ - [#8844](https://github.com/scalar/scalar/pull/8844): feat: support default values for coersion
34
+
3
35
  ## 0.3.2
4
36
 
5
37
  ### Patch Changes
package/dist/coerce.d.ts CHANGED
@@ -1,5 +1,31 @@
1
1
  import type { Schema } from './schema.js';
2
2
  import type { Static } from './types.js';
3
+ /**
4
+ * Memoizes `schema.schema()` per lazy schema so that recursive definitions
5
+ * such as `lazy(() => object({ child: lazy(() => T) }))` resolve to the same
6
+ * inner schema reference across calls. Without this, every traversal would
7
+ * synthesize a fresh inner schema, defeating the `(value, schema)` cycle
8
+ * cache and producing infinite recursion on self-referential values.
9
+ *
10
+ * The cache is supplied by the top-level `coerce` call so it never leaks
11
+ * resolved schemas between unrelated invocations.
12
+ */
13
+ type LazyCache = WeakMap<object, Schema>;
14
+ /**
15
+ * Falls back to `any` when `S` widens all the way to the full `Schema` union and
16
+ * returns the precise `Static<S>` otherwise. Computing `Static<Schema>` forces
17
+ * TypeScript to expand every variant of the recursive `Schema` definition and
18
+ * exhausts the depth limit, surfacing at call sites as
19
+ * `TS2589: Type instantiation is excessively deep and possibly infinite`.
20
+ * Degrading to `any` in that single case keeps the type tractable; callers that
21
+ * pass a specific schema (for example an `intersection(...)` literal) still get
22
+ * the precise static type.
23
+ *
24
+ * `[Schema] extends [S]` is wrapped in tuples to prevent distribution over union
25
+ * members — we want a single check that the whole `Schema` union is assignable
26
+ * to `S`, not a check that runs once per variant.
27
+ */
28
+ type SafeStatic<S extends Schema> = [Schema] extends [S] ? any : Static<S>;
3
29
  /**
4
30
  * Coerces an unknown value toward the static type implied by `schema`. Values that
5
31
  * pass {@link validate} for that branch are kept; otherwise primitives default to
@@ -18,5 +44,6 @@ import type { Static } from './types.js';
18
44
  * The optional `cache` argument tracks visited object–schema pairs to stop infinite recursion
19
45
  * on cyclic graphs; callers normally omit it.
20
46
  */
21
- export declare const coerce: <S extends Schema>(schema: S, value: unknown, cache?: WeakMap<object, Set<Schema>>) => Static<S>;
47
+ export declare const coerce: <S extends Schema>(schema: S, value: unknown, cache?: WeakMap<object, Map<Schema, unknown>>, lazyCache?: LazyCache) => SafeStatic<S>;
48
+ export {};
22
49
  //# sourceMappingURL=coerce.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"coerce.d.ts","sourceRoot":"","sources":["../src/coerce.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AA0FrC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,MAAM,EACrC,QAAQ,CAAC,EACT,OAAO,OAAO,EACd,QAAO,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAiB,KAClD,MAAM,CAAC,CAAC,CA6GV,CAAA"}
1
+ {"version":3,"file":"coerce.d.ts","sourceRoot":"","sources":["../src/coerce.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAGrC;;;;;;;;;GASG;AACH,KAAK,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AA+SxC;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,MAAM,EACrC,QAAQ,CAAC,EACT,OAAO,OAAO,EACd,QAAO,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAiB,EAC5D,YAAW,SAAyB,KACnC,UAAU,CAAC,CAAC,CAAkE,CAAA"}
package/dist/coerce.js CHANGED
@@ -1,5 +1,14 @@
1
1
  import { isObject } from './helpers/is-object.js';
2
2
  import { validate } from './validate.js';
3
+ const resolveLazy = (schema, lazyCache) => {
4
+ const cached = lazyCache.get(schema);
5
+ if (cached) {
6
+ return cached;
7
+ }
8
+ const resolved = schema.schema();
9
+ lazyCache.set(schema, resolved);
10
+ return resolved;
11
+ };
3
12
  /**
4
13
  * True when this property schema is only used to discriminate union branches
5
14
  * (single literal, or a union of literals). No presence bonus when the value
@@ -24,116 +33,157 @@ const isDiscriminatorProperty = (schema) => {
24
33
  * Higher score means a closer match. Literals and matching object shapes
25
34
  * are weighted more heavily. Objects are scored by shape/literals;
26
35
  * arrays/records by structural type; primitives by validation; unions try all branches.
36
+ *
37
+ * The `scoringCache` tracks `(value, schema)` pairs that are currently being
38
+ * scored higher up the call stack. Without it, a recursive lazy schema such as
39
+ * `lazy(() => union([object({ child: optional(lazy(() => T)) }), …]))` scored
40
+ * against a self-referential value would recurse forever through
41
+ * `lazy → union → object → property → lazy → …` and overflow the stack.
42
+ *
43
+ * On re-entry of a pair we return `1` rather than `0` — a neutral positive
44
+ * score consistent with `validateInner` short-circuiting cycles to `true`.
45
+ * Markers are removed in `finally` so sibling union branches that share a
46
+ * schema reference are scored independently rather than inheriting a stale
47
+ * "in cycle" marker.
27
48
  */
28
- const scoreUnion = (schema, value) => {
29
- if (schema.type === 'object') {
30
- if (!isObject(value)) {
31
- return 0;
32
- }
33
- // Missing keys contribute 0 (including optional keys — matches prior union heuristics).
34
- // Discriminator properties (`literal` or `union` of literals): recurse with scoreUnion;
35
- // matching values get a high weight (×10) so `type: literal('A')` beats unrelated fields
36
- // on another branch; mismatches score 0 (no "key present" tie-break).
37
- // Other properties: scoreUnion plus +1 when the value fails validation so `{ a: null }`
38
- // can still prefer the branch that declares `a`.
39
- return Object.keys(schema.properties).reduce((acc, key) => {
40
- if (!(key in value)) {
41
- return acc;
42
- }
43
- const propSchema = schema.properties[key];
44
- const raw = value[key];
45
- const base = scoreUnion(propSchema, raw);
46
- if (isDiscriminatorProperty(propSchema)) {
47
- return acc + (base > 0 ? base * 10 : 0);
48
- }
49
- return acc + (base > 0 ? base : 1);
50
- }, 0);
51
- }
52
- if (schema.type === 'array') {
53
- // Score 1 if value is an array, otherwise 0
54
- return Array.isArray(value) ? 1 : 0;
55
- }
56
- if (schema.type === 'record') {
57
- // TODO: implement smarter scoring for records (just a placeholder for now)
58
- return isObject(value) ? 1 : 0;
59
- }
60
- if (schema.type === 'optional') {
61
- return value === undefined ? 1 : scoreUnion(schema.schema, value);
49
+ const scoreUnion = (schema, value, lazyCache, scoringCache = new WeakMap()) => {
50
+ // Short-circuit on cycles: this exact (value, schema) pair is already being
51
+ // scored higher up the call stack. The enclosing call's score subsumes any
52
+ // contribution we could compute here, so return a neutral positive score.
53
+ if (isObject(value) && scoringCache.get(value)?.has(schema)) {
54
+ return 1;
62
55
  }
63
- if (schema.type === 'union') {
64
- // For a union, use the highest score among all sub-schemas
65
- return Math.max(...schema.schemas.map((schema) => scoreUnion(schema, value)));
56
+ const trackable = isObject(value);
57
+ if (trackable) {
58
+ const schemas = scoringCache.get(value) ?? new Set();
59
+ schemas.add(schema);
60
+ scoringCache.set(value, schemas);
66
61
  }
67
- if (schema.type === 'intersection') {
68
- if (schema.schemas.length === 0) {
69
- return 1;
62
+ try {
63
+ if (schema.type === 'object') {
64
+ if (!isObject(value)) {
65
+ return 0;
66
+ }
67
+ const keys = Object.keys(schema.properties);
68
+ // If there are no properties, we want to score 1 since we want to outscore if there are inline primitives
69
+ if (keys.length === 0) {
70
+ return 1;
71
+ }
72
+ // Missing keys contribute 0 (including optional keys — matches prior union heuristics).
73
+ // Discriminator properties (`literal` or `union` of literals): recurse with scoreUnion;
74
+ // matching values get a high weight (×10) so `type: literal('A')` beats unrelated fields
75
+ // on another branch; mismatches score 0 (no "key present" tie-break).
76
+ // Other properties: scoreUnion plus +1 when the value fails validation so `{ a: null }`
77
+ // can still prefer the branch that declares `a`.
78
+ return keys.reduce((acc, key) => {
79
+ if (!(key in value)) {
80
+ return acc;
81
+ }
82
+ const propSchema = schema.properties[key];
83
+ const raw = value[key];
84
+ const base = scoreUnion(propSchema, raw, lazyCache, scoringCache);
85
+ if (isDiscriminatorProperty(propSchema)) {
86
+ return acc + (base > 0 ? base * 10 : 0);
87
+ }
88
+ return acc + (base > 0 ? base : 1);
89
+ }, 0);
70
90
  }
71
- return schema.schemas.reduce((acc, sub) => acc + scoreUnion(sub, value), 0);
72
- }
73
- if (schema.type === 'lazy') {
74
- // For a lazy schema, evaluate the inner schema and recurse
75
- return scoreUnion(schema.schema(), value);
91
+ if (schema.type === 'array') {
92
+ // Score 1 if value is an array, otherwise 0
93
+ return Array.isArray(value) ? 1 : 0;
94
+ }
95
+ if (schema.type === 'record') {
96
+ // TODO: implement smarter scoring for records (just a placeholder for now)
97
+ return isObject(value) ? 1 : 0;
98
+ }
99
+ if (schema.type === 'optional') {
100
+ return value === undefined ? 1 : scoreUnion(schema.schema, value, lazyCache, scoringCache);
101
+ }
102
+ if (schema.type === 'union') {
103
+ // For a union, use the highest score among all sub-schemas
104
+ return Math.max(...schema.schemas.map((branch) => scoreUnion(branch, value, lazyCache, scoringCache)));
105
+ }
106
+ if (schema.type === 'intersection') {
107
+ if (schema.schemas.length === 0) {
108
+ return 1;
109
+ }
110
+ return schema.schemas.reduce((acc, sub) => acc + scoreUnion(sub, value, lazyCache, scoringCache), 0);
111
+ }
112
+ if (schema.type === 'lazy') {
113
+ // For a lazy schema, evaluate the inner schema and recurse
114
+ return scoreUnion(resolveLazy(schema, lazyCache), value, lazyCache, scoringCache);
115
+ }
116
+ if (schema.type === 'evaluate') {
117
+ // For an evaluate schema, evaluate the expression and recurse
118
+ return scoreUnion(schema.schema, schema.expression(value), lazyCache, scoringCache);
119
+ }
120
+ // For primitives and any other type, return 1 if valid, otherwise 0
121
+ return validate(schema, value) ? 1 : 0;
76
122
  }
77
- if (schema.type === 'evaluate') {
78
- // For an evaluate schema, evaluate the expression and recurse
79
- return scoreUnion(schema.schema, schema.expression(value));
123
+ finally {
124
+ // Clear the in-progress marker so sibling union branches that reference
125
+ // the same schema are scored independently rather than short-circuiting
126
+ // to the cycle-neutral score.
127
+ if (trackable) {
128
+ scoringCache.get(value)?.delete(schema);
129
+ }
80
130
  }
81
- // For primitives and any other type, return 1 if valid, otherwise 0
82
- return validate(schema, value) ? 1 : 0;
83
131
  };
84
132
  /**
85
- * Coerces an unknown value toward the static type implied by `schema`. Values that
86
- * pass {@link validate} for that branch are kept; otherwise primitives default to
87
- * `0`, `''`, or `false`, and arrays, records, and objects are built recursively.
88
- * Unions pick the best-matching branch; `evaluate` runs `expression` before the inner schema.
89
- *
90
- * @example
91
- * ```ts
92
- * import { coerce, number, object, string } from '@scalar/validation'
93
- *
94
- * coerce(number(), 42) // 42
95
- * coerce(number(), 'nope') // 0 — invalid number uses default
96
- * coerce(object({ id: number(), name: string() }), { id: '1', name: 'Ada' }) // { id: 0, name: 'Ada' }
97
- * ```
98
- *
99
- * The optional `cache` argument tracks visited object–schema pairs to stop infinite recursion
100
- * on cyclic graphs; callers normally omit it.
133
+ * Records the in-progress `result` for a given `(value, schema)` pair so that
134
+ * recursive calls hitting the same pair return the already-allocated result
135
+ * instead of recursing forever. Plain objects and arrays are both tracked;
136
+ * other values cannot form cycles and are ignored.
101
137
  */
102
- export const coerce = (schema, value, cache = new WeakMap()) => {
103
- // Prevent infinite recursion
104
- if (isObject(value) && cache.get(value)?.has(schema)) {
105
- return value;
106
- }
107
- // Track visited schemas to prevent infinite recursion
108
- if (isObject(value)) {
109
- const schemas = cache.get(value) || new Set();
110
- schemas.add(schema);
138
+ const trackCycle = (value, schema, result, cache) => {
139
+ if (isObject(value) || Array.isArray(value)) {
140
+ const schemas = cache.get(value) || new Map();
141
+ schemas.set(schema, result);
111
142
  cache.set(value, schemas);
112
143
  }
144
+ };
145
+ /**
146
+ * Internal coercion implementation. Takes the wide `Schema` union and returns `unknown` so that
147
+ * recursive calls do not pay the cost of relating two generic `Static<S>` instantiations, which
148
+ * can overflow the type checker now that `LazyStatic` resolves recursive schemas without a depth
149
+ * cap. The public `coerce` wrapper preserves the typed surface.
150
+ */
151
+ const coerceInner = (schema, value, cache, lazyCache) => {
152
+ // Prevent infinite recursion by returning the in-progress result that was
153
+ // staged by an enclosing call via trackCycle.
154
+ if ((isObject(value) || Array.isArray(value)) && cache.get(value)?.has(schema)) {
155
+ return cache.get(value)?.get(schema);
156
+ }
113
157
  // If no schema is provided, return the value as is
114
158
  if (!schema) {
115
159
  return value;
116
160
  }
117
- if (schema.type === 'any') {
161
+ if (schema.type === 'any' || schema.type === 'unknown') {
118
162
  return value;
119
163
  }
164
+ if (schema.type === 'function') {
165
+ if (typeof value === 'function') {
166
+ return value;
167
+ }
168
+ return () => undefined;
169
+ }
120
170
  if (schema.type === 'number') {
121
171
  if (validate(schema, value)) {
122
172
  return value;
123
173
  }
124
- return 0;
174
+ return schema.default ?? 0;
125
175
  }
126
176
  if (schema.type === 'string') {
127
177
  if (validate(schema, value)) {
128
178
  return value;
129
179
  }
130
- return '';
180
+ return schema.default ?? '';
131
181
  }
132
182
  if (schema.type === 'boolean') {
133
183
  if (validate(schema, value)) {
134
184
  return value;
135
185
  }
136
- return false;
186
+ return schema.default ?? false;
137
187
  }
138
188
  if (schema.type === 'nullable') {
139
189
  return null;
@@ -145,56 +195,92 @@ export const coerce = (schema, value, cache = new WeakMap()) => {
145
195
  if (value === undefined) {
146
196
  return undefined;
147
197
  }
148
- return coerce(schema.schema, value, cache);
198
+ return coerceInner(schema.schema, value, cache, lazyCache);
149
199
  }
150
200
  if (schema.type === 'array') {
151
201
  if (!Array.isArray(value)) {
152
202
  return [];
153
203
  }
154
- return value.map((item) => coerce(schema.items, item, cache));
204
+ // Pre-allocate so a self-referential array can be cached before we
205
+ // recurse into its items, breaking otherwise-infinite cycles.
206
+ const result = new Array(value.length);
207
+ trackCycle(value, schema, result, cache);
208
+ for (let i = 0; i < value.length; i++) {
209
+ result[i] = coerceInner(schema.items, value[i], cache, lazyCache);
210
+ }
211
+ return result;
155
212
  }
156
213
  if (schema.type === 'record') {
157
214
  if (!isObject(value)) {
158
215
  return {};
159
216
  }
160
- return Object.fromEntries(Object.entries(value).map(([key, value]) => [key, coerce(schema.value, value, cache)]));
217
+ // Pre-allocate so a self-referential record can be cached before we
218
+ // recurse into its entries, breaking otherwise-infinite cycles.
219
+ const result = {};
220
+ trackCycle(value, schema, result, cache);
221
+ for (const key of Object.keys(value)) {
222
+ result[key] = coerceInner(schema.value, value[key], cache, lazyCache);
223
+ }
224
+ return result;
161
225
  }
162
226
  if (schema.type === 'object') {
163
227
  const keys = Object.keys(schema.properties);
164
228
  const target = isObject(value) ? value : null;
165
- const entries = [];
229
+ // Pre-allocate so a self-referential object can be cached before we
230
+ // recurse into its properties, breaking otherwise-infinite cycles.
231
+ const result = {};
232
+ trackCycle(value, schema, result, cache);
166
233
  for (const key of keys) {
167
234
  const propSchema = schema.properties[key];
168
235
  const raw = target?.[key];
169
236
  if (propSchema.type === 'optional' && raw === undefined) {
170
237
  continue;
171
238
  }
172
- entries.push([key, coerce(propSchema, raw, cache)]);
239
+ result[key] = coerceInner(propSchema, raw, cache, lazyCache);
173
240
  }
174
- return Object.fromEntries(entries);
241
+ return result;
175
242
  }
176
243
  if (schema.type === 'union') {
177
- const branch = schema.schemas.reduce((acc, schema) => {
178
- const score = scoreUnion(schema, value);
179
- return score > acc.score ? { schema, score } : acc;
244
+ const branch = schema.schemas.reduce((acc, branchSchema) => {
245
+ const score = scoreUnion(branchSchema, value, lazyCache);
246
+ return score > acc.score ? { schema: branchSchema, score } : acc;
180
247
  }, { schema: schema.schemas[0], score: 0 });
181
248
  // We need some way to pick one of the union values
182
- return coerce(branch.schema, value, cache);
249
+ return coerceInner(branch.schema, value, cache, lazyCache);
183
250
  }
184
251
  if (schema.type === 'intersection') {
185
- return schema.schemas.reduce((acc, subSchema) => Object.assign(acc, coerce(subSchema, value, cache)), {});
252
+ return schema.schemas.reduce((acc, subSchema) => Object.assign(acc, coerceInner(subSchema, value, cache, lazyCache)), {});
186
253
  }
187
254
  if (schema.type === 'literal') {
188
255
  return schema.value;
189
256
  }
190
257
  if (schema.type === 'lazy') {
191
- return coerce(schema.schema(), value, cache);
258
+ return coerceInner(resolveLazy(schema, lazyCache), value, cache, lazyCache);
192
259
  }
193
260
  if (schema.type === 'evaluate') {
194
- return coerce(schema.schema, schema.expression(value), cache);
261
+ return coerceInner(schema.schema, schema.expression(value), cache, lazyCache);
195
262
  }
196
263
  // We need to assert here that schema has the type never so we know we handle all cases
197
264
  const _exhaustive = schema;
198
265
  console.warn('Unknown schema type:', _exhaustive);
199
266
  return value;
200
267
  };
268
+ /**
269
+ * Coerces an unknown value toward the static type implied by `schema`. Values that
270
+ * pass {@link validate} for that branch are kept; otherwise primitives default to
271
+ * `0`, `''`, or `false`, and arrays, records, and objects are built recursively.
272
+ * Unions pick the best-matching branch; `evaluate` runs `expression` before the inner schema.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * import { coerce, number, object, string } from '@scalar/validation'
277
+ *
278
+ * coerce(number(), 42) // 42
279
+ * coerce(number(), 'nope') // 0 — invalid number uses default
280
+ * coerce(object({ id: number(), name: string() }), { id: '1', name: 'Ada' }) // { id: 0, name: 'Ada' }
281
+ * ```
282
+ *
283
+ * The optional `cache` argument tracks visited object–schema pairs to stop infinite recursion
284
+ * on cyclic graphs; callers normally omit it.
285
+ */
286
+ export const coerce = (schema, value, cache = new WeakMap(), lazyCache = new WeakMap()) => coerceInner(schema, value, cache, lazyCache);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { coerce } from './coerce.js';
2
- export { type AnySchema, type ArraySchema, type BooleanSchema, type EvaluateSchema, type IntersectionSchema, type LazySchema, type LiteralSchema, type NotDefinedSchema, type NullableSchema, type NumberSchema, type ObjectSchema, type OptionalSchema, type RecordSchema, type Schema, type StringSchema, type UnionSchema, any, array, boolean, evaluate, intersection, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, } from './schema.js';
2
+ export { type AnySchema, type ArraySchema, type BooleanSchema, type EvaluateSchema, type FunctionSchema, type IntersectionMember, type IntersectionSchema, type LazySchema, type LiteralSchema, type NotDefinedSchema, type NullableSchema, type NumberSchema, type ObjectSchema, type OptionalSchema, type RecordSchema, type Schema, type StringSchema, type UnionMember, type UnionSchema, type UnknownSchema, any, array, boolean, evaluate, fn, intersection, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, unknown, } from './schema.js';
3
3
  export { type GenerateTypesOptions, generateTypes } from './typegen.js';
4
4
  export type { Static } from './types.js';
5
5
  export { validate } from './validate.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EACL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,IAAI,EACJ,OAAO,EACP,UAAU,EACV,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,EACR,MAAM,EACN,MAAM,EACN,KAAK,GACN,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACpE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EACL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,GAAG,EACH,KAAK,EACL,OAAO,EACP,QAAQ,EACR,EAAE,EACF,YAAY,EACZ,IAAI,EACJ,OAAO,EACP,UAAU,EACV,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,EACR,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,GACR,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACpE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { coerce } from './coerce.js';
2
- export { any, array, boolean, evaluate, intersection, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, } from './schema.js';
2
+ export { any, array, boolean, evaluate, fn, intersection, lazy, literal, notDefined, nullable, number, object, optional, record, string, union, unknown, } from './schema.js';
3
3
  export { generateTypes } from './typegen.js';
4
4
  export { validate } from './validate.js';
package/dist/schema.d.ts CHANGED
@@ -12,14 +12,17 @@ type Documentation = Partial<{
12
12
  /** Schema for finite numeric values. {@link Static} resolves to `number`. */
13
13
  export type NumberSchema = {
14
14
  type: 'number';
15
+ default?: number;
15
16
  } & Documentation;
16
17
  /** Schema for string values. {@link Static} resolves to `string`. */
17
18
  export type StringSchema = {
18
19
  type: 'string';
20
+ default?: string;
19
21
  } & Documentation;
20
22
  /** Schema for boolean values. {@link Static} resolves to `boolean`. */
21
23
  export type BooleanSchema = {
22
24
  type: 'boolean';
25
+ default?: boolean;
23
26
  } & Documentation;
24
27
  /** Schema for `null`. {@link Static} resolves to `null`. */
25
28
  export type NullableSchema = {
@@ -33,6 +36,20 @@ export type NotDefinedSchema = {
33
36
  export type AnySchema = {
34
37
  type: 'any';
35
38
  } & Documentation;
39
+ /** Schema that accepts any value. {@link Static} resolves to `unknown` instead of `any`. */
40
+ export type UnknownSchema = {
41
+ type: 'unknown';
42
+ } & Documentation;
43
+ /**
44
+ * Schema for function values. Validates only that the value is `typeof === 'function'`.
45
+ * The type parameter `T` carries the full function signature for {@link Static} inference
46
+ * but is never checked at runtime.
47
+ */
48
+ export type FunctionSchema<T extends (...args: any[]) => any = (...args: unknown[]) => unknown> = {
49
+ type: 'function';
50
+ /** Phantom field — never set at runtime. Carries `T` so that `Static` can extract it. */
51
+ _fn?: T;
52
+ } & Documentation;
36
53
  /** Schema for homogeneous lists. {@link Static} resolves to an array of the item static type. */
37
54
  export type ArraySchema<Item extends Schema> = {
38
55
  type: 'array';
@@ -50,10 +67,52 @@ export type ObjectSchema<Properties extends Record<string, Schema>> = {
50
67
  properties: Properties;
51
68
  } & Documentation;
52
69
  /** Schema that matches if any member schema matches (discriminated union when literals or object tags differ). */
53
- export type UnionSchema<Schemas extends Schema[]> = {
70
+ export type UnionSchema<Schemas extends readonly Schema[]> = {
54
71
  type: 'union';
55
72
  schemas: Schemas;
56
73
  } & Documentation;
74
+ /**
75
+ * Members that may appear inside a `union([...])`.
76
+ *
77
+ * Discriminant-only for the same reason as {@link IntersectionMember}: a full `Schema` constraint
78
+ * at the call site forces TypeScript to eagerly evaluate tuple elements and breaks circular
79
+ * inference when members use `lazy(() => self)` (for example recursive navigation trees).
80
+ */
81
+ export type UnionMember = {
82
+ type: 'number';
83
+ } | {
84
+ type: 'string';
85
+ } | {
86
+ type: 'boolean';
87
+ } | {
88
+ type: 'nullable';
89
+ } | {
90
+ type: 'notDefined';
91
+ } | {
92
+ type: 'any';
93
+ } | {
94
+ type: 'unknown';
95
+ } | {
96
+ type: 'function';
97
+ } | {
98
+ type: 'array';
99
+ } | {
100
+ type: 'record';
101
+ } | {
102
+ type: 'object';
103
+ } | {
104
+ type: 'union';
105
+ } | {
106
+ type: 'optional';
107
+ } | {
108
+ type: 'intersection';
109
+ } | {
110
+ type: 'literal';
111
+ } | {
112
+ type: 'lazy';
113
+ } | {
114
+ type: 'evaluate';
115
+ };
57
116
  /**
58
117
  * Schema that accepts `undefined` or a value matching the inner schema.
59
118
  * In {@link Static} and type generation, object properties use `key?:` instead of `T | undefined`.
@@ -63,10 +122,28 @@ export type OptionalSchema<S extends Schema> = {
63
122
  schema: S;
64
123
  } & Documentation;
65
124
  /**
66
- * `UnionSchema<any>` avoids a variance pitfall: `UnionSchema<[A, B]>` is not assignable to
67
- * `UnionSchema<ObjectSchema<any>[]>`, which breaks `infer` when resolving `Static`.
125
+ * Members that may appear inside an `intersection([...])`.
126
+ *
127
+ * This is intentionally a discriminant-only structural type rather than a union of the full
128
+ * schema types. If we use the full schema types here, the constraint check at the call site
129
+ * of `intersection` forces TypeScript to *eagerly* evaluate each tuple element, which breaks
130
+ * circular type inference when a member transitively contains a `lazy(() => self)` reference.
131
+ *
132
+ * The narrower discriminant shape only requires TypeScript to verify the `type` literal, which
133
+ * is cheap and does not trigger evaluation of nested schemas. Every `ObjectSchema`, `UnionSchema`,
134
+ * `IntersectionSchema`, and `LazySchema` is still assignable to this type because each carries
135
+ * the appropriate `type` field, so the public API and compile-time safety are preserved.
68
136
  */
69
- export type IntersectionSchema<Schemas extends readonly (ObjectSchema<any> | UnionSchema<any>)[]> = {
137
+ export type IntersectionMember = {
138
+ type: 'object';
139
+ } | {
140
+ type: 'union';
141
+ } | {
142
+ type: 'intersection';
143
+ } | {
144
+ type: 'lazy';
145
+ };
146
+ export type IntersectionSchema<Schemas extends readonly Schema[]> = {
70
147
  type: 'intersection';
71
148
  schemas: Schemas;
72
149
  } & Documentation;
@@ -93,21 +170,41 @@ export type EvaluateSchema<S extends Schema> = {
93
170
  expression: (value: unknown) => unknown;
94
171
  schema: S;
95
172
  };
96
- export type Schema = NumberSchema | StringSchema | BooleanSchema | NullableSchema | NotDefinedSchema | AnySchema | ArraySchema<any> | RecordSchema<any, any> | ObjectSchema<Record<string, any>> | UnionSchema<any[]> | OptionalSchema<any> | IntersectionSchema<readonly (ObjectSchema<any> | UnionSchema<ObjectSchema<any>[]>)[]> | LiteralSchema<any> | LazySchema<any> | EvaluateSchema<any>;
97
- declare const number: (options?: Documentation) => NumberSchema;
98
- declare const string: (options?: Documentation) => StringSchema;
99
- declare const boolean: (options?: Documentation) => BooleanSchema;
173
+ export type Schema = NumberSchema | StringSchema | BooleanSchema | NullableSchema | NotDefinedSchema | AnySchema | UnknownSchema | FunctionSchema<any> | ArraySchema<any> | RecordSchema<any, any> | ObjectSchema<Record<string, any>> | UnionSchema<readonly Schema[]> | OptionalSchema<any> | IntersectionSchema<readonly Schema[]> | LiteralSchema<any> | LazySchema<any> | EvaluateSchema<any>;
174
+ declare const number: (options?: Documentation & {
175
+ default?: number;
176
+ }) => NumberSchema;
177
+ declare const string: (options?: Documentation & {
178
+ default?: string;
179
+ }) => StringSchema;
180
+ declare const boolean: (options?: Documentation & {
181
+ default?: boolean;
182
+ }) => BooleanSchema;
100
183
  declare const nullable: (options?: Documentation) => NullableSchema;
101
184
  declare const notDefined: (options?: Documentation) => NotDefinedSchema;
102
185
  declare const any: (options?: Documentation) => AnySchema;
186
+ declare const unknown: (options?: Documentation) => UnknownSchema;
187
+ declare const fn: <T extends (...args: any[]) => any = (...args: unknown[]) => unknown>(options?: Documentation) => FunctionSchema<T>;
103
188
  declare const array: <Item extends Schema>(items: Item, options?: Documentation) => ArraySchema<Item>;
104
189
  declare const record: <Key extends StringSchema | AnySchema, Value extends Schema>(key: Key, value: Value, options?: Documentation) => RecordSchema<Key, Value>;
105
190
  declare const object: <Properties extends Record<string, Schema>>(properties: Properties, options?: Documentation) => ObjectSchema<Properties>;
106
- declare const union: <Schemas extends Schema[]>(schemas: Schemas, options?: Documentation) => UnionSchema<Schemas>;
107
- declare const intersection: <const Schemas extends readonly (ObjectSchema<any> | UnionSchema<ObjectSchema<any>[]>)[]>(schemas: Schemas, options?: Documentation) => IntersectionSchema<Schemas>;
191
+ /**
192
+ * The conditional return type mirrors {@link intersection}: lightweight input constraint,
193
+ * precise `UnionSchema<Schemas>` output without re-triggering eager evaluation.
194
+ */
195
+ declare const union: <const Schemas extends readonly UnionMember[]>(schemas: Schemas, options?: Documentation) => Schemas extends readonly Schema[] ? UnionSchema<Schemas> : never;
196
+ /**
197
+ * The conditional return type is what unlocks circular `intersection([... lazy(() => self) ...])`.
198
+ *
199
+ * The input constraint is the lightweight `IntersectionMember` (discriminant-only) so the call-site
200
+ * constraint check does not force TypeScript to eagerly evaluate each tuple element. The conditional
201
+ * `Schemas extends readonly Schema[]` is always true in practice (every passed value is a real schema)
202
+ * and lets us produce a precise `IntersectionSchema<Schemas>` without re-introducing the heavy check.
203
+ */
204
+ declare const intersection: <const Schemas extends readonly IntersectionMember[]>(schemas: Schemas, options?: Documentation) => Schemas extends readonly Schema[] ? IntersectionSchema<Schemas> : never;
108
205
  declare const optional: <S extends Schema>(schema: S, options?: Documentation) => OptionalSchema<S>;
109
206
  declare const literal: <Value extends string | number | boolean | bigint>(value: Value) => LiteralSchema<Value>;
110
207
  declare const lazy: <S extends () => Schema>(schema: S) => LazySchema<S>;
111
208
  declare const evaluate: <S extends Schema>(expression: (value: unknown) => unknown, schema: S) => EvaluateSchema<S>;
112
- export { number, string, boolean, nullable, notDefined, any, array, record, object, union, intersection, optional, literal, lazy, evaluate, };
209
+ export { number, string, boolean, nullable, notDefined, any, unknown, fn, array, record, object, union, intersection, optional, literal, lazy, evaluate, };
113
210
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,KAAK,aAAa,GAAG,OAAO,CAAC;IAC3B,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAA;IACnB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAC,CAAA;AAEF,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,GAAG,aAAa,CAAA;AAEjB,qEAAqE;AACrE,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,GAAG,aAAa,CAAA;AAEjB,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,SAAS,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,GAAG,aAAa,CAAA;AAEjB,4DAA4D;AAC5D,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,UAAU,CAAA;CACjB,GAAG,aAAa,CAAA;AAEjB,qFAAqF;AACrF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,YAAY,CAAA;CACnB,GAAG,aAAa,CAAA;AAEjB,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,KAAK,CAAA;CACZ,GAAG,aAAa,CAAA;AAEjB,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,SAAS,CAAA;CAChB,GAAG,aAAa,CAAA;AAEjB;;;;GAIG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,IAAI;IAChG,IAAI,EAAE,UAAU,CAAA;IAChB,yFAAyF;IACzF,GAAG,CAAC,EAAE,CAAC,CAAA;CACR,GAAG,aAAa,CAAA;AAEjB,iGAAiG;AACjG,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,MAAM,IAAI;IAC7C,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,IAAI,CAAA;CACZ,GAAG,aAAa,CAAA;AAEjB,4GAA4G;AAC5G,MAAM,MAAM,YAAY,CAAC,GAAG,SAAS,YAAY,GAAG,YAAY,GAAG,SAAS,EAAE,KAAK,SAAS,MAAM,IAAI;IACpG,IAAI,EAAE,QAAQ,CAAA;IACd,GAAG,EAAE,GAAG,CAAA;IACR,KAAK,EAAE,KAAK,CAAA;CACb,GAAG,aAAa,CAAA;AAEjB,yFAAyF;AACzF,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI;IACpE,IAAI,EAAE,QAAQ,CAAA;IACd,UAAU,EAAE,UAAU,CAAA;CACvB,GAAG,aAAa,CAAA;AAEjB,kHAAkH;AAClH,MAAM,MAAM,WAAW,CAAC,OAAO,SAAS,SAAS,MAAM,EAAE,IAAI;IAC3D,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;CACjB,GAAG,aAAa,CAAA;AAEjB;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAExB;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI;IAC7C,IAAI,EAAE,UAAU,CAAA;IAChB,MAAM,EAAE,CAAC,CAAA;CACV,GAAG,aAAa,CAAA;AAEjB;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAErH,MAAM,MAAM,kBAAkB,CAAC,OAAO,SAAS,SAAS,MAAM,EAAE,IAAI;IAClE,IAAI,EAAE,cAAc,CAAA;IACpB,OAAO,EAAE,OAAO,CAAA;CACjB,GAAG,aAAa,CAAA;AAEjB,oHAAoH;AACpH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,IAAI;IACxE,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,EAAE,CAAC,CAAA;CACT,GAAG,aAAa,CAAA;AAEjB;;;;GAIG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,MAAM,IAAI;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,CAAC,CAAA;CACV,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI;IAC7C,IAAI,EAAE,UAAU,CAAA;IAChB,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;IACvC,MAAM,EAAE,CAAC,CAAA;CACV,CAAA;AAED,MAAM,MAAM,MAAM,GACd,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,cAAc,GACd,gBAAgB,GAChB,SAAS,GACT,aAAa,GACb,cAAc,CAAC,GAAG,CAAC,GACnB,WAAW,CAAC,GAAG,CAAC,GAChB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,GACtB,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,GACjC,WAAW,CAAC,SAAS,MAAM,EAAE,CAAC,GAC9B,cAAc,CAAC,GAAG,CAAC,GACnB,kBAAkB,CAAC,SAAS,MAAM,EAAE,CAAC,GACrC,aAAa,CAAC,GAAG,CAAC,GAClB,UAAU,CAAC,GAAG,CAAC,GACf,cAAc,CAAC,GAAG,CAAC,CAAA;AAEvB,QAAA,MAAM,MAAM,GAAI,UAAU,aAAa,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,YAK/D,CAAA;AAEF,QAAA,MAAM,MAAM,GAAI,UAAU,aAAa,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,YAK/D,CAAA;AAEF,QAAA,MAAM,OAAO,GAAI,UAAU,aAAa,GAAG;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,KAAG,aAKjE,CAAA;AAEF,QAAA,MAAM,QAAQ,GAAI,UAAU,aAAa,KAAG,cAI1C,CAAA;AAEF,QAAA,MAAM,UAAU,GAAI,UAAU,aAAa,KAAG,gBAI5C,CAAA;AAEF,QAAA,MAAM,GAAG,GAAI,UAAU,aAAa,KAAG,SAIrC,CAAA;AAEF,QAAA,MAAM,OAAO,GAAI,UAAU,aAAa,KAAG,aAIzC,CAAA;AAEF,QAAA,MAAM,EAAE,GAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAC7E,UAAU,aAAa,KACtB,cAAc,CAAC,CAAC,CAIjB,CAAA;AAEF,QAAA,MAAM,KAAK,GAAI,IAAI,SAAS,MAAM,EAAE,OAAO,IAAI,EAAE,UAAU,aAAa,KAAG,WAAW,CAAC,IAAI,CAKzF,CAAA;AAEF,QAAA,MAAM,MAAM,GAAI,GAAG,SAAS,YAAY,GAAG,SAAS,EAAE,KAAK,SAAS,MAAM,EACxE,KAAK,GAAG,EACR,OAAO,KAAK,EACZ,UAAU,aAAa,KACtB,YAAY,CAAC,GAAG,EAAE,KAAK,CAMxB,CAAA;AAEF,QAAA,MAAM,MAAM,GAAI,UAAU,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACvD,YAAY,UAAU,EACtB,UAAU,aAAa,KACtB,YAAY,CAAC,UAAU,CAKxB,CAAA;AAEF;;;GAGG;AACH,QAAA,MAAM,KAAK,GAAI,KAAK,CAAC,OAAO,SAAS,SAAS,WAAW,EAAE,EACzD,SAAS,OAAO,EAChB,UAAU,aAAa,KACtB,OAAO,SAAS,SAAS,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,KAMjD,CAAA;AAEb;;;;;;;GAOG;AACH,QAAA,MAAM,YAAY,GAAI,KAAK,CAAC,OAAO,SAAS,SAAS,kBAAkB,EAAE,EACvE,SAAS,OAAO,EAChB,UAAU,aAAa,KACtB,OAAO,SAAS,SAAS,MAAM,EAAE,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,KAMxD,CAAA;AAEb,QAAA,MAAM,QAAQ,GAAI,CAAC,SAAS,MAAM,EAAE,QAAQ,CAAC,EAAE,UAAU,aAAa,KAAG,cAAc,CAAC,CAAC,CAKvF,CAAA;AAEF,QAAA,MAAM,OAAO,GAAI,KAAK,SAAS,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,OAAO,KAAK,KAAG,aAAa,CAAC,KAAK,CAGnG,CAAA;AAEF,QAAA,MAAM,IAAI,GAAI,CAAC,SAAS,MAAM,MAAM,EAAE,QAAQ,CAAC,KAAG,UAAU,CAAC,CAAC,CAG5D,CAAA;AAEF,QAAA,MAAM,QAAQ,GAAI,CAAC,SAAS,MAAM,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,EAAE,QAAQ,CAAC,KAAG,cAAc,CAAC,CAAC,CAIvG,CAAA;AAEF,OAAO,EACL,MAAM,EACN,MAAM,EACN,OAAO,EACP,QAAQ,EACR,UAAU,EACV,GAAG,EACH,OAAO,EACP,EAAE,EACF,KAAK,EACL,MAAM,EACN,MAAM,EACN,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,QAAQ,GACT,CAAA"}
package/dist/schema.js CHANGED
@@ -1,15 +1,18 @@
1
1
  const number = (options) => ({
2
2
  type: 'number',
3
+ default: options?.default,
3
4
  typeName: options?.typeName,
4
5
  typeComment: options?.typeComment,
5
6
  });
6
7
  const string = (options) => ({
7
8
  type: 'string',
9
+ default: options?.default,
8
10
  typeName: options?.typeName,
9
11
  typeComment: options?.typeComment,
10
12
  });
11
13
  const boolean = (options) => ({
12
14
  type: 'boolean',
15
+ default: options?.default,
13
16
  typeName: options?.typeName,
14
17
  typeComment: options?.typeComment,
15
18
  });
@@ -28,6 +31,16 @@ const any = (options) => ({
28
31
  typeName: options?.typeName,
29
32
  typeComment: options?.typeComment,
30
33
  });
34
+ const unknown = (options) => ({
35
+ type: 'unknown',
36
+ typeName: options?.typeName,
37
+ typeComment: options?.typeComment,
38
+ });
39
+ const fn = (options) => ({
40
+ type: 'function',
41
+ typeName: options?.typeName,
42
+ typeComment: options?.typeComment,
43
+ });
31
44
  const array = (items, options) => ({
32
45
  type: 'array',
33
46
  items,
@@ -47,12 +60,24 @@ const object = (properties, options) => ({
47
60
  typeName: options?.typeName,
48
61
  typeComment: options?.typeComment,
49
62
  });
63
+ /**
64
+ * The conditional return type mirrors {@link intersection}: lightweight input constraint,
65
+ * precise `UnionSchema<Schemas>` output without re-triggering eager evaluation.
66
+ */
50
67
  const union = (schemas, options) => ({
51
68
  type: 'union',
52
69
  schemas,
53
70
  typeName: options?.typeName,
54
71
  typeComment: options?.typeComment,
55
72
  });
73
+ /**
74
+ * The conditional return type is what unlocks circular `intersection([... lazy(() => self) ...])`.
75
+ *
76
+ * The input constraint is the lightweight `IntersectionMember` (discriminant-only) so the call-site
77
+ * constraint check does not force TypeScript to eagerly evaluate each tuple element. The conditional
78
+ * `Schemas extends readonly Schema[]` is always true in practice (every passed value is a real schema)
79
+ * and lets us produce a precise `IntersectionSchema<Schemas>` without re-introducing the heavy check.
80
+ */
56
81
  const intersection = (schemas, options) => ({
57
82
  type: 'intersection',
58
83
  schemas,
@@ -78,4 +103,4 @@ const evaluate = (expression, schema) => ({
78
103
  expression,
79
104
  schema,
80
105
  });
81
- export { number, string, boolean, nullable, notDefined, any, array, record, object, union, intersection, optional, literal, lazy, evaluate, };
106
+ export { number, string, boolean, nullable, notDefined, any, unknown, fn, array, record, object, union, intersection, optional, literal, lazy, evaluate, };
package/dist/typegen.d.ts CHANGED
@@ -11,6 +11,12 @@ export type GenerateTypesOptions = {
11
11
  * trailing root type) in `export namespace Name { ... }` so consumers reference `Name.SomeType`.
12
12
  */
13
13
  namespace?: string;
14
+ /**
15
+ * When set to a valid TypeScript identifier, the root schema is emitted as
16
+ * `export type <typeName> = …` instead of an anonymous inline type.
17
+ * Overrides any `typeName` already present on the schema itself.
18
+ */
19
+ typeName?: string;
14
20
  };
15
21
  /**
16
22
  * Returns TypeScript for the schema: named `typeName` nodes become `export type` aliases (once each),
@@ -1 +1 @@
1
- {"version":3,"file":"typegen.d.ts","sourceRoot":"","sources":["../src/typegen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAItC,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAcD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,EAAE,UAAU,oBAAoB,KAAG,MAuB9E,CAAA"}
1
+ {"version":3,"file":"typegen.d.ts","sourceRoot":"","sources":["../src/typegen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAItC,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAcD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,EAAE,UAAU,oBAAoB,KAAG,MA0C9E,CAAA"}
package/dist/typegen.js CHANGED
@@ -10,19 +10,35 @@ const DEFAULT_MAX_DEPTH = 10;
10
10
  */
11
11
  export const generateTypes = (schema, options) => {
12
12
  const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
13
+ const rootTypeName = options?.typeName && isValidTypeScriptIdentifier(options.typeName) ? options.typeName : undefined;
13
14
  const ctx = {
14
15
  definitions: new Map(),
15
16
  declarations: [],
16
17
  inProgress: new Set(),
17
18
  };
18
- const root = emitSchema(schema, maxDepth, ctx, '');
19
+ // Override the schema's own typeName so the root is emitted under the
20
+ // caller-specified name. This also handles schemas that already carry a
21
+ // typeName — the option wins.
22
+ let effectiveSchema = schema;
23
+ if (rootTypeName) {
24
+ effectiveSchema = { ...schema, typeName: rootTypeName };
25
+ }
26
+ const root = emitSchema(effectiveSchema, maxDepth, ctx, '');
27
+ // When a root typeName is provided and the schema did not already emit it as
28
+ // a named declaration (e.g. intersection / union schemas have no typeName
29
+ // slot), register it now so the output contains an explicit
30
+ // `export type <typeName> = …` instead of a bare anonymous type.
31
+ if (rootTypeName && root !== rootTypeName) {
32
+ ctx.definitions.set(rootTypeName, root);
33
+ ctx.declarations.push({ name: rootTypeName, body: root });
34
+ }
19
35
  if (ctx.declarations.length === 0) {
20
36
  return root;
21
37
  }
22
38
  const declStrings = ctx.declarations.map(formatNamedDeclaration);
23
39
  const body = declStrings.join('\n\n');
24
40
  const lastDeclared = ctx.declarations.at(-1)?.name;
25
- let content = lastDeclared === root ? body : `${body}\n\n${root}`;
41
+ let content = lastDeclared === root || lastDeclared === rootTypeName ? body : `${body}\n\n${root}`;
26
42
  const ns = options?.namespace;
27
43
  if (ns && isValidTypeScriptIdentifier(ns)) {
28
44
  content = wrapDeclarationsInNamespace(ns, content);
@@ -135,6 +151,10 @@ const structuralEmit = (schema, depth, ctx, braceIndent) => {
135
151
  return 'undefined';
136
152
  case 'any':
137
153
  return 'any';
154
+ case 'unknown':
155
+ return 'unknown';
156
+ case 'function':
157
+ return '(...args: unknown[]) => unknown';
138
158
  case 'array': {
139
159
  const item = emitSchema(schema.items, next, ctx, braceIndent);
140
160
  return needsArrayItemParen(item) ? `(${item})[]` : `${item}[]`;
@@ -195,14 +215,30 @@ const literalToTs = (value) => {
195
215
  return JSON.stringify(value);
196
216
  };
197
217
  const needsArrayItemParen = (t) => {
198
- if (t === 'number' || t === 'string' || t === 'boolean' || t === 'null' || t === 'undefined' || t === 'any') {
218
+ if (t === 'number' ||
219
+ t === 'string' ||
220
+ t === 'boolean' ||
221
+ t === 'null' ||
222
+ t === 'undefined' ||
223
+ t === 'any' ||
224
+ t === 'unknown') {
199
225
  return false;
200
226
  }
227
+ // `() => void[]` parses as `() => (void[])`, so function signatures need wrapping.
228
+ if (t.includes(' => ')) {
229
+ return true;
230
+ }
201
231
  // Union: `A | B[]` is `A | (B[])`; intersection: `A & B[]` is `A & (B[])`. Wrap the whole item type.
202
232
  return t.includes(' | ') || t.includes(' & ');
203
233
  };
204
234
  const wrapUnionMember = (t) => {
205
- if (t === 'number' || t === 'string' || t === 'boolean' || t === 'null' || t === 'undefined' || t === 'any') {
235
+ if (t === 'number' ||
236
+ t === 'string' ||
237
+ t === 'boolean' ||
238
+ t === 'null' ||
239
+ t === 'undefined' ||
240
+ t === 'any' ||
241
+ t === 'unknown') {
206
242
  return t;
207
243
  }
208
244
  if (/^(?:-?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?|-?\d+n|"(?:[^"\\]|\\.)*"|true|false)$/.test(t)) {
package/dist/types.d.ts CHANGED
@@ -1,5 +1,21 @@
1
- import type { AnySchema, ArraySchema, BooleanSchema, EvaluateSchema, IntersectionSchema, LazySchema, LiteralSchema, NotDefinedSchema, NullableSchema, NumberSchema, ObjectSchema, OptionalSchema, RecordSchema, Schema, StringSchema, UnionSchema } from './schema.js';
1
+ import type { AnySchema, ArraySchema, BooleanSchema, EvaluateSchema, FunctionSchema, IntersectionSchema, LazySchema, LiteralSchema, NotDefinedSchema, NullableSchema, NumberSchema, ObjectSchema, OptionalSchema, RecordSchema, Schema, StringSchema, UnionSchema, UnknownSchema } from './schema.js';
2
2
  export type Static<T> = _Static<T, 10>;
3
+ /**
4
+ * Indirection through a named generic alias for `LazySchema`.
5
+ *
6
+ * TypeScript caches and lazily expands named generic type aliases, so referencing
7
+ * `LazyStatic<F>` instead of inlining `_Static<ReturnType<F>>` lets the resolved
8
+ * type be re-entered for circular schemas (`lazy(() => self)`) without hitting the
9
+ * depth limit. Accessing properties on the resulting type expands one level at a
10
+ * time on demand, instead of materialising the whole structure up front.
11
+ */
12
+ type LazyStatic<F extends () => Schema> = F extends () => infer S ? (S extends Schema ? _Static<S, 10> : never) : never;
13
+ /**
14
+ * Folds union member schemas into a union of their static types.
15
+ * Uses `Schema` for tuple positions (not a narrower alias) so `infer First extends …` does not
16
+ * reject valid tuple elements and collapse to `never`.
17
+ */
18
+ type UnionObjectStatics<Schemas extends readonly Schema[], Depth extends number> = Schemas extends readonly [] ? never : Schemas extends readonly [infer First extends Schema, ...infer Rest extends readonly Schema[]] ? _Static<First, Depth> | UnionObjectStatics<Rest, Depth> : _Static<Schemas[number], Depth>;
3
19
  /**
4
20
  * Folds intersection member schemas into an intersection of their static types.
5
21
  * Uses `Schema` for tuple positions (not a narrower alias) so `infer First extends …` does not
@@ -22,7 +38,7 @@ type ObjectStatics<Properties, Depth extends number> = [keyof Properties] extend
22
38
  } & {
23
39
  [K in OptionalPropertyKeys<Properties>]?: _Static<OptionalSchemaInner<Properties[K]>, Prev<Depth>>;
24
40
  };
25
- type _Static<T, Depth extends number = 10> = Depth extends 0 ? any : T extends LiteralSchema<infer Value> ? Value : T extends NumberSchema ? number : T extends StringSchema ? string : T extends BooleanSchema ? boolean : T extends NullableSchema ? null : T extends NotDefinedSchema ? undefined : T extends AnySchema ? any : T extends ArraySchema<infer Item> ? Array<_Static<Item, Prev<Depth>>> : T extends RecordSchema<infer Key, infer Value> ? Record<_Static<Key, Prev<Depth>> & PropertyKey, _Static<Value, Prev<Depth>>> : T extends ObjectSchema<infer Properties> ? ObjectStatics<Properties, Depth> : T extends OptionalSchema<infer S> ? _Static<S, Prev<Depth>> | undefined : T extends IntersectionSchema<infer Schemas> ? IntersectObjectStatics<Schemas, Prev<Depth>> : T extends UnionSchema<infer Schemas> ? _Static<Schemas[number], Prev<Depth>> : T extends EvaluateSchema<infer S> ? _Static<S, Prev<Depth>> : T extends LazySchema<infer S> ? _Static<ReturnType<S>, Prev<Depth>> : never;
41
+ type _Static<T, Depth extends number = 10> = Depth extends 0 ? any : T extends LiteralSchema<infer Value> ? Value : T extends NumberSchema ? number : T extends StringSchema ? string : T extends BooleanSchema ? boolean : T extends NullableSchema ? null : T extends NotDefinedSchema ? undefined : T extends AnySchema ? any : T extends UnknownSchema ? unknown : T extends FunctionSchema<infer F> ? F : T extends ArraySchema<infer Item> ? Array<_Static<Item, Prev<Depth>>> : T extends RecordSchema<infer Key, infer Value> ? Record<_Static<Key, Prev<Depth>> & PropertyKey, _Static<Value, Prev<Depth>>> : T extends ObjectSchema<infer Properties> ? ObjectStatics<Properties, Depth> : T extends OptionalSchema<infer S> ? _Static<S, Prev<Depth>> | undefined : T extends IntersectionSchema<infer Schemas> ? IntersectObjectStatics<Schemas, Prev<Depth>> : T extends UnionSchema<infer Schemas> ? UnionObjectStatics<Schemas, Prev<Depth>> : T extends EvaluateSchema<infer S> ? _Static<S, Prev<Depth>> : T extends LazySchema<infer S> ? LazyStatic<S> : never;
26
42
  type Prev<T extends number> = T extends 10 ? 9 : T extends 9 ? 8 : T extends 8 ? 7 : T extends 7 ? 6 : T extends 6 ? 5 : T extends 5 ? 4 : T extends 4 ? 3 : T extends 3 ? 2 : T extends 2 ? 1 : T extends 1 ? 0 : 0;
27
43
  export {};
28
44
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,8 @@ import type { Schema } from './schema.js';
5
5
  * The schema describes the expected structure/type of data.
6
6
  * Supported schema types include:
7
7
  * - 'any': Accepts any value.
8
+ * - 'unknown': Accepts any value (generates `unknown` instead of `any` in types).
9
+ * - 'function': Only functions are valid (signature is not checked at runtime).
8
10
  * - 'number': Only numbers are valid.
9
11
  * - 'string': Only strings are valid.
10
12
  * - 'boolean': Only booleans are valid.
@@ -29,8 +31,12 @@ import type { Schema } from './schema.js';
29
31
  * validate(schema, { id: 1, name: 2 }) // false
30
32
  * ```
31
33
  *
34
+ * The optional `cache` argument tracks visited object–schema pairs to stop
35
+ * infinite recursion on cyclic value graphs (for example a node whose child
36
+ * points back at itself paired with a `lazy` schema). Callers normally omit it.
37
+ *
32
38
  * If schema is `undefined`, validation fails.
33
39
  * Returns true if the value matches the schema, false otherwise.
34
40
  */
35
- export declare const validate: (schema: Schema | undefined, value: unknown) => boolean;
41
+ export declare const validate: (schema: Schema | undefined, value: unknown, cache?: WeakMap<object, Set<Schema>>) => boolean;
36
42
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,QAAQ,GAAI,QAAQ,MAAM,GAAG,SAAS,EAAE,OAAO,OAAO,KAAG,OAqErE,CAAA"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAsHtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,QAAQ,GACnB,QAAQ,MAAM,GAAG,SAAS,EAC1B,OAAO,OAAO,EACd,QAAO,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAiB,KAClD,OAA8C,CAAA"}
package/dist/validate.js CHANGED
@@ -1,10 +1,122 @@
1
1
  import { isObject } from './helpers/is-object.js';
2
+ /**
3
+ * Internal validation implementation. Threads a `cache` of `(object, schema)`
4
+ * pairs that are *currently in flight on the call stack* so cyclic graphs
5
+ * terminate instead of recursing forever. Re-entering a pair that is already
6
+ * being validated higher up the stack short-circuits to `true`: any concrete
7
+ * mismatch would surface at that enclosing call rather than via the cycle.
8
+ *
9
+ * Crucially the marker is removed before this call returns, so the cache only
10
+ * ever describes the live call stack and not "ever visited in this run".
11
+ * Without that removal a failed branch (for example the first member of a
12
+ * `union`) would leave stale entries that later sibling branches sharing the
13
+ * same schema reference would mistake for a successful cycle short-circuit.
14
+ */
15
+ const validateInner = (schema, value, cache) => {
16
+ if (!schema) {
17
+ return false;
18
+ }
19
+ // Short-circuit on cycles: this exact `(value, schema)` pair is already
20
+ // being validated higher up the call stack.
21
+ const trackable = isObject(value) || Array.isArray(value);
22
+ if (trackable && cache.get(value)?.has(schema)) {
23
+ return true;
24
+ }
25
+ // Mark this `(value, schema)` pair as in-progress for the duration of this
26
+ // call. Plain objects and arrays can form cycles; primitives and other
27
+ // non-plain objects do not need (or get) an entry.
28
+ if (trackable) {
29
+ const schemas = cache.get(value) ?? new Set();
30
+ schemas.add(schema);
31
+ cache.set(value, schemas);
32
+ }
33
+ try {
34
+ if (schema.type === 'any' || schema.type === 'unknown') {
35
+ return true;
36
+ }
37
+ if (schema.type === 'function') {
38
+ return typeof value === 'function';
39
+ }
40
+ if (schema.type === 'number') {
41
+ return typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value);
42
+ }
43
+ if (schema.type === 'string') {
44
+ return typeof value === 'string';
45
+ }
46
+ if (schema.type === 'boolean') {
47
+ return typeof value === 'boolean';
48
+ }
49
+ if (schema.type === 'nullable') {
50
+ return value === null;
51
+ }
52
+ if (schema.type === 'notDefined') {
53
+ return value === undefined;
54
+ }
55
+ if (schema.type === 'array') {
56
+ return Array.isArray(value) && value.every((item) => validateInner(schema.items, item, cache));
57
+ }
58
+ if (schema.type === 'record') {
59
+ if (!isObject(value)) {
60
+ return false;
61
+ }
62
+ const keys = Object.keys(value);
63
+ return keys.every((key) => validateInner(schema.key, key, cache) && validateInner(schema.value, value[key], cache));
64
+ }
65
+ if (schema.type === 'object') {
66
+ if (!isObject(value)) {
67
+ return false;
68
+ }
69
+ const schemaKeys = Object.keys(schema.properties);
70
+ return schemaKeys.every((key) => validateInner(schema.properties[key], value[key], cache));
71
+ }
72
+ if (schema.type === 'optional') {
73
+ return value === undefined || validateInner(schema.schema, value, cache);
74
+ }
75
+ if (schema.type === 'union') {
76
+ return schema.schemas.some((branch) => validateInner(branch, value, cache));
77
+ }
78
+ if (schema.type === 'intersection') {
79
+ if (schema.schemas.length === 0) {
80
+ // Vacuous: no constraints (matches `Array.prototype.every` on an empty list).
81
+ return true;
82
+ }
83
+ if (!isObject(value)) {
84
+ return false;
85
+ }
86
+ return schema.schemas.every((subSchema) => validateInner(subSchema, value, cache));
87
+ }
88
+ if (schema.type === 'literal') {
89
+ return value === schema.value;
90
+ }
91
+ if (schema.type === 'lazy') {
92
+ return validateInner(schema.schema(), value, cache);
93
+ }
94
+ if (schema.type === 'evaluate') {
95
+ return validateInner(schema.schema, schema.expression(value), cache);
96
+ }
97
+ // We need to assert here that schema has the type never so we know we handle all cases
98
+ const _exhaustive = schema;
99
+ console.warn('Unknown schema type:', _exhaustive);
100
+ return false;
101
+ }
102
+ finally {
103
+ // Always clear the in-progress marker, even when a sub-call throws. This
104
+ // keeps the cache scoped to the live call stack so sibling branches (for
105
+ // example other `union` members or earlier-failed `intersection` members)
106
+ // re-validate the shared schema instead of inheriting a stale `true`.
107
+ if (trackable) {
108
+ cache.get(value)?.delete(schema);
109
+ }
110
+ }
111
+ };
2
112
  /**
3
113
  * Validates that a given value matches the specified schema.
4
114
  *
5
115
  * The schema describes the expected structure/type of data.
6
116
  * Supported schema types include:
7
117
  * - 'any': Accepts any value.
118
+ * - 'unknown': Accepts any value (generates `unknown` instead of `any` in types).
119
+ * - 'function': Only functions are valid (signature is not checked at runtime).
8
120
  * - 'number': Only numbers are valid.
9
121
  * - 'string': Only strings are valid.
10
122
  * - 'boolean': Only booleans are valid.
@@ -29,75 +141,11 @@ import { isObject } from './helpers/is-object.js';
29
141
  * validate(schema, { id: 1, name: 2 }) // false
30
142
  * ```
31
143
  *
144
+ * The optional `cache` argument tracks visited object–schema pairs to stop
145
+ * infinite recursion on cyclic value graphs (for example a node whose child
146
+ * points back at itself paired with a `lazy` schema). Callers normally omit it.
147
+ *
32
148
  * If schema is `undefined`, validation fails.
33
149
  * Returns true if the value matches the schema, false otherwise.
34
150
  */
35
- export const validate = (schema, value) => {
36
- if (!schema) {
37
- return false;
38
- }
39
- if (schema.type === 'any') {
40
- return true;
41
- }
42
- if (schema.type === 'number') {
43
- return typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value);
44
- }
45
- if (schema.type === 'string') {
46
- return typeof value === 'string';
47
- }
48
- if (schema.type === 'boolean') {
49
- return typeof value === 'boolean';
50
- }
51
- if (schema.type === 'nullable') {
52
- return value === null;
53
- }
54
- if (schema.type === 'notDefined') {
55
- return value === undefined;
56
- }
57
- if (schema.type === 'array') {
58
- return Array.isArray(value) && value.every((item) => validate(schema.items, item));
59
- }
60
- if (schema.type === 'record') {
61
- if (!isObject(value)) {
62
- return false;
63
- }
64
- const keys = Object.keys(value);
65
- return keys.every((key) => validate(schema.key, key) && validate(schema.value, value[key]));
66
- }
67
- if (schema.type === 'object') {
68
- if (!isObject(value)) {
69
- return false;
70
- }
71
- const schemaKeys = Object.keys(schema.properties);
72
- return schemaKeys.every((key) => validate(schema.properties[key], value[key]));
73
- }
74
- if (schema.type === 'optional') {
75
- return value === undefined || validate(schema.schema, value);
76
- }
77
- if (schema.type === 'union') {
78
- return schema.schemas.some((schema) => validate(schema, value));
79
- }
80
- if (schema.type === 'intersection') {
81
- if (schema.schemas.length === 0) {
82
- // Vacuous: no constraints (matches `Array.prototype.every` on an empty list).
83
- return true;
84
- }
85
- if (!isObject(value)) {
86
- return false;
87
- }
88
- return schema.schemas.every((subSchema) => validate(subSchema, value));
89
- }
90
- if (schema.type === 'literal') {
91
- return value === schema.value;
92
- }
93
- if (schema.type === 'lazy') {
94
- return validate(schema.schema(), value);
95
- }
96
- if (schema.type === 'evaluate') {
97
- return validate(schema.schema, schema.expression(value));
98
- }
99
- // We need to assert here that schema has the type never so we know we handle all cases
100
- const _exhaustive = schema;
101
- console.warn('Unknown schema type:', _exhaustive);
102
- return false;
103
- };
151
+ export const validate = (schema, value, cache = new WeakMap()) => validateInner(schema, value, cache);
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "coerce",
16
16
  "scalar"
17
17
  ],
18
- "version": "0.3.2",
18
+ "version": "0.6.0",
19
19
  "engines": {
20
20
  "node": ">=20"
21
21
  },