@oh-my-pi/omptype 17.2.6 → 17.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/type.ts CHANGED
@@ -10,12 +10,25 @@
10
10
  * A schema is a callable: `schema(data)` returns the (possibly morphed)
11
11
  * output, or an `OmpErrors` on failure (`result instanceof type.errors`).
12
12
  */
13
- import { compile } from "./compile";
14
- import { OmpErrors, OmpTypeError, TraversalError } from "./errors";
15
- import type { InferDef, InferObjectLiteral, InferString } from "./infer";
13
+ import { compile, compileAllows } from "./compile";
14
+ import { type ErrorConfig, OmpErrors, OmpTypeError, TraversalError } from "./errors";
15
+ import type { InferDef, InferDefIn, InferObjectLiteral, InferString } from "./infer";
16
16
  import { walk } from "./interp";
17
- import { type Def, embed, hasMorph, type IR, IR_BRAND, parseDef } from "./ir";
18
- import { irToJsonSchema } from "./json-schema";
17
+ import {
18
+ type AliasResolver,
19
+ type Constructor,
20
+ type Def,
21
+ embed,
22
+ expectedOf,
23
+ hasMorph,
24
+ type IR,
25
+ IR_BRAND,
26
+ keyOf,
27
+ type PropIR,
28
+ parseDef,
29
+ } from "./ir";
30
+ import { irToJsonSchema, type JsonSchemaOptions } from "./json-schema";
31
+ import { keywordIR, patternIR } from "./keywords";
19
32
 
20
33
  /** Context passed to `.narrow()` / `.pipe()` callbacks. */
21
34
  export interface NarrowContext {
@@ -25,20 +38,43 @@ export interface NarrowContext {
25
38
  reject(problem: string): false;
26
39
  }
27
40
 
28
- /** Options accepted by `Type.toJsonSchema` (ArkType-compatible; emission is always draft 2020-12). */
29
- export interface ToJsonSchemaOptions {
30
- target?: string;
31
- dialect?: string;
32
- fallback?: (ctx: { base: Record<string, unknown> }) => unknown;
41
+ /** Schema metadata and validation-message overrides accepted by `.configure()`. */
42
+ export interface SchemaConfig extends ErrorConfig {
43
+ readonly description?: string;
33
44
  }
34
45
 
35
- interface SchemaInference<out t> {
46
+ /** Options accepted by `Type.toJsonSchema`. */
47
+ export interface ToJsonSchemaOptions extends JsonSchemaOptions {}
48
+
49
+ declare const brand: unique symbol;
50
+
51
+ /** Inference-only nominal brand attached by `.brand(name)`. */
52
+ export type Brand<t, name extends string> = t & { readonly [brand]: name };
53
+
54
+ interface SchemaInference<out t, i = t> {
36
55
  readonly [IR_BRAND]: true;
37
56
  readonly infer: t;
57
+ readonly inferIn: i;
58
+ }
59
+
60
+ /** Property descriptor exposed by object schemas and consumed by `.map()`. */
61
+ export interface TypeProperty {
62
+ readonly kind: "required" | "optional";
63
+ readonly key: PropertyKey;
64
+ readonly value: FluentType<unknown>;
65
+ readonly default?: unknown;
66
+ readonly meta: Readonly<Record<string, unknown>>;
67
+ }
68
+
69
+ /** Structural node returned by `.select()`. */
70
+ export interface SelectedNode {
71
+ readonly kind: string;
72
+ readonly node: IR;
73
+ readonly unit?: unknown;
38
74
  }
39
75
 
40
76
  /** A compiled schema: callable validator plus composition methods. */
41
- export interface Type<out t = unknown> {
77
+ export interface Type<out t = unknown, i = t> {
42
78
  (data: unknown): t | OmpErrors;
43
79
  readonly [IR_BRAND]: true;
44
80
  /** Structural IR (base type; runtime steps live in `steps`). */
@@ -54,37 +90,120 @@ export interface Type<out t = unknown> {
54
90
  /** Inference-only output type (no runtime value). */
55
91
  readonly infer: t;
56
92
  /** Inference-only input type (no runtime value). */
57
- readonly inferIn: t;
93
+ readonly inferIn: i;
58
94
 
59
95
  /** Structural + narrow check without running pipes. */
60
- allows(data: unknown): data is t;
96
+ allows(data: unknown): data is i;
61
97
  /** Validate and return output, throwing `TraversalError` on failure. */
62
98
  assert(data: unknown): t;
63
- /** JSON Schema (draft 2020-12) for this schema's structural base. */
99
+ /** Validate a statically typed input and return its output. */
100
+ from(data: i): t;
101
+ /** JSON Schema for this schema's structural base. */
64
102
  toJsonSchema(options?: ToJsonSchemaOptions): Record<string, unknown>;
65
103
  }
66
104
 
67
- /** Schema returned by omptype builders, with precise object-literal composition inference. */
68
- export interface FluentType<t = unknown> extends Type<t> {
69
- describe(description: string): FluentType<t>;
70
- default(value: t | (() => t)): FluentType<t>;
71
- or<r>(def: SchemaInference<r>): FluentType<t | r>;
72
- or<const def extends string>(def: def): FluentType<t | InferString<def>>;
73
- or<const def extends Record<string, unknown>>(def: def): FluentType<t | InferObjectLiteral<def>>;
105
+ type MergeTypes<left, right> = left extends object
106
+ ? right extends object
107
+ ? Omit<left, keyof right> & right
108
+ : right
109
+ : right;
110
+
111
+ interface FluentMethods<t, i> {
112
+ describe(description: string): FluentType<t, i>;
113
+ configure(config: SchemaConfig): FluentType<t, i>;
114
+ default(value: t | (() => t)): FluentType<t, i>;
115
+ optional(): readonly [SchemaInference<t, i>, "?"];
116
+ or<r, ri>(def: SchemaInference<r, ri>): FluentType<t | r, i | ri>;
117
+ or<const def extends string>(def: def): FluentType<t | InferString<def>, i | InferString<def>>;
118
+ or<const def extends Record<string, unknown>>(
119
+ def: def,
120
+ ): FluentType<t | InferObjectLiteral<def>, i | InferObjectLiteral<def>>;
74
121
  or(def: Def): FluentType<unknown>;
75
- and<r>(def: SchemaInference<r>): FluentType<t & r>;
76
- and<const def extends Record<string, unknown>>(def: def): FluentType<t & InferObjectLiteral<def>>;
122
+ and<r, ri>(def: SchemaInference<r, ri>): FluentType<t & r, i & ri>;
123
+ and<const def extends Record<string, unknown>>(
124
+ def: def,
125
+ ): FluentType<t & InferObjectLiteral<def>, i & InferObjectLiteral<def>>;
77
126
  and(def: Def): FluentType<unknown>;
78
- array(): FluentType<t[]>;
79
- atLeastLength(bound: number): FluentType<t>;
80
- atMostLength(bound: number): FluentType<t>;
81
- atLeast(bound: number): FluentType<t>;
82
- atMost(bound: number): FluentType<t>;
83
- pipe<r>(fn: (data: t, ctx: NarrowContext) => r): FluentType<Exclude<r, OmpErrors>>;
84
- narrow<narrowed extends t>(fn: (data: t, ctx: NarrowContext) => data is narrowed): FluentType<narrowed>;
85
- narrow(fn: (data: t, ctx: NarrowContext) => boolean): FluentType<t>;
127
+ equals(def: Def): boolean;
128
+ ifEquals(def: Def): FluentType<t, i> | undefined;
129
+ extends(def: Def): boolean;
130
+ overlaps(def: Def): boolean;
131
+ distribute<r>(mapper: (branch: FluentType<unknown>) => SchemaInference<r>): FluentType<r>;
132
+ select(kind: string): readonly SelectedNode[];
133
+ array(): FluentType<t[], i[]>;
134
+ atLeastLength(bound: number): FluentType<t, i>;
135
+ atMostLength(bound: number): FluentType<t, i>;
136
+ moreThanLength(bound: number): FluentType<t, i>;
137
+ lessThanLength(bound: number): FluentType<t, i>;
138
+ exactlyLength(bound: number): FluentType<t, i>;
139
+ atLeast(bound: number): FluentType<t, i>;
140
+ atMost(bound: number): FluentType<t, i>;
141
+ moreThan(bound: number): FluentType<t, i>;
142
+ lessThan(bound: number): FluentType<t, i>;
143
+ divisibleBy(divisor: number): FluentType<t, i>;
144
+ positive(): FluentType<t, i>;
145
+ negative(): FluentType<t, i>;
146
+ nonNegative(): FluentType<t, i>;
147
+ nonPositive(): FluentType<t, i>;
148
+ matching(pattern: RegExp): FluentType<t, i>;
149
+ atOrAfter(bound: Date): FluentType<t, i>;
150
+ atOrBefore(bound: Date): FluentType<t, i>;
151
+ laterThan(bound: Date): FluentType<t, i>;
152
+ earlierThan(bound: Date): FluentType<t, i>;
153
+ pipe<r>(fn: (data: t, ctx: NarrowContext) => r): FluentType<Exclude<r, OmpErrors>, i>;
154
+ to<const def>(def: def): FluentType<InferDef<def>, i>;
155
+ filter<narrowed extends i>(fn: (data: i, ctx: NarrowContext) => data is narrowed): FluentType<t, narrowed>;
156
+ filter(fn: (data: i, ctx: NarrowContext) => boolean): FluentType<t, i>;
157
+ narrow<narrowed extends t>(fn: (data: t, ctx: NarrowContext) => data is narrowed): FluentType<narrowed, i>;
158
+ narrow(fn: (data: t, ctx: NarrowContext) => boolean): FluentType<t, i>;
159
+ brand<const name extends string>(name: name): FluentType<Brand<t, name>, i>;
160
+ as<castTo>(): FluentType<castTo, i>;
161
+ extract<r, ri>(def: SchemaInference<r, ri>): FluentType<Extract<t, r>, Extract<i, ri>>;
162
+ extract<const def extends string>(def: def): FluentType<Extract<t, InferString<def>>, Extract<i, InferString<def>>>;
163
+ extract(def: Def): FluentType<unknown>;
164
+ exclude<r, ri>(def: SchemaInference<r, ri>): FluentType<Exclude<t, r>, Exclude<i, ri>>;
165
+ exclude<const def extends string>(def: def): FluentType<Exclude<t, InferString<def>>, Exclude<i, InferString<def>>>;
166
+ exclude(def: Def): FluentType<unknown>;
167
+ onUndeclaredKey(behavior: "ignore" | "reject" | "delete"): FluentType<t, i>;
168
+ onDeepUndeclaredKey(behavior: "ignore" | "reject" | "delete"): FluentType<t, i>;
169
+ }
170
+
171
+ type InputObject<i> = i extends object ? i : object;
172
+
173
+ interface ObjectMethods<t extends object, i> {
174
+ readonly props: readonly TypeProperty[];
175
+ map(
176
+ mapper: (property: TypeProperty) => TypeProperty | readonly TypeProperty[],
177
+ ): FluentType<Record<PropertyKey, unknown>>;
178
+ keyof(): FluentType<Extract<keyof t, PropertyKey>, Extract<keyof InputObject<i>, PropertyKey>>;
179
+ get<key extends keyof t>(
180
+ key: key,
181
+ ): FluentType<t[key], key extends keyof InputObject<i> ? InputObject<i>[key] : unknown>;
182
+ pick<const keys extends readonly (keyof t)[]>(
183
+ ...keys: keys
184
+ ): FluentType<Pick<t, keys[number]>, Pick<InputObject<i>, Extract<keys[number], keyof InputObject<i>>>>;
185
+ omit<const keys extends readonly (keyof t)[]>(
186
+ ...keys: keys
187
+ ): FluentType<Omit<t, keys[number]>, Omit<InputObject<i>, Extract<keys[number], keyof InputObject<i>>>>;
188
+ partial(): FluentType<Partial<t>, Partial<InputObject<i>>>;
189
+ required(): FluentType<Required<t>, Required<InputObject<i>>>;
190
+ merge<r, ri>(def: SchemaInference<r, ri>): FluentType<MergeTypes<t, r>, MergeTypes<i, ri>>;
191
+ merge<const def extends Record<string, unknown>>(
192
+ def: def,
193
+ ): FluentType<MergeTypes<t, InferObjectLiteral<def>>, MergeTypes<i, InferObjectLiteral<def>>>;
194
+ merge(def: Def): FluentType<unknown>;
86
195
  }
87
196
 
197
+ type ObjectMethodsFor<t, i> = [t] extends [never]
198
+ ? unknown
199
+ : [t] extends [readonly unknown[]]
200
+ ? unknown
201
+ : [t] extends [object]
202
+ ? ObjectMethods<t & object, i>
203
+ : unknown;
204
+ /** Callable schema with fluent methods specialized to its output and input. */
205
+ export type FluentType<t = unknown, i = t> = Type<t, i> & FluentMethods<t, i> & ObjectMethodsFor<t, i>;
206
+
88
207
  /** Runtime constructor-like value used by ArkType-compatible `instanceof Type` checks. */
89
208
  export const Type = Object.defineProperty(function Type(): void {}, Symbol.hasInstance, {
90
209
  value: (value: unknown): boolean =>
@@ -92,14 +211,15 @@ export const Type = Object.defineProperty(function Type(): void {}, Symbol.hasIn
92
211
  });
93
212
 
94
213
  interface Step {
95
- kind: "pipe" | "narrow";
96
- fn: (data: never, ctx: NarrowContext) => unknown;
214
+ kind: "pipe" | "narrow" | "filter";
215
+ fn: (data: unknown, ctx: NarrowContext) => unknown;
97
216
  }
98
217
 
99
218
  interface TypeMeta {
100
219
  description?: string;
101
220
  defaultValue?: unknown;
102
221
  hasDefault?: boolean;
222
+ errorConfig?: ErrorConfig;
103
223
  }
104
224
 
105
225
  class Ctx implements NarrowContext {
@@ -116,12 +236,385 @@ class Ctx implements NarrowContext {
116
236
  }
117
237
  }
118
238
 
239
+ type Validator = (data: unknown) => unknown;
240
+ type Allows = (data: unknown) => boolean;
241
+
242
+ const kBase = Symbol("omptype.base");
243
+ const kSteps = Symbol("omptype.steps");
244
+
245
+ const EMPTY_STEPS: Step[] = [];
246
+ const EMPTY_META: TypeMeta = {};
247
+
248
+ interface InternalType {
249
+ (data: unknown): unknown;
250
+ [IR_BRAND]: true;
251
+ [kBase]: Validator;
252
+ [kSteps]: Step[];
253
+ allows: Allows;
254
+ ir: IR;
255
+ hasSteps: boolean;
256
+ hasDefault: boolean;
257
+ defaultValue?: unknown;
258
+ description?: string;
259
+ errorConfig?: ErrorConfig;
260
+ run: Validator;
261
+ }
262
+
119
263
  /** Calls before the JIT compiles a schema (first two run the interpreter). */
120
264
  const JIT_THRESHOLD = 3;
121
265
 
122
- function makeType(ir: IR, steps: Step[], meta: TypeMeta): FluentType {
266
+ function metaOf(schema: InternalType): TypeMeta {
267
+ return {
268
+ description: schema.description,
269
+ defaultValue: schema.defaultValue,
270
+ hasDefault: schema.hasDefault,
271
+ errorConfig: schema.errorConfig,
272
+ };
273
+ }
274
+
275
+ const typeMethods = {
276
+ describe(this: InternalType, description: string): InternalType {
277
+ return makeType({ ...this.ir, desc: description }, this[kSteps], { ...metaOf(this), description });
278
+ },
279
+
280
+ configure(this: InternalType, config: SchemaConfig): InternalType {
281
+ const errorConfig: ErrorConfig = {
282
+ ...this.errorConfig,
283
+ ...(config.expected === undefined ? {} : { expected: config.expected }),
284
+ ...(config.actual === undefined ? {} : { actual: config.actual }),
285
+ ...(config.problem === undefined ? {} : { problem: config.problem }),
286
+ ...(config.message === undefined ? {} : { message: config.message }),
287
+ };
288
+ const meta = {
289
+ ...metaOf(this),
290
+ errorConfig,
291
+ ...(config.description === undefined ? {} : { description: config.description }),
292
+ };
293
+ return config.description === undefined
294
+ ? makeType(this.ir, this[kSteps], meta)
295
+ : makeType({ ...this.ir, desc: config.description }, this[kSteps], meta);
296
+ },
297
+
298
+ default(this: InternalType, value: unknown): InternalType {
299
+ return makeType(this.ir, this[kSteps], { ...metaOf(this), defaultValue: value, hasDefault: true });
300
+ },
301
+
302
+ optional(this: InternalType): readonly [InternalType, "?"] {
303
+ return [this, "?"];
304
+ },
305
+
306
+ or(this: InternalType, def: Def): InternalType {
307
+ const other = parseDef(def);
308
+ const a = embed(this);
309
+ const members = [...(a.k === "union" ? a.members : [a]), ...(other.k === "union" ? other.members : [other])];
310
+ return makeType({ k: "union", members }, [], {});
311
+ },
312
+
313
+ equals(this: InternalType, def: Def): boolean {
314
+ return irEquals(embed(this), parseDef(def));
315
+ },
316
+
317
+ ifEquals(this: InternalType, def: Def): InternalType | undefined {
318
+ return irEquals(embed(this), parseDef(def)) ? this : undefined;
319
+ },
320
+
321
+ extends(this: InternalType, def: Def): boolean {
322
+ return isSubtype(embed(this), parseDef(def));
323
+ },
324
+
325
+ overlaps(this: InternalType, def: Def): boolean {
326
+ try {
327
+ intersect(embed(this), parseDef(def));
328
+ return true;
329
+ } catch (error) {
330
+ if (error instanceof OmpTypeError) return false;
331
+ throw error;
332
+ }
333
+ },
334
+
335
+ distribute(this: InternalType, mapper: (branch: BaseType) => InternalType): InternalType {
336
+ const branches = this.ir.k === "union" ? this.ir.members : [embed(this)];
337
+ const members = branches.map(branch => embed(mapper(makeType(branch, [], {}) as unknown as BaseType)));
338
+ return makeType(members.length === 1 ? members[0] : { k: "union", members }, [], {});
339
+ },
340
+
341
+ select(this: InternalType, kind: string): readonly SelectedNode[] {
342
+ return selectNodes(this.ir, kind);
343
+ },
344
+
345
+ and(this: InternalType, def: Def): InternalType {
346
+ return makeType(intersect(embed(this), parseDef(def)), [], {});
347
+ },
348
+
349
+ array(this: InternalType): InternalType {
350
+ return makeType({ k: "array", el: embed(this) }, [], {});
351
+ },
352
+
353
+ atLeastLength(this: InternalType, bound: number): InternalType {
354
+ return makeType(withLengthBound(this.ir, "min", bound), this[kSteps], metaOf(this));
355
+ },
356
+
357
+ atMostLength(this: InternalType, bound: number): InternalType {
358
+ return makeType(withLengthBound(this.ir, "max", bound), this[kSteps], metaOf(this));
359
+ },
360
+
361
+ moreThanLength(this: InternalType, bound: number): InternalType {
362
+ return makeType(withLengthBound(this.ir, "min", bound + 1), this[kSteps], metaOf(this));
363
+ },
364
+
365
+ lessThanLength(this: InternalType, bound: number): InternalType {
366
+ return makeType(withLengthBound(this.ir, "max", bound - 1), this[kSteps], metaOf(this));
367
+ },
368
+
369
+ exactlyLength(this: InternalType, bound: number): InternalType {
370
+ const bounded = withLengthBound(withLengthBound(this.ir, "min", bound), "max", bound);
371
+ return makeType(bounded, this[kSteps], metaOf(this));
372
+ },
373
+
374
+ atLeast(this: InternalType, bound: number): InternalType {
375
+ return makeType(withNumericBound(this.ir, "min", bound), this[kSteps], metaOf(this));
376
+ },
377
+
378
+ atMost(this: InternalType, bound: number): InternalType {
379
+ return makeType(withNumericBound(this.ir, "max", bound), this[kSteps], metaOf(this));
380
+ },
381
+
382
+ moreThan(this: InternalType, bound: number): InternalType {
383
+ return makeType(withNumericBound(this.ir, "min", bound, true), this[kSteps], metaOf(this));
384
+ },
385
+
386
+ lessThan(this: InternalType, bound: number): InternalType {
387
+ return makeType(withNumericBound(this.ir, "max", bound, true), this[kSteps], metaOf(this));
388
+ },
389
+
390
+ divisibleBy(this: InternalType, divisor: number): InternalType {
391
+ if (this.ir.k !== "number") throw new OmpTypeError(`cannot apply divisibility to ${this.ir.k}`);
392
+ if (!Number.isFinite(divisor) || divisor === 0) throw new OmpTypeError("divisor must be non-zero");
393
+ return makeType({ ...this.ir, divisor }, this[kSteps], metaOf(this));
394
+ },
395
+
396
+ positive(this: InternalType): InternalType {
397
+ return makeType(withNumericBound(this.ir, "min", 0, true), this[kSteps], metaOf(this));
398
+ },
399
+
400
+ negative(this: InternalType): InternalType {
401
+ return makeType(withNumericBound(this.ir, "max", 0, true), this[kSteps], metaOf(this));
402
+ },
403
+
404
+ nonNegative(this: InternalType): InternalType {
405
+ return makeType(withNumericBound(this.ir, "min", 0), this[kSteps], metaOf(this));
406
+ },
407
+
408
+ nonPositive(this: InternalType): InternalType {
409
+ return makeType(withNumericBound(this.ir, "max", 0), this[kSteps], metaOf(this));
410
+ },
411
+
412
+ matching(this: InternalType, pattern: RegExp): InternalType {
413
+ return makeType(intersect(this.ir, patternIR(pattern)), this[kSteps], metaOf(this));
414
+ },
415
+
416
+ atOrAfter(this: InternalType, bound: Date): InternalType {
417
+ return dateRefinement(this, bound, "at or after", value => value >= bound.valueOf());
418
+ },
419
+
420
+ atOrBefore(this: InternalType, bound: Date): InternalType {
421
+ return dateRefinement(this, bound, "at or before", value => value <= bound.valueOf());
422
+ },
423
+
424
+ laterThan(this: InternalType, bound: Date): InternalType {
425
+ return dateRefinement(this, bound, "later than", value => value > bound.valueOf());
426
+ },
427
+
428
+ earlierThan(this: InternalType, bound: Date): InternalType {
429
+ return dateRefinement(this, bound, "earlier than", value => value < bound.valueOf());
430
+ },
431
+
432
+ pipe(this: InternalType, fn: Step["fn"]): InternalType {
433
+ return makeType(this.ir, [...this[kSteps], { kind: "pipe", fn }], metaOf(this));
434
+ },
435
+
436
+ to(this: InternalType, def: unknown): InternalType {
437
+ const output = makeType(parseDef(def), [], {});
438
+ return makeType(
439
+ this.ir,
440
+ [
441
+ ...this[kSteps],
442
+ {
443
+ kind: "pipe",
444
+ fn: value => output(value),
445
+ },
446
+ ],
447
+ metaOf(this),
448
+ );
449
+ },
450
+
451
+ filter(this: InternalType, fn: Step["fn"]): InternalType {
452
+ return makeType(this.ir, [{ kind: "filter", fn }, ...this[kSteps]], metaOf(this));
453
+ },
454
+
455
+ narrow(this: InternalType, fn: Step["fn"]): InternalType {
456
+ return makeType(this.ir, [...this[kSteps], { kind: "narrow", fn }], metaOf(this));
457
+ },
458
+
459
+ brand(this: InternalType): InternalType {
460
+ return makeType(this.ir, this[kSteps], metaOf(this));
461
+ },
462
+
463
+ as(this: InternalType): InternalType {
464
+ return makeType(this.ir, this[kSteps], metaOf(this));
465
+ },
466
+
467
+ keyof(this: InternalType): InternalType {
468
+ return makeType(keyOf(this.ir), [], {});
469
+ },
470
+
471
+ get(this: InternalType, key: PropertyKey): InternalType {
472
+ const object = requireObject(this.ir, "get");
473
+ const prop = object.props.find(candidate => candidate.key === String(key));
474
+ if (!prop) throw new OmpTypeError(`key ${String(key)} is not declared`);
475
+ const ir: IR = prop.opt ? { k: "union", members: [prop.val, { k: "undefined" }] } : prop.val;
476
+ return makeType(ir, [], {});
477
+ },
478
+
479
+ pick(this: InternalType, ...keys: PropertyKey[]): InternalType {
480
+ const object = requireObject(this.ir, "pick");
481
+ const selected = new Set(keys.map(String));
482
+ return makeType({ ...object, props: object.props.filter(prop => selected.has(prop.key)) }, [], {});
483
+ },
484
+
485
+ omit(this: InternalType, ...keys: PropertyKey[]): InternalType {
486
+ const object = requireObject(this.ir, "omit");
487
+ const omitted = new Set(keys.map(String));
488
+ return makeType({ ...object, props: object.props.filter(prop => !omitted.has(prop.key)) }, [], {});
489
+ },
490
+
491
+ partial(this: InternalType): InternalType {
492
+ const object = requireObject(this.ir, "partial");
493
+ return makeType({ ...object, props: object.props.map(prop => ({ ...prop, opt: true })) }, [], {});
494
+ },
495
+
496
+ required(this: InternalType): InternalType {
497
+ const object = requireObject(this.ir, "required");
498
+ return makeType({ ...object, props: object.props.map(prop => ({ ...prop, opt: false })) }, [], {});
499
+ },
500
+
501
+ map(this: InternalType, mapper: (property: TypeProperty) => TypeProperty | readonly TypeProperty[]): InternalType {
502
+ const object = requireObject(this.ir, "map");
503
+ const props = object.props.flatMap(prop => {
504
+ const mapped = mapper(propertyFromIR(prop));
505
+ return (Array.isArray(mapped) ? mapped : [mapped]).map(propertyToIR);
506
+ });
507
+ return makeType({ ...object, props }, [], {});
508
+ },
509
+
510
+ merge(this: InternalType, def: unknown): InternalType {
511
+ return makeType(mergeObjects(requireObject(this.ir, "merge"), requireObject(parseDef(def), "merge")), [], {});
512
+ },
513
+
514
+ extract(this: InternalType, def: unknown): InternalType {
515
+ const other = parseDef(def);
516
+ return makeType(
517
+ {
518
+ k: "refine",
519
+ base: embed(this),
520
+ pred: value => !(walk(other, value) instanceof OmpErrors),
521
+ expected: "a value included by the extracted type",
522
+ },
523
+ [],
524
+ {},
525
+ );
526
+ },
527
+
528
+ exclude(this: InternalType, def: unknown): InternalType {
529
+ const other = parseDef(def);
530
+ return makeType(
531
+ {
532
+ k: "refine",
533
+ base: embed(this),
534
+ pred: value => walk(other, value) instanceof OmpErrors,
535
+ expected: "a value not excluded by the type",
536
+ },
537
+ [],
538
+ {},
539
+ );
540
+ },
541
+
542
+ onUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
543
+ const object = requireObject(this.ir, "onUndeclaredKey");
544
+ return makeType({ ...object, extras: behavior === "ignore" ? "keep" : behavior }, this[kSteps], metaOf(this));
545
+ },
546
+
547
+ onDeepUndeclaredKey(this: InternalType, behavior: "ignore" | "reject" | "delete"): InternalType {
548
+ return makeType(withDeepExtras(this.ir, behavior === "ignore" ? "keep" : behavior), this[kSteps], metaOf(this));
549
+ },
550
+
551
+ allows(this: InternalType, data: unknown): boolean {
552
+ const steps = this[kSteps];
553
+ let needsPredicates = false;
554
+ for (const step of steps) {
555
+ if (step.kind !== "pipe") {
556
+ needsPredicates = true;
557
+ break;
558
+ }
559
+ }
560
+ if (!needsPredicates) {
561
+ const allows = compileAllows(this.ir);
562
+ // Shadow the shared dispatcher once this schema has its specialized check.
563
+ this.allows = allows;
564
+ return allows(data);
565
+ }
566
+ for (const step of steps) {
567
+ if (step.kind === "filter" && !step.fn(data, new Ctx())) return false;
568
+ }
569
+ const out = this[kBase](data);
570
+ if (out instanceof OmpErrors) return false;
571
+ for (const step of steps) {
572
+ if (step.kind === "narrow" && !step.fn(out, new Ctx())) return false;
573
+ }
574
+ return true;
575
+ },
576
+
577
+ assert(this: InternalType, data: unknown): unknown {
578
+ const out = this.run(data);
579
+ if (out instanceof OmpErrors) throw new TraversalError(out);
580
+ return out;
581
+ },
582
+
583
+ from(this: InternalType, data: unknown): unknown {
584
+ const out = this.run(data);
585
+ if (out instanceof OmpErrors) throw new TraversalError(out);
586
+ return out;
587
+ },
588
+
589
+ toJsonSchema(this: InternalType, options?: ToJsonSchemaOptions): Record<string, unknown> {
590
+ const description = options?.description ?? this.description;
591
+ return irToJsonSchema(
592
+ this.ir,
593
+ options === undefined && description === undefined
594
+ ? undefined
595
+ : { ...options, ...(description === undefined ? {} : { description }) },
596
+ );
597
+ },
598
+ };
599
+
600
+ Object.defineProperty(typeMethods, "props", {
601
+ get(this: InternalType): readonly TypeProperty[] {
602
+ const object = requireObject(this.ir, "props");
603
+ return object.props.map(prop => propertyFromIR(prop));
604
+ },
605
+ });
606
+
607
+ // Share the fluent surface without per-schema method allocations or copies.
608
+ // Function.prototype remains in the chain, except bind is intentionally hidden
609
+ // so generic tool wrappers recognize callable schemas rather than rebinding them.
610
+ Object.setPrototypeOf(typeMethods, Function.prototype);
611
+ Object.defineProperty(typeMethods, "bind", { value: undefined });
612
+
613
+ function makeType(ir: IR, steps: Step[], meta: TypeMeta): InternalType;
614
+ function makeType<t = unknown, i = t>(ir: IR, steps: Step[], meta: TypeMeta): FluentType<t, i>;
615
+ function makeType(ir: IR, steps: Step[], meta: TypeMeta): unknown {
123
616
  let calls = 0;
124
- let impl = (data: unknown): unknown => {
617
+ let impl: Validator = (data: unknown): unknown => {
125
618
  if (++calls >= JIT_THRESHOLD) {
126
619
  impl = compile(ir);
127
620
  return impl(data);
@@ -129,99 +622,256 @@ function makeType(ir: IR, steps: Step[], meta: TypeMeta): FluentType {
129
622
  return walk(ir, data);
130
623
  };
131
624
 
132
- const runSteps = (data: unknown): unknown => {
133
- let out = impl(data);
134
- if (out instanceof OmpErrors) return out;
135
- for (const step of steps) {
136
- const ctx = new Ctx();
137
- // steps are typed against the schema's output; validated data satisfies it
138
- const fn = step.fn as (data: unknown, ctx: NarrowContext) => unknown;
139
- if (step.kind === "narrow") {
140
- if (!fn(out, ctx)) return OmpErrors.single([], ctx.expectation ?? "valid (narrow predicate failed)", out);
141
- } else {
142
- out = fn(out, ctx);
143
- if (out instanceof OmpErrors) return out;
144
- }
145
- }
146
- return out;
147
- };
625
+ const base: Validator =
626
+ meta.errorConfig === undefined
627
+ ? (data: unknown): unknown => impl(data)
628
+ : (data: unknown): unknown => {
629
+ const result = impl(data);
630
+ return result instanceof OmpErrors ? result.configure(meta.errorConfig ?? {}) : result;
631
+ };
148
632
 
149
- const callable =
150
- steps.length === 0 ? (data: unknown): unknown => impl(data) : (data: unknown): unknown => runSteps(data);
151
-
152
- const self = callable as FluentType & {
153
- [IR_BRAND]: true;
154
- ir: IR;
155
- hasSteps: boolean;
156
- hasDefault: boolean;
157
- defaultValue?: unknown;
158
- description?: string;
159
- run: (data: unknown) => unknown;
160
- };
633
+ const callable: Validator =
634
+ steps.length === 0
635
+ ? base
636
+ : (data: unknown): unknown => {
637
+ for (const step of steps) {
638
+ if (step.kind !== "filter") continue;
639
+ const ctx = new Ctx();
640
+ if (!step.fn(data, ctx)) {
641
+ return OmpErrors.single(
642
+ [],
643
+ ctx.expectation ?? "valid (input predicate failed)",
644
+ data,
645
+ meta.errorConfig,
646
+ );
647
+ }
648
+ }
649
+ let out = base(data);
650
+ if (out instanceof OmpErrors) return out;
651
+ for (const step of steps) {
652
+ if (step.kind === "filter") continue;
653
+ const ctx = new Ctx();
654
+ if (step.kind === "narrow") {
655
+ if (!step.fn(out, ctx)) {
656
+ return OmpErrors.single(
657
+ [],
658
+ ctx.expectation ?? "valid (narrow predicate failed)",
659
+ out,
660
+ meta.errorConfig,
661
+ );
662
+ }
663
+ } else {
664
+ out = step.fn(out, ctx);
665
+ if (out instanceof OmpErrors) {
666
+ return meta.errorConfig === undefined ? out : out.configure(meta.errorConfig);
667
+ }
668
+ }
669
+ }
670
+ return out;
671
+ };
672
+
673
+ const self = callable as InternalType;
161
674
  self[IR_BRAND] = true;
675
+ self[kBase] = base;
676
+ self[kSteps] = steps;
162
677
  self.ir = ir;
163
678
  self.hasSteps = steps.length > 0;
164
679
  self.hasDefault = meta.hasDefault === true;
165
680
  self.defaultValue = meta.defaultValue;
166
681
  self.description = meta.description;
682
+ self.errorConfig = meta.errorConfig;
167
683
  self.run = callable;
168
- // ArkType schema callables deliberately do not expose Function#bind. Generic
169
- // tool wrappers use that distinction to bind executable methods while passing
170
- // callable parameter schemas through unchanged; exposing bind here would turn
171
- // the schema into a bare bound function and strip its schema surface.
172
- Object.defineProperty(self, "bind", { value: undefined });
173
-
174
- const methods: Omit<FluentType, keyof typeof self | "infer" | "inferIn" | typeof IR_BRAND> = {
175
- describe: (description: string) => makeType({ ...ir, desc: description }, steps, { ...meta, description }),
176
- default: (value: unknown) => makeType(ir, steps, { ...meta, defaultValue: value, hasDefault: true }),
177
- or: (def: unknown) => {
178
- const other = parseDef(def as Def);
179
- const a = embed(self);
180
- const members = [...(a.k === "union" ? a.members : [a]), ...(other.k === "union" ? other.members : [other])];
181
- return makeType({ k: "union", members }, [], {});
182
- },
183
- and: (def: unknown) => makeType(intersect(embed(self), parseDef(def as Def)), [], {}),
184
- array: () => makeType({ k: "array", el: embed(self) }, [], {}),
185
- atLeastLength: (bound: number) => makeType(withLengthBound(ir, "min", bound), steps, meta),
186
- atMostLength: (bound: number) => makeType(withLengthBound(ir, "max", bound), steps, meta),
187
- atLeast: (bound: number) => makeType(withNumericBound(ir, "min", bound), steps, meta),
188
- atMost: (bound: number) => makeType(withNumericBound(ir, "max", bound), steps, meta),
189
- pipe: (fn: (data: never, ctx: NarrowContext) => unknown) => makeType(ir, [...steps, { kind: "pipe", fn }], meta),
190
- narrow: (fn: (data: never, ctx: NarrowContext) => boolean) =>
191
- makeType(ir, [...steps, { kind: "narrow", fn }], meta),
192
- allows: (data: unknown): boolean => {
193
- const out = impl(data);
194
- if (out instanceof OmpErrors) return false;
195
- for (const step of steps) {
196
- if (step.kind !== "narrow") continue;
197
- const fn = step.fn as (data: unknown, ctx: NarrowContext) => unknown;
198
- if (!fn(out, new Ctx())) return false;
199
- }
200
- return true;
201
- },
202
- assert: (data: unknown): unknown => {
203
- const out = callable(data);
204
- if (out instanceof OmpErrors) throw new TraversalError(out);
205
- return out;
206
- },
207
- toJsonSchema: (_options?: ToJsonSchemaOptions) =>
208
- irToJsonSchema(ir, meta.description === undefined ? undefined : { description: meta.description }),
209
- } as never;
210
- Object.assign(self, methods);
684
+ Object.setPrototypeOf(self, typeMethods);
211
685
  return self;
212
686
  }
213
687
 
214
- /** Merge two IR nodes for `.and()`; supports the object/object case this repo uses. */
688
+ type ObjectIR = Extract<IR, { k: "object" }>;
689
+
690
+ function requireObject(ir: IR, operation: string): ObjectIR {
691
+ if (ir.k !== "object") throw new OmpTypeError(`${operation} requires an object schema`);
692
+ return ir;
693
+ }
694
+
695
+ function propertyFromIR(prop: PropIR): TypeProperty {
696
+ return {
697
+ kind: prop.opt ? "optional" : "required",
698
+ key: prop.key,
699
+ value: makeType(prop.val, [], {}) as unknown as FluentType<unknown>,
700
+ ...(prop.hasDefault ? { default: prop.def } : {}),
701
+ meta: {},
702
+ };
703
+ }
704
+
705
+ function propertyToIR(property: TypeProperty): PropIR {
706
+ if (property.kind !== "required" && property.kind !== "optional") {
707
+ throw new OmpTypeError(`mapped property ${String(property.key)} has invalid kind`);
708
+ }
709
+ if (!(IR_BRAND in property.value)) {
710
+ throw new OmpTypeError(`mapped property ${String(property.key)} must contain a schema value`);
711
+ }
712
+ const hasDefault = Object.hasOwn(property, "default");
713
+ return {
714
+ key: String(property.key),
715
+ opt: property.kind === "optional",
716
+ val: embed(property.value),
717
+ ...(hasDefault
718
+ ? { def: property.default, defFactory: typeof property.default === "function", hasDefault: true }
719
+ : {}),
720
+ };
721
+ }
722
+
723
+ function dateRefinement(
724
+ schema: InternalType,
725
+ bound: Date,
726
+ relation: string,
727
+ predicate: (value: number) => boolean,
728
+ ): InternalType {
729
+ if (!Number.isFinite(bound.valueOf())) throw new OmpTypeError("date bound must be valid");
730
+ return makeType(
731
+ {
732
+ k: "refine",
733
+ base: schema.ir,
734
+ pred: value => value instanceof Date && predicate(value.valueOf()),
735
+ expected: `a Date ${relation} ${bound.toISOString()}`,
736
+ json: relation.includes("after") ? { minimum: bound.toISOString() } : { maximum: bound.toISOString() },
737
+ },
738
+ schema[kSteps],
739
+ metaOf(schema),
740
+ );
741
+ }
742
+
743
+ function selectNodes(root: IR, kind: string): readonly SelectedNode[] {
744
+ const selected: SelectedNode[] = [];
745
+ const seen = new Set<IR>();
746
+ const visit = (node: IR): void => {
747
+ if (seen.has(node)) return;
748
+ seen.add(node);
749
+ const nodeKind = node.k === "lit" ? "unit" : node.k;
750
+ if (kind === nodeKind || kind === node.k) {
751
+ selected.push(node.k === "lit" ? { kind: nodeKind, node, unit: node.v } : { kind: nodeKind, node });
752
+ }
753
+ switch (node.k) {
754
+ case "alias":
755
+ visit(node.resolve());
756
+ break;
757
+ case "array":
758
+ visit(node.el);
759
+ break;
760
+ case "tuple":
761
+ for (const item of node.prefix) visit(item.val);
762
+ if (node.variadic !== undefined) visit(node.variadic);
763
+ for (const item of node.postfix) visit(item);
764
+ break;
765
+ case "object":
766
+ for (const prop of node.props) visit(prop.val);
767
+ if (node.index !== undefined) visit(node.index);
768
+ break;
769
+ case "union":
770
+ case "intersection":
771
+ for (const member of node.members) visit(member);
772
+ break;
773
+ case "refine":
774
+ visit(node.base);
775
+ break;
776
+ case "morph":
777
+ visit(node.input);
778
+ if (node.out !== undefined) visit(node.out);
779
+ break;
780
+ case "sub":
781
+ visit(node.schema.ir);
782
+ break;
783
+ }
784
+ };
785
+ visit(root);
786
+ return selected;
787
+ }
788
+
789
+ function mergeObjects(left: ObjectIR, right: ObjectIR): ObjectIR {
790
+ const props = [...left.props];
791
+ for (const prop of right.props) {
792
+ const index = props.findIndex(candidate => candidate.key === prop.key);
793
+ if (index < 0) props.push(prop);
794
+ else props[index] = prop;
795
+ }
796
+ return {
797
+ k: "object",
798
+ props,
799
+ index: right.index ?? left.index,
800
+ extras: right.extras === "keep" ? left.extras : right.extras,
801
+ };
802
+ }
803
+
804
+ function withDeepExtras(ir: IR, extras: ObjectIR["extras"]): IR {
805
+ switch (ir.k) {
806
+ case "object":
807
+ return {
808
+ ...ir,
809
+ extras,
810
+ props: ir.props.map(prop => ({ ...prop, val: withDeepExtras(prop.val, extras) })),
811
+ index: ir.index === undefined ? undefined : withDeepExtras(ir.index, extras),
812
+ };
813
+ case "array":
814
+ return { ...ir, el: withDeepExtras(ir.el, extras) };
815
+ case "tuple":
816
+ return {
817
+ ...ir,
818
+ prefix: ir.prefix.map(item => ({ ...item, val: withDeepExtras(item.val, extras) })),
819
+ variadic: ir.variadic === undefined ? undefined : withDeepExtras(ir.variadic, extras),
820
+ postfix: ir.postfix.map(item => withDeepExtras(item, extras)),
821
+ };
822
+ case "union":
823
+ case "intersection":
824
+ return { ...ir, members: ir.members.map(member => withDeepExtras(member, extras)) };
825
+ case "refine":
826
+ return { ...ir, base: withDeepExtras(ir.base, extras) };
827
+ case "morph":
828
+ return {
829
+ ...ir,
830
+ input: withDeepExtras(ir.input, extras),
831
+ out: ir.out === undefined ? undefined : withDeepExtras(ir.out, extras),
832
+ };
833
+ default:
834
+ return ir;
835
+ }
836
+ }
837
+
838
+ /** Intersect two IR nodes, rejecting statically disjoint domains. */
215
839
  function intersect(a: IR, b: IR): IR {
840
+ if (a.k === "alias") return intersect(a.resolve(), b);
841
+ if (b.k === "alias") return intersect(a, b.resolve());
842
+ if (a.k === "never" || b.k === "never") throw new OmpTypeError("intersection with never is unsatisfiable");
843
+ if (a.k === "unknown") return b;
844
+ if (b.k === "unknown") return a;
845
+ if (a === b) return a;
846
+ if (a.k === "union" || b.k === "union") {
847
+ const union = a.k === "union" ? a : b.k === "union" ? b : undefined;
848
+ if (union === undefined) throw new OmpTypeError("union intersection invariant failed");
849
+ const branches = union.members;
850
+ const other = a.k === "union" ? b : a;
851
+ const members: IR[] = [];
852
+ for (const branch of branches) {
853
+ try {
854
+ members.push(intersect(branch, other));
855
+ } catch (error) {
856
+ if (!(error instanceof OmpTypeError)) throw error;
857
+ }
858
+ }
859
+ if (members.length === 0) throw new OmpTypeError("intersection has no satisfiable branches");
860
+ return members.length === 1 ? members[0] : { k: "union", members };
861
+ }
862
+ if (a.k === "lit") {
863
+ if (walk(b, a.v) instanceof OmpErrors) throw new OmpTypeError("literal is excluded by the intersection");
864
+ return a;
865
+ }
866
+ if (b.k === "lit") return intersect(b, a);
216
867
  if (a.k === "object" && b.k === "object") {
217
868
  const props = [...a.props];
218
869
  for (const bp of b.props) {
219
- const i = props.findIndex(p => p.key === bp.key);
220
- if (i < 0) {
221
- props.push(bp);
222
- } else {
223
- const ap = props[i];
224
- props[i] = { ...ap, opt: ap.opt && bp.opt, val: intersect(ap.val, bp.val) };
870
+ const index = props.findIndex(prop => prop.key === bp.key);
871
+ if (index < 0) props.push(bp);
872
+ else {
873
+ const ap = props[index];
874
+ props[index] = { ...ap, opt: ap.opt && bp.opt, val: intersect(ap.val, bp.val) };
225
875
  }
226
876
  }
227
877
  const extras =
@@ -234,22 +884,213 @@ function intersect(a: IR, b: IR): IR {
234
884
  return { k: "object", props, index, extras };
235
885
  }
236
886
  if (a.k === "string" && b.k === "string") {
237
- return { k: "string", min: maxOf(a.min, b.min), max: minOf(a.max, b.max), url: a.url || b.url };
887
+ const min = maxOf(a.min, b.min);
888
+ const max = minOf(a.max, b.max);
889
+ if (min !== undefined && max !== undefined && min > max) {
890
+ throw new OmpTypeError("string length intersection is unsatisfiable");
891
+ }
892
+ return { k: "string", min, max, url: a.url || b.url };
238
893
  }
239
894
  if (a.k === "number" && b.k === "number") {
895
+ const min = maxOf(a.min, b.min);
896
+ const max = minOf(a.max, b.max);
897
+ const xmin = min !== undefined && ((a.min === min && a.xmin === true) || (b.min === min && b.xmin === true));
898
+ const xmax = max !== undefined && ((a.max === max && a.xmax === true) || (b.max === max && b.xmax === true));
899
+ if (min !== undefined && max !== undefined && (min > max || (min === max && (xmin || xmax)))) {
900
+ throw new OmpTypeError("numeric range intersection is unsatisfiable");
901
+ }
902
+ if (a.divisor !== undefined && b.divisor !== undefined && a.divisor !== b.divisor) {
903
+ return { k: "intersection", members: [a, b] };
904
+ }
240
905
  return {
241
906
  k: "number",
242
907
  int: a.int || b.int,
243
- min: maxOf(a.min, b.min),
244
- max: minOf(a.max, b.max),
245
- xmin: a.xmin || b.xmin,
246
- xmax: a.xmax || b.xmax,
908
+ divisor: a.divisor ?? b.divisor,
909
+ min,
910
+ max,
911
+ xmin,
912
+ xmax,
247
913
  };
248
914
  }
249
- if (a.k === "unknown") return b;
250
- if (b.k === "unknown") return a;
251
- if (JSON.stringify(a) === JSON.stringify(b)) return a;
252
- throw new OmpTypeError(`unsupported intersection of ${a.k} and ${b.k}`);
915
+ if (a.k === "array" && b.k === "array") {
916
+ const min = maxOf(a.min, b.min);
917
+ const max = minOf(a.max, b.max);
918
+ if (min !== undefined && max !== undefined && min > max) {
919
+ throw new OmpTypeError("array length intersection is unsatisfiable");
920
+ }
921
+ return { k: "array", el: intersect(a.el, b.el), min, max };
922
+ }
923
+ if (a.k === "instance" && b.k === "instance") {
924
+ if (a.ctor === b.ctor || a.ctor.prototype instanceof b.ctor) return a;
925
+ if (b.ctor.prototype instanceof a.ctor) return b;
926
+ throw new OmpTypeError(`intersection of ${a.expected} and ${b.expected} is unsatisfiable`);
927
+ }
928
+ if (a.k === b.k && ["null", "undefined", "boolean", "bigint", "symbol", "anyobject"].includes(a.k)) return a;
929
+ const leftDomain = domainOf(a);
930
+ const rightDomain = domainOf(b);
931
+ if (leftDomain !== undefined && rightDomain !== undefined && leftDomain !== rightDomain) {
932
+ throw new OmpTypeError(`intersection of ${leftDomain} and ${rightDomain} is unsatisfiable`);
933
+ }
934
+ if (a.k === "anyobject" && rightDomain === "object") return b;
935
+ if (b.k === "anyobject" && leftDomain === "object") return a;
936
+ const members = [...(a.k === "intersection" ? a.members : [a]), ...(b.k === "intersection" ? b.members : [b])];
937
+ return { k: "intersection", members };
938
+ }
939
+
940
+ function domainOf(ir: IR): string | undefined {
941
+ switch (ir.k) {
942
+ case "null":
943
+ return "null";
944
+ case "undefined":
945
+ case "boolean":
946
+ case "bigint":
947
+ case "symbol":
948
+ case "string":
949
+ case "number":
950
+ return ir.k;
951
+ case "array":
952
+ case "tuple":
953
+ return "array";
954
+ case "object":
955
+ case "anyobject":
956
+ case "instance":
957
+ return "object";
958
+ case "lit":
959
+ return ir.v === null ? "null" : Array.isArray(ir.v) ? "array" : typeof ir.v;
960
+ case "refine":
961
+ return domainOf(ir.base);
962
+ case "morph":
963
+ return domainOf(ir.input);
964
+ case "sub":
965
+ return domainOf(ir.schema.ir);
966
+ case "alias":
967
+ return domainOf(ir.resolve());
968
+ case "union":
969
+ case "intersection": {
970
+ const first = domainOf(ir.members[0] ?? { k: "never" });
971
+ return ir.members.every(member => domainOf(member) === first) ? first : undefined;
972
+ }
973
+ default:
974
+ return undefined;
975
+ }
976
+ }
977
+ function lowerBoundWithin(source: Extract<IR, { k: "number" }>, target: Extract<IR, { k: "number" }>): boolean {
978
+ if (target.min === undefined) return true;
979
+ if (source.min === undefined || source.min < target.min) return false;
980
+ return source.min !== target.min || target.xmin !== true || source.xmin === true;
981
+ }
982
+
983
+ function upperBoundWithin(source: Extract<IR, { k: "number" }>, target: Extract<IR, { k: "number" }>): boolean {
984
+ if (target.max === undefined) return true;
985
+ if (source.max === undefined || source.max > target.max) return false;
986
+ return source.max !== target.max || target.xmax !== true || source.xmax === true;
987
+ }
988
+
989
+ function lengthWithin(
990
+ source: Extract<IR, { k: "string" | "array" }>,
991
+ target: Extract<IR, { k: "string" | "array" }>,
992
+ ): boolean {
993
+ return (
994
+ (target.min === undefined || (source.min !== undefined && source.min >= target.min)) &&
995
+ (target.max === undefined || (source.max !== undefined && source.max <= target.max))
996
+ );
997
+ }
998
+
999
+ function isSubtype(source: IR, target: IR, seen = new WeakMap<IR, Set<IR>>()): boolean {
1000
+ if (source === target || target.k === "unknown" || source.k === "never") return true;
1001
+ let targets = seen.get(source);
1002
+ if (targets?.has(target)) return true;
1003
+ if (targets === undefined) {
1004
+ targets = new Set();
1005
+ seen.set(source, targets);
1006
+ }
1007
+ targets.add(target);
1008
+ if (source.k === "alias") return isSubtype(source.resolve(), target, seen);
1009
+ if (target.k === "alias") return isSubtype(source, target.resolve(), seen);
1010
+ if (source.k === "union") return source.members.every(member => isSubtype(member, target, seen));
1011
+ if (target.k === "union") return target.members.some(member => isSubtype(source, member, seen));
1012
+ if (target.k === "intersection") return target.members.every(member => isSubtype(source, member, seen));
1013
+ if (source.k === "intersection") return source.members.some(member => isSubtype(member, target, seen));
1014
+ if (source.k === "lit") return !(walk(target, source.v) instanceof OmpErrors);
1015
+ if (source.k === "refine") return isSubtype(source.base, target, seen);
1016
+ if (source.k === "morph") return isSubtype(source.out ?? source.input, target, seen);
1017
+ if (source.k === "sub") return isSubtype(source.schema.ir, target, seen);
1018
+ if (target.k === "refine" || target.k === "morph" || target.k === "sub") return false;
1019
+ if (source.k === "string" && target.k === "string") {
1020
+ return lengthWithin(source, target) && (!target.url || source.url === true);
1021
+ }
1022
+ if (source.k === "number" && target.k === "number") {
1023
+ return (
1024
+ lowerBoundWithin(source, target) &&
1025
+ upperBoundWithin(source, target) &&
1026
+ (!target.int || source.int === true) &&
1027
+ (target.divisor === undefined || (source.divisor !== undefined && source.divisor % target.divisor === 0))
1028
+ );
1029
+ }
1030
+ if (source.k === "array" && target.k === "array") {
1031
+ return lengthWithin(source, target) && isSubtype(source.el, target.el, seen);
1032
+ }
1033
+ if (source.k === "object" && target.k === "object") {
1034
+ for (const targetProp of target.props) {
1035
+ const sourceProp = source.props.find(prop => prop.key === targetProp.key);
1036
+ if (sourceProp === undefined) {
1037
+ if (!targetProp.opt) return false;
1038
+ continue;
1039
+ }
1040
+ if (!targetProp.opt && sourceProp.opt) return false;
1041
+ if (!isSubtype(sourceProp.val, targetProp.val, seen)) return false;
1042
+ }
1043
+ if (target.extras === "reject") {
1044
+ if (source.extras !== "reject") return false;
1045
+ if (
1046
+ target.index === undefined &&
1047
+ source.props.some(sourceProp => !target.props.some(targetProp => targetProp.key === sourceProp.key))
1048
+ ) {
1049
+ return false;
1050
+ }
1051
+ }
1052
+ return true;
1053
+ }
1054
+ if (source.k === "instance" && target.k === "instance") {
1055
+ return source.ctor === target.ctor || source.ctor.prototype instanceof target.ctor;
1056
+ }
1057
+ if (source.k === "object" && target.k === "anyobject") return true;
1058
+ if (source.k === "instance" && target.k === "anyobject") return true;
1059
+ if (source.k === "tuple" && target.k === "array") {
1060
+ return (
1061
+ source.prefix.every(item => isSubtype(item.val, target.el, seen)) &&
1062
+ source.postfix.every(item => isSubtype(item, target.el, seen)) &&
1063
+ (source.variadic === undefined || isSubtype(source.variadic, target.el, seen))
1064
+ );
1065
+ }
1066
+ if (source.k !== target.k) return false;
1067
+ switch (source.k) {
1068
+ case "null":
1069
+ case "undefined":
1070
+ case "boolean":
1071
+ case "bigint":
1072
+ case "symbol":
1073
+ case "anyobject":
1074
+ return true;
1075
+ case "tuple":
1076
+ return target.k === "tuple" && expectedTuple(source) === expectedTuple(target);
1077
+ case "instance":
1078
+ return target.k === "instance" && source.ctor === target.ctor;
1079
+ default:
1080
+ return false;
1081
+ }
1082
+ }
1083
+
1084
+ function expectedTuple(tuple: Extract<IR, { k: "tuple" }>): string {
1085
+ return JSON.stringify({
1086
+ prefix: tuple.prefix.map(item => [item.opt, item.hasDefault, expectedOf(item.val)]),
1087
+ variadic: tuple.variadic === undefined ? undefined : expectedOf(tuple.variadic),
1088
+ postfix: tuple.postfix.map(expectedOf),
1089
+ });
1090
+ }
1091
+
1092
+ function irEquals(left: IR, right: IR): boolean {
1093
+ return isSubtype(left, right) && isSubtype(right, left);
253
1094
  }
254
1095
 
255
1096
  function maxOf(a: number | undefined, b: number | undefined): number | undefined {
@@ -270,21 +1111,131 @@ function withLengthBound(ir: IR, side: "min" | "max", bound: number): IR {
270
1111
  }
271
1112
  throw new OmpTypeError(`cannot apply length bound to ${ir.k}`);
272
1113
  }
273
- function withNumericBound(ir: IR, side: "min" | "max", bound: number): IR {
1114
+ function withNumericBound(ir: IR, side: "min" | "max", bound: number, exclusive = false): IR {
274
1115
  if (ir.k === "number") {
275
- return side === "min" ? { ...ir, min: bound, xmin: false } : { ...ir, max: bound, xmax: false };
1116
+ return side === "min" ? { ...ir, min: bound, xmin: exclusive } : { ...ir, max: bound, xmax: exclusive };
276
1117
  }
277
1118
  throw new OmpTypeError(`cannot apply numeric bound to ${ir.k}`);
278
1119
  }
279
1120
 
280
1121
  /**
281
- * The `type()` builder: parses a definition into a callable schema.
282
- *
283
- * The `const def` generic drives static inference (`typeof schema.infer`);
284
- * the runtime is definition-shape-agnostic, hence the cast.
1122
+ * Parse a definition into a callable schema with distinct input/output inference.
285
1123
  */
286
- export function type<const def>(def: def): FluentType<InferDef<def>> {
287
- return makeType(parseDef(def as Def), [], {}) as unknown as FluentType<InferDef<def>>;
1124
+ export function type<const def>(def: def): FluentType<InferDef<def>, InferDefIn<def>> {
1125
+ return makeType<InferDef<def>, InferDefIn<def>>(parseDef(def), EMPTY_STEPS, EMPTY_META);
1126
+ }
1127
+
1128
+ /** String keyword with a parser that morphs validated text to another output. */
1129
+ export interface ParsedStringKeyword<parsed> extends FluentType<string> {
1130
+ readonly parse: FluentType<parsed, string>;
1131
+ }
1132
+
1133
+ /** Morphing string keyword paired with its non-morphing preformatted validator. */
1134
+ export interface PreformattedKeyword extends FluentType<string, string> {
1135
+ readonly preformatted: FluentType<string>;
1136
+ }
1137
+
1138
+ /** Base64 keyword with its URL-safe alphabet variant. */
1139
+ export interface Base64Keyword extends FluentType<string> {
1140
+ readonly url: FluentType<string>;
1141
+ }
1142
+
1143
+ /** Date-string keyword family. */
1144
+ export interface DateStringKeyword extends ParsedStringKeyword<Date> {
1145
+ readonly iso: ParsedStringKeyword<Date>;
1146
+ readonly epoch: ParsedStringKeyword<Date>;
1147
+ }
1148
+
1149
+ /** IP address keyword family. */
1150
+ export interface IpKeyword extends FluentType<string> {
1151
+ readonly v4: FluentType<string>;
1152
+ readonly v6: FluentType<string>;
1153
+ }
1154
+
1155
+ /** UUID keyword family. */
1156
+ export interface UuidKeyword extends FluentType<string> {
1157
+ readonly v1: FluentType<string>;
1158
+ readonly v2: FluentType<string>;
1159
+ readonly v3: FluentType<string>;
1160
+ readonly v4: FluentType<string>;
1161
+ readonly v5: FluentType<string>;
1162
+ readonly v6: FluentType<string>;
1163
+ readonly v7: FluentType<string>;
1164
+ readonly v8: FluentType<string>;
1165
+ }
1166
+
1167
+ /** String normalization keyword family. */
1168
+ export interface NormalizeKeyword extends PreformattedKeyword {
1169
+ readonly NFC: PreformattedKeyword;
1170
+ readonly NFD: PreformattedKeyword;
1171
+ readonly NFKC: PreformattedKeyword;
1172
+ readonly NFKD: PreformattedKeyword;
1173
+ }
1174
+
1175
+ /** Runtime string parsers exposed under `type.parse`. */
1176
+ export interface ParseKeyword {
1177
+ readonly number: FluentType<number, string>;
1178
+ readonly integer: FluentType<number, string>;
1179
+ readonly json: FluentType<unknown, string>;
1180
+ readonly date: FluentType<Date, string>;
1181
+ readonly url: FluentType<URL, string>;
1182
+ readonly boolean: FluentType<boolean, string>;
1183
+ readonly bigint: FluentType<bigint, string>;
1184
+ }
1185
+
1186
+ /** Full string keyword module attached to `type.string`. */
1187
+ export interface StringKeyword extends FluentType<string> {
1188
+ readonly alpha: FluentType<string>;
1189
+ readonly alphanumeric: FluentType<string>;
1190
+ readonly base64: Base64Keyword;
1191
+ readonly capitalize: PreformattedKeyword;
1192
+ readonly creditCard: FluentType<string>;
1193
+ readonly date: DateStringKeyword;
1194
+ readonly digits: FluentType<string>;
1195
+ readonly email: FluentType<string>;
1196
+ readonly hex: FluentType<string>;
1197
+ readonly integer: ParsedStringKeyword<number>;
1198
+ readonly ip: IpKeyword;
1199
+ readonly json: ParsedStringKeyword<unknown>;
1200
+ readonly lower: PreformattedKeyword;
1201
+ readonly normalize: NormalizeKeyword;
1202
+ readonly numeric: ParsedStringKeyword<number>;
1203
+ readonly regex: FluentType<string>;
1204
+ readonly semver: FluentType<string>;
1205
+ readonly trim: PreformattedKeyword;
1206
+ readonly upper: PreformattedKeyword;
1207
+ readonly url: ParsedStringKeyword<URL>;
1208
+ readonly uuid: UuidKeyword;
1209
+ }
1210
+
1211
+ /** Number keyword module attached to `type.number`. */
1212
+ export interface NumberKeyword extends FluentType<number> {
1213
+ readonly integer: FluentType<number>;
1214
+ }
1215
+
1216
+ type Constructed<ctor> = ctor extends abstract new (...args: never[]) => infer instance ? instance : never;
1217
+
1218
+ function keywordSchema<output = string, input = output>(name: string): FluentType<output, input> {
1219
+ const ir = keywordIR(name);
1220
+ if (ir === undefined) throw new OmpTypeError(`missing built-in keyword ${name}`);
1221
+ return makeType<output, input>(ir, [], {});
1222
+ }
1223
+
1224
+ function parsedKeyword<parsed>(name: string): ParsedStringKeyword<parsed> {
1225
+ return Object.assign(keywordSchema<string>(name), {
1226
+ parse: keywordSchema<parsed, string>(`${name}.parse`),
1227
+ });
1228
+ }
1229
+
1230
+ function preformattedKeyword(name: string): PreformattedKeyword {
1231
+ return Object.assign(keywordSchema<string, string>(name), {
1232
+ preformatted: keywordSchema(`${name}.preformatted`),
1233
+ });
1234
+ }
1235
+
1236
+ function caseResolver(value: unknown): (input: unknown) => unknown {
1237
+ if (typeof value !== "function") throw new OmpTypeError("match case values must be functions");
1238
+ return input => Reflect.apply(value, undefined, [input]);
288
1239
  }
289
1240
 
290
1241
  export namespace type {
@@ -292,38 +1243,242 @@ export namespace type {
292
1243
  export const errors = OmpErrors;
293
1244
  export type errors = OmpErrors;
294
1245
 
295
- /** Keyword statics for fluent building, e.g. `type.number.atLeast(5)`. */
296
- export const string: FluentType<string> = makeType({ k: "string" }, [], {}) as unknown as FluentType<string>;
297
- export const number: FluentType<number> = makeType({ k: "number" }, [], {}) as unknown as FluentType<number>;
298
- export const boolean: FluentType<boolean> = makeType({ k: "boolean" }, [], {}) as unknown as FluentType<boolean>;
299
- export const unknown: FluentType<unknown> = makeType({ k: "unknown" }, [], {});
1246
+ const normalize: NormalizeKeyword = Object.assign(keywordSchema<string, string>("string.normalize"), {
1247
+ preformatted: keywordSchema("string.normalize.NFC.preformatted"),
1248
+ NFC: preformattedKeyword("string.normalize.NFC"),
1249
+ NFD: preformattedKeyword("string.normalize.NFD"),
1250
+ NFKC: preformattedKeyword("string.normalize.NFKC"),
1251
+ NFKD: preformattedKeyword("string.normalize.NFKD"),
1252
+ });
1253
+ const base64: Base64Keyword = Object.assign(keywordSchema("string.base64"), {
1254
+ url: keywordSchema("string.base64.url"),
1255
+ });
1256
+ const date: DateStringKeyword = Object.assign(parsedKeyword<Date>("string.date"), {
1257
+ iso: parsedKeyword<Date>("string.date.iso"),
1258
+ epoch: parsedKeyword<Date>("string.date.epoch"),
1259
+ });
1260
+ const ip: IpKeyword = Object.assign(keywordSchema("string.ip"), {
1261
+ v4: keywordSchema("string.ip.v4"),
1262
+ v6: keywordSchema("string.ip.v6"),
1263
+ });
1264
+ const uuid: UuidKeyword = Object.assign(keywordSchema("string.uuid"), {
1265
+ v1: keywordSchema("string.uuid.v1"),
1266
+ v2: keywordSchema("string.uuid.v2"),
1267
+ v3: keywordSchema("string.uuid.v3"),
1268
+ v4: keywordSchema("string.uuid.v4"),
1269
+ v5: keywordSchema("string.uuid.v5"),
1270
+ v6: keywordSchema("string.uuid.v6"),
1271
+ v7: keywordSchema("string.uuid.v7"),
1272
+ v8: keywordSchema("string.uuid.v8"),
1273
+ });
1274
+
1275
+ /** String validator and its refinement/morph keyword module. */
1276
+ export const string: StringKeyword = Object.assign(makeType<string>({ k: "string" }, [], {}), {
1277
+ alpha: keywordSchema("string.alpha"),
1278
+ alphanumeric: keywordSchema("string.alphanumeric"),
1279
+ base64,
1280
+ capitalize: preformattedKeyword("string.capitalize"),
1281
+ creditCard: keywordSchema("string.creditCard"),
1282
+ date,
1283
+ digits: keywordSchema("string.digits"),
1284
+ email: keywordSchema("string.email"),
1285
+ hex: keywordSchema("string.hex"),
1286
+ integer: parsedKeyword<number>("string.integer"),
1287
+ ip,
1288
+ json: parsedKeyword<unknown>("string.json"),
1289
+ lower: preformattedKeyword("string.lower"),
1290
+ normalize,
1291
+ numeric: parsedKeyword<number>("string.numeric"),
1292
+ regex: keywordSchema("string.regex"),
1293
+ semver: keywordSchema("string.semver"),
1294
+ trim: preformattedKeyword("string.trim"),
1295
+ upper: preformattedKeyword("string.upper"),
1296
+ url: parsedKeyword<URL>("string.url"),
1297
+ uuid,
1298
+ });
1299
+
1300
+ /** Runtime parser keyword family. */
1301
+ export const parse: ParseKeyword = {
1302
+ number: keywordSchema<number, string>("parse.number"),
1303
+ integer: keywordSchema<number, string>("parse.integer"),
1304
+ json: keywordSchema<unknown, string>("parse.json"),
1305
+ date: keywordSchema<Date, string>("parse.date"),
1306
+ url: keywordSchema<URL, string>("parse.url"),
1307
+ boolean: keywordSchema<boolean, string>("parse.boolean"),
1308
+ bigint: keywordSchema<bigint, string>("parse.bigint"),
1309
+ };
1310
+
1311
+ /** Number validator with integer refinement. */
1312
+ export const number: NumberKeyword = Object.assign(makeType<number>({ k: "number" }, [], {}), {
1313
+ integer: makeType<number>({ k: "number", int: true }, [], {}),
1314
+ });
1315
+
1316
+ /** Boolean validator. */
1317
+ export const boolean = makeType<boolean>({ k: "boolean" }, [], {});
1318
+ /** Bigint validator. */
1319
+ export const bigint = makeType<bigint>({ k: "bigint" }, [], {});
1320
+ /** Symbol validator. */
1321
+ export const symbol = makeType<symbol>({ k: "symbol" }, [], {});
1322
+ /** Non-null object validator. */
1323
+ export const object = makeType<object>({ k: "anyobject" }, [], {});
1324
+ /** Unknown validator. */
1325
+ export const unknown = makeType<unknown>({ k: "unknown" }, [], {});
1326
+ /** Alias of the unknown validator. */
1327
+ export const any = unknown;
1328
+ /** Validator that rejects every value. */
1329
+ export const never = makeType<never>({ k: "never" }, [], {});
1330
+ /** Date instance validator. */
1331
+ // biome-ignore lint/suspicious/noShadowRestrictedNames: ArkType exposes this exact keyword.
1332
+ export const Date = makeType<globalThis.Date>({ k: "instance", ctor: globalThis.Date, expected: "a Date" }, [], {});
300
1333
 
301
- /** Union of literal values from a runtime array (`type.enumerated(...list)`). */
1334
+ /** Validate instances of `ctor`. */
1335
+ export function instanceOf<const ctor extends Constructor>(ctor: ctor): FluentType<Constructed<ctor>> {
1336
+ const name = Reflect.get(ctor, "name");
1337
+ const expected = typeof name === "string" && name.length > 0 ? `an instance of ${name}` : "an instance";
1338
+ return makeType<Constructed<ctor>>({ k: "instance", ctor, expected }, [], {});
1339
+ }
1340
+
1341
+ /** Validate one exact unit value. */
1342
+ export function unit<const value>(value: value): FluentType<value> {
1343
+ return makeType<value>({ k: "lit", v: value }, [], {});
1344
+ }
1345
+
1346
+ /** Union of literal values from a runtime array. */
302
1347
  export function enumerated<const values extends readonly unknown[]>(...values: values): FluentType<values[number]> {
1348
+ const members: IR[] = values.map(value => ({ k: "lit", v: value }));
303
1349
  const ir: IR =
304
- values.length === 1
305
- ? { k: "lit", v: values[0] }
306
- : { k: "union", members: values.map(v => ({ k: "lit", v }) as IR) };
307
- return makeType(ir, [], {}) as unknown as FluentType<values[number]>;
1350
+ members.length === 0 ? { k: "never" } : members.length === 1 ? members[0] : { k: "union", members };
1351
+ return makeType<values[number]>(ir, [], {});
1352
+ }
1353
+
1354
+ /** Build a first-match dispatcher from schema-expression keys and a `default` case. */
1355
+ export function match<const cases extends Record<string, unknown>>(cases: cases): (value: unknown) => unknown {
1356
+ const branches: { schema: BaseType; resolve: (value: unknown) => unknown }[] = [];
1357
+ let fallback: ((value: unknown) => unknown) | undefined;
1358
+ for (const definition in cases) {
1359
+ const resolver = caseResolver(cases[definition]);
1360
+ if (definition === "default") fallback = resolver;
1361
+ else branches.push({ schema: raw(definition), resolve: resolver });
1362
+ }
1363
+ return value => {
1364
+ for (const branch of branches) {
1365
+ if (branch.schema.allows(value)) return branch.resolve(value);
1366
+ }
1367
+ if (fallback !== undefined) return fallback(value);
1368
+ throw new OmpTypeError("match requires a matching case or default");
1369
+ };
1370
+ }
1371
+ /** Preserve a definition's literal type while authoring reusable modules. */
1372
+ export function define<const definition>(definition: definition): definition {
1373
+ return definition;
1374
+ }
1375
+
1376
+ /** Build a lazy named scope from aliases and recursive definitions. */
1377
+ export function scope(aliases: Record<string, unknown>, options?: ScopeOptions): TypeScope {
1378
+ return buildScope(aliases, options);
1379
+ }
1380
+
1381
+ /** Compile a named schema module whose definitions may reference each other. */
1382
+ export function module<const definitions extends Record<string, unknown>>(
1383
+ definitions: definitions,
1384
+ ): { [name in keyof definitions]: Type<InferDef<definitions[name]>, InferDefIn<definitions[name]>> } {
1385
+ return scope(definitions).export() as unknown as {
1386
+ [name in keyof definitions]: Type<InferDef<definitions[name]>, InferDefIn<definitions[name]>>;
1387
+ };
308
1388
  }
309
1389
 
310
- /** Untyped builder for runtime-assembled definitions (`type.raw({...})`). */
1390
+ /** Build a runtime generic whose parameter names are supplied as `"<t, u>"`. */
1391
+ export function generic<const definition>(
1392
+ parameters: string,
1393
+ definition: definition,
1394
+ ): (...arguments_: readonly unknown[]) => BaseType {
1395
+ const names = parameters
1396
+ .replace(/^<|>$/g, "")
1397
+ .split(",")
1398
+ .map(name => name.trim())
1399
+ .filter(Boolean);
1400
+ return (...arguments_) => {
1401
+ if (arguments_.length !== names.length) {
1402
+ throw new OmpTypeError(`generic expects ${names.length} arguments (received ${arguments_.length})`);
1403
+ }
1404
+ const aliases: Record<string, unknown> = {};
1405
+ for (let index = 0; index < names.length; index++) aliases[names[index]] = arguments_[index];
1406
+ return scope(aliases).type(definition) as unknown as BaseType;
1407
+ };
1408
+ }
1409
+
1410
+ /** Untyped builder for runtime-assembled definitions. */
311
1411
  export function raw(def: unknown): BaseType {
312
- return makeType(parseDef(def as Def), [], {});
1412
+ return makeType(parseDef(def), [], {}) as unknown as BaseType;
313
1413
  }
314
1414
  }
315
1415
 
1416
+ // Reserved words cannot be declared as namespace bindings, but ArkType exposes
1417
+ // them as runtime keyword properties.
1418
+ Object.assign(type, {
1419
+ null: makeType<null>({ k: "null" }, [], {}),
1420
+ undefined: makeType<undefined>({ k: "undefined" }, [], {}),
1421
+ true: makeType<true>({ k: "lit", v: true }, [], {}),
1422
+ false: makeType<false>({ k: "lit", v: false }, [], {}),
1423
+ });
1424
+
316
1425
  export interface ScopeOptions {
317
1426
  jitless?: boolean;
318
1427
  }
319
1428
 
320
- /** ArkType-compatible scope wrapper; omptype's global builder is already lazy. */
321
- export function scope(_aliases: Record<string, unknown>, _options?: ScopeOptions): { type: typeof type } {
322
- return { type };
1429
+ /** Callable builder bound to one alias scope. */
1430
+ export type ScopedBuilder = <const definition>(
1431
+ definition: definition,
1432
+ ) => FluentType<InferDef<definition>, InferDefIn<definition>>;
1433
+
1434
+ /** Named schema scope with a scoped builder and compiled module export. */
1435
+ export interface TypeScope {
1436
+ readonly type: ScopedBuilder;
1437
+ export(): Record<string, BaseType>;
1438
+ }
1439
+
1440
+ /** Build a scope whose aliases resolve lazily, including recursive cycles. */
1441
+ export function scope(aliases: Record<string, unknown>, options?: ScopeOptions): TypeScope {
1442
+ return buildScope(aliases, options);
1443
+ }
1444
+
1445
+ function buildScope(aliases: Record<string, unknown>, _options?: ScopeOptions): TypeScope {
1446
+ const references = new Map<string, IR>();
1447
+ const targets = new Map<string, IR>();
1448
+ const resolve: AliasResolver = name => {
1449
+ if (!Object.hasOwn(aliases, name)) return undefined;
1450
+ const existing = references.get(name);
1451
+ if (existing !== undefined) return existing;
1452
+ const reference: IR = {
1453
+ k: "alias",
1454
+ name,
1455
+ resolve: () => {
1456
+ const target = targets.get(name);
1457
+ if (target !== undefined) return target;
1458
+ const parsed = parseDef(aliases[name], resolve);
1459
+ targets.set(name, parsed);
1460
+ return parsed;
1461
+ },
1462
+ };
1463
+ references.set(name, reference);
1464
+ return reference;
1465
+ };
1466
+ const scoped = Object.assign(
1467
+ (definition: unknown) => makeType(parseDef(definition, resolve), [], {}),
1468
+ type,
1469
+ ) as unknown as ScopedBuilder;
1470
+ return {
1471
+ type: scoped,
1472
+ export() {
1473
+ const schemas: Record<string, BaseType> = {};
1474
+ for (const name in aliases) schemas[name] = scoped(name) as unknown as BaseType;
1475
+ return schemas;
1476
+ },
1477
+ };
323
1478
  }
324
1479
 
325
1480
  /** A schema whose output type is not statically known (`type.raw` results). */
326
- export type BaseType = FluentType<unknown>;
1481
+ export type BaseType = FluentType<unknown, unknown>;
327
1482
 
328
1483
  /** `hasMorph` re-export for diagnostics/tooling. */
329
1484
  export { hasMorph };