1import { assert } from "@effect/vitest"
2import { Effect, Schema, SchemaAST, SchemaGetter, SchemaTransformation } from "effect"
3import { runInNewContext } from "node:vm"
4import { describe, it } from "vitest"
5import { deepStrictEqual, doesNotThrow, strictEqual, throws } from "../utils/assert.ts"
6
7describe("SchemaAST", () => {
8 describe("mapOrSame", () => {
9 it("returns the original array when no element changes", () => {
10 const input = [{ value: 1 }, { value: 2 }]
11 strictEqual(SchemaAST.mapOrSame(input, (value) => value), input)
12 })
13
14 it("returns all mapped elements when an element changes", () => {
15 const first = { value: 1 }
16 const second = { value: 2 }
17 const third = { value: 3 }
18 const replacement = { value: 4 }
19 const input = [first, second, third]
20 let calls = 0
21 const output = SchemaAST.mapOrSame(input, (value) => {
22 calls++
23 return value === second ? replacement : value
24 })
25
26 deepStrictEqual(output, [first, replacement, third])
27 strictEqual(output === input, false)
28 strictEqual(calls, input.length)
29 })
30 })
31
32 describe("AST updates", () => {
33 it("preserves the prototype, children and enumerable symbols when updating a frozen node", () => {
34 const marker = Symbol("marker")
35 class Objects extends SchemaAST.Objects {
36 readonly [marker] = { value: "custom" }
37 }
38 const ast = Object.freeze(new Objects([new SchemaAST.PropertySignature("a", SchemaAST.string)], []))
39 const checks: SchemaAST.Checks = [Schema.isMinProperties(1)]
40 const updated = SchemaAST.replaceChecks(ast, checks)
41
42 strictEqual(updated === ast, false)
43 strictEqual(updated instanceof Objects, true)
44 strictEqual(updated[marker], ast[marker])
45 strictEqual(updated.propertySignatures, ast.propertySignatures)
46 strictEqual(updated.indexSignatures, ast.indexSignatures)
47 strictEqual(updated.checks, checks)
48 strictEqual(ast.checks, undefined)
49 deepStrictEqual(Schema.decodeUnknownSync(Schema.make<Schema.Codec<unknown>>(updated))({ a: "value" }), {
50 a: "value"
51 })
52 })
53
54 it("keeps identity for unchanged checks, encoding and context", () => {
55 const ast = Schema.NumberFromString.check(Schema.isGreaterThan(0)).ast
56
57 strictEqual(SchemaAST.replaceChecks(ast, ast.checks), ast)
58 strictEqual(SchemaAST.replaceEncoding(ast, ast.encoding), ast)
59 strictEqual(SchemaAST.replaceContext(ast, ast.context), ast)
60 })
61
62 it("removes encoding without changing the source or its checks", () => {
63 const ast = Schema.NumberFromString.check(Schema.isGreaterThan(0)).ast
64 const encoding = ast.encoding
65 const updated = SchemaAST.replaceEncoding(ast, undefined)
66
67 strictEqual(updated.encoding, undefined)
68 strictEqual(updated.checks, ast.checks)
69 strictEqual(ast.encoding, encoding)
70 strictEqual(encoding === undefined, false)
71 strictEqual(Schema.decodeUnknownSync(Schema.make<Schema.Codec<unknown>>(updated))(1), 1)
72 throws(() => Schema.decodeUnknownSync(Schema.make<Schema.Codec<unknown>>(updated))(-1))
73 })
74
75 it("restores context owners without losing changes made to a contextual node", () => {
76 const ast = Schema.Struct({ a: Schema.String }).ast
77 const context = new SchemaAST.Context(true, false)
78 const contextual = SchemaAST.replaceContext(ast, context)
79 const checks: SchemaAST.Checks = [Schema.isMinProperties(1)]
80 const checked = SchemaAST.replaceChecks(contextual, checks)
81 const restored = SchemaAST.replaceContext(checked, undefined)
82
83 strictEqual(SchemaAST.getContextOwner(contextual), ast)
84 strictEqual(SchemaAST.replaceContext(contextual, undefined), ast)
85 strictEqual(SchemaAST.getContextOwner(checked), checked)
86 strictEqual(restored.checks, checks)
87 strictEqual(restored.context, undefined)
88 strictEqual(checked.context, context)
89 strictEqual(ast.context, undefined)
90 })
91
92 it("shares prepared template data when merging annotations", () => {
93 const ast = Schema.TemplateLiteral(["item-", Schema.String]).ast
94 const annotated = SchemaAST.annotate(ast, { title: "Item" })
95 const updated = SchemaAST.annotate(annotated, { description: "An item identifier" })
96
97 strictEqual(updated.parts, ast.parts)
98 strictEqual(updated.encodedParts, ast.encodedParts)
99 strictEqual(updated.literals, ast.literals)
100 strictEqual(updated.suffixLengths, ast.suffixLengths)
101 deepStrictEqual(updated.annotations, { title: "Item", description: "An item identifier" })
102 strictEqual(annotated.annotations?.description, undefined)
103 strictEqual(Schema.decodeUnknownSync(Schema.make<Schema.Codec<unknown>>(updated))("item-a"), "item-a")
104 })
105
106 it("restores the outer context without restoring an earlier encoding", () => {
107 const source = Schema.NumberFromString.ast
108 const optional = SchemaAST.optionalKey(source)
109 const restored = SchemaAST.replaceContext(optional, source.context)
110 const schema = Schema.make<Schema.Codec<unknown>>(
111 new SchemaAST.Objects([new SchemaAST.PropertySignature("value", restored)], [])
112 )
113
114 assert.strictEqual(restored.encoding, optional.encoding)
115 assert.strictEqual(SchemaAST.isOptional(SchemaAST.toType(restored)), false)
116 assert.strictEqual(SchemaAST.isOptional(SchemaAST.toEncoded(restored)), true)
117 assert.deepStrictEqual(Schema.decodeUnknownSync(schema)({ value: "1" }), { value: 1 })
118 assert.strictEqual(Schema.decodeUnknownExit(schema)({})._tag, "Failure")
119 })
120
121 it("shares the memoized suspended thunk when copying metadata", () => {
122 let calls = 0
123 const ast = new SchemaAST.Suspend(() => {
124 calls++
125 return SchemaAST.string
126 })
127 const annotated = SchemaAST.annotate(ast, { title: "Suspended string" })
128
129 strictEqual(calls, 0)
130 strictEqual(annotated.thunk, ast.thunk)
131 strictEqual(annotated.thunk(), SchemaAST.string)
132 strictEqual(ast.thunk(), SchemaAST.string)
133 strictEqual(calls, 1)
134 throws(
135 () => SchemaAST.replaceChecks(annotated, [Schema.isMinLength(1)]),
136 new Error("Cannot add checks to Suspend")
137 )
138 })
139 })
140
141 it("stores constructor defaults directly in the context", () => {
142 const defaultValue = Effect.succeed("default")
143 const ast = SchemaAST.withConstructorDefault(SchemaAST.string, defaultValue)
144 strictEqual(ast.context?.constructorDefault, defaultValue)
145 })
146
147 describe("Suspend", () => {
148 it("memoizes the thunk", () => {
149 let calls = 0
150 const ast = new SchemaAST.Suspend(() => {
151 calls++
152 return SchemaAST.string
153 })
154
155 strictEqual(calls, 0)
156 strictEqual(ast.thunk(), SchemaAST.string)
157 strictEqual(ast.thunk(), SchemaAST.string)
158 strictEqual(calls, 1)
159 })
160 })
161
162 it("isJson", () => {
163 strictEqual(SchemaAST.isJson(null), true)
164 strictEqual(SchemaAST.isJson(undefined), false)
165 strictEqual(SchemaAST.isJson(true), true)
166 strictEqual(SchemaAST.isJson(false), true)
167 strictEqual(SchemaAST.isJson("string"), true)
168 strictEqual(SchemaAST.isJson(1), true)
169 strictEqual(SchemaAST.isJson(1.5), true)
170 strictEqual(SchemaAST.isJson(1n), false)
171 strictEqual(SchemaAST.isJson(NaN), false)
172 strictEqual(SchemaAST.isJson(Infinity), false)
173 strictEqual(SchemaAST.isJson(-Infinity), false)
174 strictEqual(SchemaAST.isJson(Symbol.for("symbol")), false)
175 strictEqual(SchemaAST.isJson([]), true)
176 strictEqual(SchemaAST.isJson([1]), true)
177 strictEqual(SchemaAST.isJson(new Array(1)), false)
178 strictEqual(SchemaAST.isJson([1, undefined]), false)
179 strictEqual(SchemaAST.isJson([1, 1n]), false)
180 let getterCalls = 0
181 const arrayWithGetter = [0]
182 Object.defineProperty(arrayWithGetter, "0", {
183 enumerable: true,
184 get() {
185 getterCalls++
186 return 1
187 }
188 })
189 strictEqual(SchemaAST.isJson(arrayWithGetter), true)
190 strictEqual(getterCalls, 1)
191 strictEqual(SchemaAST.isJson({}), true)
192 strictEqual(SchemaAST.isJson({ a: 1 }), true)
193 strictEqual(SchemaAST.isJson({ a: undefined }), false)
194 strictEqual(SchemaAST.isJson({ a: 1, b: 1n }), false)
195 strictEqual(SchemaAST.isJson(new Map([["a", 1]])), false)
196 strictEqual(SchemaAST.isJson(new Set([1])), false)
197 strictEqual(SchemaAST.isJson(new Date(0)), false)
198 strictEqual(SchemaAST.isJson(/a/), false)
199 strictEqual(SchemaAST.isJson(new Uint8Array([1])), false)
200 class A {
201 readonly a = 1
202 }
203 strictEqual(SchemaAST.isJson(new A()), false)
204 const nullPrototype: Record<string, unknown> = Object.create(null)
205 nullPrototype.a = { b: [1, true, null] }
206 strictEqual(SchemaAST.isJson(nullPrototype), true)
207 const crossRealmRecord: unknown = runInNewContext("({ a: [1, true, null] })")
208 strictEqual(SchemaAST.isJson(crossRealmRecord), true)
209 const crossRealmClass: unknown = runInNewContext("new (class A { a = 1 })()")
210 strictEqual(SchemaAST.isJson(crossRealmClass), false)
211 // nested
212 strictEqual(SchemaAST.isJson({ a: { b: 1 } }), true)
213 strictEqual(SchemaAST.isJson({ a: [1, { b: "c" }] }), true)
214 strictEqual(SchemaAST.isJson({ a: { b: 1n } }), false)
215 // circular reference
216 const circular: Record<string, unknown> = {}
217 circular.self = circular
218 strictEqual(SchemaAST.isJson(circular), false)
219 // accepts DAGs
220 const shared = { a: 1 }
221 strictEqual(SchemaAST.isJson({ x: shared, y: shared }), true)
222 strictEqual(SchemaAST.isJson([shared, { nested: shared }]), true)
223 // Nested DAG
224 const deeper = { parent: { left: shared, right: shared } }
225 strictEqual(SchemaAST.isJson(deeper), true)
226 })
227
228 it("isJson is stack safe", () => {
229 let valid: unknown = null
230 let invalid: unknown = undefined
231 for (let i = 0; i < 25_000; i++) {
232 valid = [valid]
233 invalid = [invalid]
234 }
235 strictEqual(SchemaAST.isJson(valid), true)
236 strictEqual(SchemaAST.isJson(invalid), false)
237 })
238
239 it("Schema.toCodecJson rejects non-JSON objects", () => {
240 const encode = Schema.encodeUnknownExit(Schema.toCodecJson(Schema.Unknown))
241 strictEqual(encode(new Map([["a", 1]]))._tag, "Failure")
242 })
243
244 it("isStringTree", () => {
245 strictEqual(SchemaAST.isStringTree(undefined), true)
246 strictEqual(SchemaAST.isStringTree("string"), true)
247 strictEqual(SchemaAST.isStringTree(null), false)
248 strictEqual(SchemaAST.isStringTree(true), false)
249 strictEqual(SchemaAST.isStringTree(false), false)
250 strictEqual(SchemaAST.isStringTree(1), false)
251 strictEqual(SchemaAST.isStringTree(1n), false)
252 strictEqual(SchemaAST.isStringTree(Symbol.for("symbol")), false)
253 strictEqual(SchemaAST.isStringTree([]), true)
254 strictEqual(SchemaAST.isStringTree(["a"]), true)
255 strictEqual(SchemaAST.isStringTree(["a", undefined]), true)
256 strictEqual(SchemaAST.isStringTree(["a", 1]), false)
257 strictEqual(SchemaAST.isStringTree(new Array(1)), true)
258 const sparseWithInheritedValue = new Array(1)
259 const arrayPrototype = Object.create(Array.prototype)
260 arrayPrototype[0] = 1
261 Object.setPrototypeOf(sparseWithInheritedValue, arrayPrototype)
262 strictEqual(SchemaAST.isStringTree(sparseWithInheritedValue), false)
263 strictEqual(SchemaAST.isStringTree({}), true)
264 strictEqual(SchemaAST.isStringTree({ a: "b" }), true)
265 strictEqual(SchemaAST.isStringTree({ a: undefined }), true)
266 strictEqual(SchemaAST.isStringTree({ a: "b", c: 1 }), false)
267 strictEqual(SchemaAST.isStringTree(new Map([["a", "b"]])), false)
268 strictEqual(SchemaAST.isStringTree(new Date(0)), false)
269 class A {
270 readonly a = "a"
271 }
272 strictEqual(SchemaAST.isStringTree(new A()), false)
273 // nested
274 strictEqual(SchemaAST.isStringTree({ a: { b: "c" } }), true)
275 strictEqual(SchemaAST.isStringTree({ a: ["b", { c: "d" }] }), true)
276 strictEqual(SchemaAST.isStringTree({ a: { b: 1 } }), false)
277 // DAG
278 const shared = { value: "a" }
279 strictEqual(SchemaAST.isStringTree({ left: shared, right: shared }), true)
280 // circular reference
281 const circular: Record<string, unknown> = {}
282 circular.self = circular
283 strictEqual(SchemaAST.isStringTree(circular), false)
284 })
285
286 it("isStringTree is stack safe", () => {
287 let valid: unknown = "value"
288 let invalid: unknown = 1
289 for (let i = 0; i < 25_000; i++) {
290 valid = { value: valid }
291 invalid = { value: invalid }
292 }
293 strictEqual(SchemaAST.isStringTree(valid), true)
294 strictEqual(SchemaAST.isStringTree(invalid), false)
295 })
296
297 describe("toType", () => {
298 it("preserves different key optionality on the decoded and encoded sides", () => {
299 const value = Schema.NumberFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("1")))
300 const schema = Schema.Struct({ value })
301
302 assert.strictEqual(SchemaAST.isOptional(SchemaAST.toType(value.ast)), false)
303 assert.strictEqual(SchemaAST.isOptional(SchemaAST.toEncoded(value.ast)), true)
304 assert.deepStrictEqual(Schema.decodeUnknownSync(schema)({}), { value: 1 })
305 assert.deepStrictEqual(Schema.decodeUnknownSync(Schema.toEncoded(schema))({}), {})
306 assert.strictEqual(Schema.decodeUnknownExit(Schema.toType(schema))({})._tag, "Failure")
307 })
308
309 it("preserves declaration parsers when projecting contextual copies", () => {
310 const ast = new SchemaAST.Declaration(
311 [],
312 () => () => Effect.succeed("decoded"),
313 undefined,
314 undefined,
315 undefined,
316 undefined,
317 undefined,
318 () => () => Effect.succeed("encoded")
319 )
320 const contextual = SchemaAST.optionalKey(ast)
321 const decoded = SchemaAST.toType(contextual)
322 const encoded = SchemaAST.toEncoded(contextual)
323
324 assert.strictEqual(SchemaAST.isOptional(decoded), true)
325 assert.strictEqual(SchemaAST.isOptional(encoded), true)
326 assert.strictEqual(Schema.decodeUnknownSync(Schema.make<Schema.Codec<string>>(decoded))("input"), "decoded")
327 assert.strictEqual(Schema.decodeUnknownSync(Schema.make<Schema.Codec<string>>(encoded))("input"), "encoded")
328 assert.strictEqual(
329 Schema.decodeUnknownSync(Schema.make<Schema.Codec<string>>(SchemaAST.toType(SchemaAST.flip(contextual))))(
330 "input"
331 ),
332 "encoded"
333 )
334 })
335
336 it("removes outer encoding and promotes checks when children are unchanged", () => {
337 const encoding: SchemaAST.Encoding = [new SchemaAST.Link(SchemaAST.unknown, SchemaTransformation.passthrough())]
338 const context = new SchemaAST.Context(true, false)
339 const check = Schema.makeFilter((value: { readonly a: string }) => value.a.length > 1)
340 const ast = Object.freeze(
341 new SchemaAST.Objects(
342 [new SchemaAST.PropertySignature("a", SchemaAST.string)],
343 [],
344 { title: "Object" },
345 undefined,
346 encoding,
347 context,
348 [check]
349 )
350 )
351 const projected = SchemaAST.toType(ast)
352 const schema = Schema.make<Schema.Codec<unknown>>(projected)
353
354 strictEqual(projected.encoding, undefined)
355 strictEqual(projected.encodingChecks, undefined)
356 strictEqual(projected.checks?.[0], check)
357 strictEqual(projected.propertySignatures, ast.propertySignatures)
358 strictEqual(projected.annotations, ast.annotations)
359 strictEqual(projected.context, context)
360 strictEqual(ast.encoding, encoding)
361 strictEqual(ast.encodingChecks?.[0], check)
362 strictEqual(SchemaAST.toType(ast), projected)
363 strictEqual(SchemaAST.toType(projected), projected)
364 deepStrictEqual(Schema.decodeUnknownSync(schema)({ a: "ab" }), { a: "ab" })
365 throws(() => Schema.decodeUnknownSync(schema)({ a: "a" }))
366 })
367
368 it("keeps only structural encoding checks when outer encoding and child encoding are removed", () => {
369 const structural = Schema.isMinProperties(1)
370 const encoded = Schema.makeFilter((value: { readonly a: string }) => value.a.length > 1)
371 const decoded = Schema.makeFilter((value: { readonly a: number }) => value.a > 0)
372 const encoding: SchemaAST.Encoding = [new SchemaAST.Link(SchemaAST.unknown, SchemaTransformation.passthrough())]
373 const ast = new SchemaAST.Objects(
374 [new SchemaAST.PropertySignature("a", Schema.NumberFromString.ast)],
375 [],
376 undefined,
377 [decoded],
378 encoding,
379 undefined,
380 [encoded.and(structural)]
381 )
382 const projected = SchemaAST.toType(ast)
383 const schema = Schema.make<Schema.Codec<unknown>>(projected)
384
385 strictEqual(projected.encoding, undefined)
386 strictEqual(projected.encodingChecks, undefined)
387 deepStrictEqual(projected.checks, [decoded, structural])
388 strictEqual(projected.propertySignatures[0].type, SchemaAST.toType(Schema.NumberFromString.ast))
389 strictEqual(ast.encoding, encoding)
390 strictEqual(ast.propertySignatures[0].type, Schema.NumberFromString.ast)
391 strictEqual(SchemaAST.toType(projected), projected)
392 deepStrictEqual(Schema.decodeUnknownSync(schema)({ a: 1 }), { a: 1 })
393 throws(() => Schema.decodeUnknownSync(schema)({ a: -1 }))
394 throws(() => Schema.decodeUnknownSync(schema)({ a: "1" }))
395 })
396
397 it("preserves lazy recursive references when removing a suspended node's encoding", () => {
398 let calls = 0
399 const encoding: SchemaAST.Encoding = [new SchemaAST.Link(SchemaAST.unknown, SchemaTransformation.passthrough())]
400 const ast: SchemaAST.Suspend = new SchemaAST.Suspend(
401 () => {
402 calls++
403 return new SchemaAST.Objects([
404 new SchemaAST.PropertySignature("value", Schema.NumberFromString.ast),
405 new SchemaAST.PropertySignature("next", new SchemaAST.Union([SchemaAST.null, ast]))
406 ], [])
407 },
408 undefined,
409 undefined,
410 encoding
411 )
412 const projected = SchemaAST.toType(ast)
413
414 strictEqual(calls, 0)
415 strictEqual(projected.encoding, undefined)
416 strictEqual(ast.encoding, encoding)
417 strictEqual(SchemaAST.toType(ast), projected)
418 strictEqual(SchemaAST.toType(projected), projected)
419 const body = projected.thunk()
420 strictEqual(calls, 1)
421 strictEqual(projected.thunk(), body)
422 if (!SchemaAST.isObjects(body)) throw new Error("Expected Objects")
423 const next = body.propertySignatures[1].type
424 if (!SchemaAST.isUnion(next)) throw new Error("Expected Union")
425 strictEqual(next.types[1], projected)
426 const input = { value: 1, next: { value: 2, next: null } }
427 deepStrictEqual(Schema.decodeUnknownSync(Schema.make<Schema.Codec<unknown>>(projected))(input), input)
428 strictEqual(calls, 1)
429 })
430
431 it("is idempotent for suspended schemas", () => {
432 const schema = Schema.suspend(() => Schema.Struct({ a: Schema.NumberFromString }))
433 const ast = SchemaAST.toType(schema.ast)
434
435 strictEqual(SchemaAST.toType(ast), ast)
436 })
437
438 it("toEncoded is idempotent for suspended schemas", () => {
439 const schema = Schema.suspend(() => Schema.Struct({ a: Schema.NumberFromString }))
440 const ast = SchemaAST.toEncoded(schema.ast)
441
442 strictEqual(SchemaAST.toEncoded(ast), ast)
443 })
444
445 it("promotes encodingChecks when contained type shape is preserved", () => {
446 const schema = Schema.Struct({ a: Schema.String }).pipe(
447 Schema.flip,
448 Schema.check(Schema.makeFilter((o) => o.a.length > 1)),
449 Schema.flip
450 )
451
452 const ast = SchemaAST.toType(schema.ast)
453
454 strictEqual(SchemaAST.isObjects(ast), true)
455 strictEqual(ast.checks?.length, 1)
456 strictEqual(ast.encodingChecks, undefined)
457 })
458
459 it("toEncoded preserves parent checks when projecting checked children without transformations", () => {
460 const schema = Schema.Struct({
461 xs: Schema.Array(Schema.String).check(Schema.isMinLength(1))
462 }).check(Schema.makeFilter((o) => o.xs[0] === "ok"))
463 const encoded = Schema.toEncoded(schema)
464
465 strictEqual(Schema.decodeUnknownExit(schema)({ xs: ["bad"] })._tag, "Failure")
466 strictEqual(Schema.decodeUnknownExit(encoded)({ xs: ["bad"] })._tag, "Failure")
467 deepStrictEqual(Schema.decodeUnknownSync(encoded)({ xs: ["ok"] }), { xs: ["ok"] })
468 })
469
470 it("toEncoded preserves union checks when projecting checked members without transformations", () => {
471 const schema = Schema.Union([
472 Schema.Array(Schema.String).check(Schema.isMinLength(1)),
473 Schema.String
474 ]).check(Schema.makeFilter((value) => typeof value === "string" || value[0] === "ok"))
475 const encoded = Schema.toEncoded(schema)
476
477 strictEqual(Schema.decodeUnknownExit(encoded)(["bad"])._tag, "Failure")
478 deepStrictEqual(Schema.decodeUnknownSync(encoded)(["ok"]), ["ok"])
479 strictEqual(Schema.decodeUnknownSync(encoded)("ok"), "ok")
480 })
481
482 it("toEncoded drops parent checks when a union member has a transformation", () => {
483 const schema = Schema.Struct({
484 a: Schema.Union([Schema.NumberFromString, Schema.Boolean])
485 }).check(Schema.makeFilter((o) => typeof o.a === "boolean" || o.a > 0))
486 const encoded = Schema.toEncoded(schema)
487
488 strictEqual(Schema.decodeUnknownExit(schema)({ a: "-1" })._tag, "Failure")
489 deepStrictEqual(Schema.decodeUnknownSync(encoded)({ a: "-1" }), { a: "-1" })
490 })
491
492 it.each([false, true])("treats suspended children as opaque (already evaluated: %s)", (evaluated) => {
493 let calls = 0
494 const suspended = Schema.suspend(() => {
495 calls++
496 return Schema.String
497 })
498 if (evaluated) Schema.decodeUnknownSync(suspended)("ok")
499 const schema = Schema.Struct({ a: Schema.optionalKey(suspended) }).check(
500 Schema.makeFilter((o) => o.a !== "bad"),
501 Schema.isMinProperties(1)
502 )
503 const encoded = Schema.toEncoded(schema)
504
505 strictEqual(calls, evaluated ? 1 : 0)
506 strictEqual(Schema.decodeUnknownExit(encoded)({})._tag, "Failure")
507 deepStrictEqual(Schema.decodeUnknownSync(encoded)({ a: "bad" }), { a: "bad" })
508 })
509
510 it("drops encodingChecks when contained type shape changes", () => {
511 const schema = Schema.Struct({ a: Schema.FiniteFromString }).pipe(
512 Schema.flip,
513 Schema.check(Schema.makeFilter((o) => o.a.length > 1)),
514 Schema.flip
515 )
516
517 const ast = SchemaAST.toType(schema.ast)
518
519 strictEqual(SchemaAST.isObjects(ast), true)
520 strictEqual(ast.checks, undefined)
521 strictEqual(ast.encodingChecks, undefined)
522 })
523
524 it("preserves structural checks when contained type shape changes", () => {
525 const check = Schema.isMinProperties(1)
526 const schema = Schema.Struct({ a: Schema.NumberFromString }).check(check)
527
528 const ast = SchemaAST.toEncoded(schema.ast)
529
530 strictEqual(SchemaAST.isObjects(ast), true)
531 strictEqual(ast.checks?.[0], check)
532 })
533
534 it("preserves structural checks when contained element shape changes", () => {
535 const check = Schema.isMinLength(1)
536 const schema = Schema.Array(Schema.NumberFromString).check(check)
537
538 const ast = SchemaAST.toEncoded(schema.ast)
539
540 strictEqual(SchemaAST.isArrays(ast), true)
541 strictEqual(ast.checks?.[0], check)
542 })
543
544 it("preserves structural checks when a declaration type parameter shape changes", () => {
545 const check = Schema.isMinSize(1)
546 const schema = Schema.ReadonlySet(Schema.NumberFromString).check(check)
547
548 const ast = SchemaAST.toEncoded(schema.ast)
549
550 strictEqual(SchemaAST.isDeclaration(ast), true)
551 strictEqual(ast.checks?.[0], check)
552 })
553
554 it("preserves only the structural members of a mixed filter group", () => {
555 const structural = Schema.isMinProperties(1)
556 const group = structural.and(Schema.makeFilter<object>(() => true))
557 const schema = Schema.Struct({ a: Schema.NumberFromString }).check(group)
558
559 const ast = SchemaAST.toEncoded(schema.ast)
560
561 strictEqual(SchemaAST.isObjects(ast), true)
562 strictEqual(ast.checks?.length, 1)
563 strictEqual(ast.checks?.[0], structural)
564 })
565 })
566
567 describe("collectSentinels", () => {
568 describe("Declaration", () => {
569 it("~sentinels", () => {
570 class A {
571 readonly _tag = "A"
572 }
573 const schema = Schema.instanceOf(A, { "~sentinels": [{ key: "_tag", literal: "A" }] })
574 const ast = schema.ast
575 deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "A" }])
576 })
577 })
578
579 describe("Struct", () => {
580 it("required tag", () => {
581 const schema = Schema.Struct({
582 _tag: Schema.Literal("a"),
583 a: Schema.String
584 })
585 const ast = schema.ast
586 deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "a" }])
587 })
588
589 it("optional tag", () => {
590 const schema = Schema.Struct({
591 _tag: Schema.optionalKey(Schema.Literal("a")),
592 a: Schema.String
593 })
594 const ast = schema.ast
595 deepStrictEqual(SchemaAST.collectSentinels(ast), [])
596 })
597 })
598
599 describe("Tuple", () => {
600 it("required element", () => {
601 const schema = Schema.Tuple([Schema.Literal("a"), Schema.Number])
602 const ast = schema.ast
603 deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: 0, literal: "a" }])
604 })
605
606 it("optional element", () => {
607 const schema = Schema.Tuple([Schema.Number, Schema.optionalKey(Schema.Literal("a"))])
608 const ast = schema.ast
609 deepStrictEqual(SchemaAST.collectSentinels(ast), [])
610 })
611 })
612
613 it("Declaration", () => {
614 class A {
615 readonly _tag = "A"
616 }
617 const schema = Schema.instanceOf(
618 A,
619 { "~sentinels": [{ key: "_tag", literal: "A" }] }
620 )
621 const ast = schema.ast
622 deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "A" }])
623 })
624
625 it("Class", () => {
626 class A extends Schema.Class<A>("A")({
627 type: Schema.Literal("A"),
628 a: Schema.String
629 }) {}
630 const ast = A.ast
631 deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "type", literal: "A" }])
632 })
633
634 it("TaggedClass", () => {
635 class A extends Schema.TaggedClass<A>()("A", {
636 a: Schema.String
637 }) {}
638 const ast = A.ast
639 deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "A" }])
640 })
641
642 it("Error", () => {
643 class E extends Schema.Error<E>("E")({
644 type: Schema.Literal("E"),
645 e: Schema.String
646 }) {}
647 const ast = E.ast
648 deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "type", literal: "E" }])
649 })
650
651 it("TaggedError", () => {
652 class E extends Schema.TaggedError<E>()("E", {
653 e: Schema.String
654 }) {}
655 const ast = E.ast
656 deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "E" }])
657 })
658
659 it("Union: the sentinels common to every member", () => {
660 const shared = Schema.Union([
661 Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("x") }),
662 Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("y") })
663 ])
664 deepStrictEqual(SchemaAST.collectSentinels(shared.ast), [{ key: "kind", literal: "a" }])
665
666 const disjoint = Schema.Union([
667 Schema.Struct({ kind: Schema.Literal("a") }),
668 Schema.Struct({ kind: Schema.Literal("b") })
669 ])
670 deepStrictEqual(SchemaAST.collectSentinels(disjoint.ast), [])
671
672 // A suspended member stays opaque, so the intersection is conservative.
673 const withSuspend = Schema.Union([
674 Schema.Struct({ kind: Schema.Literal("a") }),
675 Schema.suspend(() => Schema.Struct({ kind: Schema.Literal("a") }))
676 ])
677 deepStrictEqual(SchemaAST.collectSentinels(withSuspend.ast), [])
678 })
679 })
680
681 describe("getCandidateIndex", () => {
682 it("should exclude never", () => {
683 const schema = Schema.Union([Schema.String, Schema.Never])
684 const index = SchemaAST.getCandidateIndex(schema.ast.types)
685 deepStrictEqual(index("a", false), [0])
686 })
687
688 it("should exclude by type", () => {
689 const schema = Schema.NullishOr(Schema.String)
690 const index = SchemaAST.getCandidateIndex(schema.ast.types)
691 deepStrictEqual(index("a", false), [0])
692 deepStrictEqual(index(null, false), [1])
693 deepStrictEqual(index(undefined, false), [2])
694 deepStrictEqual(index(1, false), [])
695 })
696
697 it("should exclude by literals", () => {
698 const schema = Schema.Union([
699 Schema.UniqueSymbol(Symbol.for("a")),
700 Schema.Literal("b"),
701 Schema.String
702 ])
703 const index = SchemaAST.getCandidateIndex(schema.ast.types)
704 deepStrictEqual(index(Symbol.for("a"), false), [0])
705 deepStrictEqual(index("b", false), [1, 2])
706 deepStrictEqual(index("c", false), [2])
707 deepStrictEqual(index(1, false), [])
708 deepStrictEqual(index(undefined, false), [])
709
710 const reversed = Schema.Union([Schema.String, Schema.Literal("b")]).ast
711 deepStrictEqual(SchemaAST.getCandidateIndex(reversed.types)("c", false), [0])
712 })
713
714 it("Literals", () => {
715 const schema = Schema.Literals(["a", "b", "c"])
716 const index = SchemaAST.getCandidateIndex(schema.ast.types)
717 deepStrictEqual(index("a", false), [0])
718 deepStrictEqual(index("b", false), [1])
719 deepStrictEqual(index("c", false), [2])
720 deepStrictEqual(index("d", false), [])
721 deepStrictEqual(index(null, false), [])
722 deepStrictEqual(index(undefined, false), [])
723 })
724
725 it("literal-only union with a unique symbol", () => {
726 const symbol = Symbol.for("a")
727 const schema = Schema.Union([Schema.UniqueSymbol(symbol), Schema.Literal("b")])
728 const index = SchemaAST.getCandidateIndex(schema.ast.types)
729 deepStrictEqual(index(symbol, false), [0])
730 deepStrictEqual(index(Symbol("a"), false), [])
731 })
732
733 it("should preserve duplicate literal candidates in member order", () => {
734 const schema = Schema.Union([
735 Schema.Literal("a").transform("first"),
736 Schema.Literal("a").transform("second"),
737 Schema.Never
738 ])
739 const index = SchemaAST.getCandidateIndex(schema.ast.types)
740 deepStrictEqual(index("a", false), [0, 1])
741 deepStrictEqual(index("b", false), [])
742 })
743
744 it("String | Literals", () => {
745 const schema = Schema.Union([Schema.String, Schema.Literals(["a", "b", "c"])])
746 const index = SchemaAST.getCandidateIndex(schema.ast.types)
747 deepStrictEqual(index(undefined, false), [])
748 })
749
750 it("should handle tagged structs", () => {
751 const schema = Schema.Union([
752 Schema.Struct({ _tag: Schema.tag("a"), a: Schema.String }),
753 Schema.Struct({ _tag: Schema.tag("b"), b: Schema.Number }),
754 Schema.String
755 ])
756 const index = SchemaAST.getCandidateIndex(schema.ast.types)
757 deepStrictEqual(index({}, false), [])
758 deepStrictEqual(index({ _tag: "a" }, false), [0])
759 deepStrictEqual(index({ _tag: "b" }, false), [1])
760 deepStrictEqual(index({ _tag: "c" }, false), [])
761 deepStrictEqual(index("", false), [2])
762 deepStrictEqual(index(1, false), [])
763 })
764
765 it("constructor mode should keep tagged candidates only when an object discriminator is missing", () => {
766 const schema = Schema.Union([
767 Schema.Struct({ _tag: Schema.tag("a"), a: Schema.String }),
768 Schema.Struct({ _tag: Schema.tag("b"), b: Schema.Number })
769 ])
770 const index = SchemaAST.getCandidateIndex(schema.ast.types)
771
772 deepStrictEqual(index({}, true), [0, 1])
773 deepStrictEqual(index({ _tag: undefined }, true), [0, 1])
774 deepStrictEqual(index({ _tag: "a" }, true), [0])
775 deepStrictEqual(index("a", true), [])
776 })
777
778 it("should handle function-valued declarations with sentinels", () => {
779 const a = Schema.declare(
780 (input): input is () => void => typeof input === "function",
781 { "~sentinels": [{ key: "kind", literal: "a" }] }
782 )
783 const b = Schema.declare(
784 (input): input is () => void => typeof input === "function",
785 { "~sentinels": [{ key: "kind", literal: "b" }] }
786 )
787 const schema = Schema.Union([a, b])
788 const index = SchemaAST.getCandidateIndex(schema.ast.types)
789 const input = Object.assign(() => {}, { kind: "a" })
790 deepStrictEqual(index(input, false), [0])
791 })
792
793 it("should preserve duplicate candidates with a common discriminator", () => {
794 const member = Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String })
795 const schema = Schema.Union([
796 member,
797 Schema.Struct({ kind: Schema.Literal("b"), value: Schema.Number }),
798 member,
799 Schema.Never
800 ])
801 const index = SchemaAST.getCandidateIndex(schema.ast.types)
802 deepStrictEqual(index({ kind: "a" }, false), [0, 2])
803 deepStrictEqual(index({ kind: "b" }, false), [1])
804 deepStrictEqual(index({ kind: "c" }, false), [])
805 deepStrictEqual(index({}, false), [])
806 deepStrictEqual(index("a", false), [])
807 })
808
809 it("should protect cached common sentinel candidates from external mutation", () => {
810 const schema = Schema.Union([
811 Schema.Struct({ kind: Schema.Literal("a") }),
812 Schema.Struct({ kind: Schema.Literal("b") })
813 ])
814 const index = SchemaAST.getCandidateIndex(schema.ast.types)
815 const input = { kind: "a" }
816 const candidates = index(input, false)
817 Reflect.set(candidates, candidates.length, 1)
818 deepStrictEqual(index(input, false), [0])
819 })
820
821 it("should reuse the candidates of a runtime type without literals", () => {
822 const schema = Schema.NullOr(Schema.Struct({ a: Schema.Number }))
823 const index = SchemaAST.getCandidateIndex(schema.ast.types)
824 const candidates = index({ a: 1 }, false)
825 deepStrictEqual(candidates, [0])
826 strictEqual(index({ b: 2 }, false), candidates)
827 strictEqual(Object.isFrozen(candidates), true)
828 deepStrictEqual(index(null, false), [1])
829 })
830
831 it("should reuse non-discriminated candidates of a tagged union", () => {
832 const schema = Schema.Union([
833 Schema.Struct({ _tag: Schema.tag("a"), a: Schema.String }),
834 Schema.Struct({ b: Schema.Number }),
835 Schema.String,
836 Schema.Literal(1)
837 ])
838 const index = SchemaAST.getCandidateIndex(schema.ast.types)
839 const strings = index("x", false)
840 deepStrictEqual(strings, [2])
841 strictEqual(index("y", false), strings)
842 strictEqual(Object.isFrozen(strings), true)
843 const objects = index({ _tag: "c" }, false)
844 deepStrictEqual(objects, [1])
845 strictEqual(index({ _tag: "d" }, false), objects)
846 strictEqual(Object.isFrozen(objects), true)
847 deepStrictEqual(index({ _tag: "a" }, false), [0, 1])
848 })
849
850 it("should handle candidates with different sentinel keys", () => {
851 const schema = Schema.Union([
852 Schema.Struct({
853 kind: Schema.Literal("a"),
854 status: Schema.Literal("ready"),
855 value: Schema.String
856 }),
857 Schema.Struct({ status: Schema.Literal("ready"), value: Schema.String })
858 ])
859 const index = SchemaAST.getCandidateIndex(schema.ast.types)
860 deepStrictEqual(
861 index({ kind: "a", status: "ready", value: "value" }, false),
862 [0, 1]
863 )
864 deepStrictEqual(
865 index({ kind: "b", status: "ready", value: "value" }, false),
866 [1]
867 )
868 deepStrictEqual(
869 index({ kind: undefined, status: "ready", value: "value" }, false),
870 [1]
871 )
872 })
873
874 it("should handle tagged tuples", () => {
875 const schema = Schema.Union([
876 Schema.Tuple([Schema.Literal("a"), Schema.String]),
877 Schema.Tuple([Schema.Literal("b"), Schema.Number]),
878 Schema.String
879 ])
880 const index = SchemaAST.getCandidateIndex(schema.ast.types)
881 deepStrictEqual(index([], false), [])
882 deepStrictEqual(index(["a"], false), [0])
883 deepStrictEqual(index(["b"], false), [1])
884 deepStrictEqual(index(["c"], false), [])
885 deepStrictEqual(index("", false), [2])
886 deepStrictEqual(index(1, false), [])
887 })
888
889 it("should handle tagged tuples with unique symbol sentinels", () => {
890 const a = Symbol.for("a")
891 const b = Symbol.for("b")
892 const schema = Schema.Union([
893 Schema.Tuple([Schema.UniqueSymbol(a), Schema.String]),
894 Schema.Tuple([Schema.UniqueSymbol(b), Schema.Number])
895 ])
896 const index = SchemaAST.getCandidateIndex(schema.ast.types)
897 deepStrictEqual(index([a, "value"], false), [0])
898 deepStrictEqual(index([b, 1], false), [1])
899 deepStrictEqual(index([Symbol("a"), "value"], false), [])
900 })
901
902 it(`should deduplicate repeated declaration sentinels in "oneOf" mode`, () => {
903 const member = Schema.declare(
904 (input): input is object => typeof input === "object" && input !== null,
905 {
906 "~sentinels": [
907 { key: "kind", literal: "a" },
908 { key: "kind", literal: "a" }
909 ]
910 }
911 )
912 const schema = Schema.Union([member], { mode: "oneOf" })
913 const input = { kind: "a" }
914 strictEqual(Schema.decodeUnknownSync(schema)(input), input)
915 })
916
917 it("should dispatch a nested union member by its common sentinel", () => {
918 const hosted = Schema.Union([
919 Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("x") }),
920 Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("y") })
921 ])
922 const flat = Schema.Struct({ kind: Schema.Literal("b") })
923 const index = SchemaAST.getCandidateIndex(Schema.Union([hosted, flat]).ast.types)
924 deepStrictEqual(index({ kind: "a" }, false), [0])
925 deepStrictEqual(index({ kind: "b" }, false), [1])
926 })
927
928 it("should exclude members whose sentinel the input contradicts", () => {
929 const schema = Schema.Union([
930 Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("x"), value: Schema.String }),
931 Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("y"), value: Schema.Number })
932 ])
933 const index = SchemaAST.getCandidateIndex(schema.ast.types)
934 deepStrictEqual(index({ kind: "a", variant: "x" }, false), [0])
935 deepStrictEqual(index({ kind: "a", variant: "z" }, false), [])
936 deepStrictEqual(index({ kind: "a", variant: undefined }, false), [])
937 // A missing sentinel key does not exclude: the member still owes the error.
938 deepStrictEqual(index({ kind: "a" }, false), [0, 1])
939 deepStrictEqual(index({ kind: "a", variant: undefined }, true), [0, 1])
940 })
941 })
942
943 describe("getIndexSignatureKeys", () => {
944 it("String", () => {
945 const sym = Symbol.for("sym")
946 const input: { readonly [x: PropertyKey]: number } = { a: 1, b: 2, [sym]: 3 }
947 deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.String.ast, SchemaAST.defaultParseOptions), [
948 "a",
949 "b"
950 ])
951 })
952
953 it("String with checks", () => {
954 const input = { a: 1, ab: 2, b: 3 }
955 deepStrictEqual(
956 SchemaAST.getIndexSignatureKeys(
957 input,
958 Schema.String.check(Schema.isPattern(/^a/)).ast,
959 SchemaAST.defaultParseOptions
960 ),
961 ["a", "ab"]
962 )
963 })
964
965 it("transformed String with decoded checks", () => {
966 const schema = Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())).check(
967 Schema.isPattern(/^aB$/)
968 )
969 const input = { a_b: 1, x_y: 2 }
970 deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, schema.ast, SchemaAST.defaultParseOptions), [
971 "a_b",
972 "x_y"
973 ])
974 })
975
976 it("TemplateLiteral", () => {
977 const schema = Schema.TemplateLiteral(["a"])
978 const input = { a: 1, ab: 2, b: 3 }
979 deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, schema.ast, SchemaAST.defaultParseOptions), ["a"])
980 })
981
982 it("TemplateLiteral with checked parts", () => {
983 const schema = Schema.TemplateLiteral(["a", Schema.NonEmptyString])
984 const input = { a: 1, ab: 2, b: 3 }
985 deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, schema.ast, SchemaAST.defaultParseOptions), ["ab"])
986 })
987
988 it("Symbol", () => {
989 const a = Symbol.for("a")
990 const b = Symbol.for("b")
991 const input: { readonly [x: PropertyKey]: number } = { c: 1, [a]: 2, [b]: 3 }
992 deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.Symbol.ast, SchemaAST.defaultParseOptions), [a, b])
993 })
994
995 it("Number", () => {
996 const input = { "1": 1, "1.5": 2, "-2": 3, a: 4, NaN: 5, x1: 6, "1x": 7 }
997 deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.Number.ast, SchemaAST.defaultParseOptions), [
998 "1",
999 "1.5",
1000 "-2",
1001 "NaN"
1002 ])
1003 })
1004
1005 it("Number with checks", () => {
1006 const input = { "1": 1, "1.5": 2, "-2": 3, a: 4, NaN: 5 }
1007 deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.Int.ast, SchemaAST.defaultParseOptions), [
1008 "1",
1009 "-2"
1010 ])
1011 })
1012
1013 it("Union", () => {
1014 const schema = Schema.Union([Schema.Symbol, Schema.Number])
1015 const sym = Symbol.for("sym")
1016 const input: { readonly [x: PropertyKey]: number } = { "1": 1, b: 2, [sym]: 3 }
1017 deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, schema.ast, SchemaAST.defaultParseOptions), [sym, "1"])
1018 })
1019 })
1020
1021 describe("record", () => {
1022 it("treats Never parameters as no keys", () => {
1023 const ast = SchemaAST.record(Schema.Never.ast, Schema.Number.ast)
1024 deepStrictEqual(ast.propertySignatures, [])
1025 deepStrictEqual(ast.indexSignatures, [])
1026 })
1027
1028 it("ignores Never arms in union parameters", () => {
1029 const ast = SchemaAST.record(Schema.Union([Schema.String, Schema.Never]).ast, Schema.Number.ast)
1030 const indexSignature = ast.indexSignatures[0]!
1031
1032 deepStrictEqual(ast.propertySignatures, [])
1033 strictEqual(ast.indexSignatures.length, 1)
1034 strictEqual(indexSignature.parameter, Schema.String.ast)
1035 strictEqual(indexSignature.type, Schema.Number.ast)
1036 })
1037 })
1038
1039 describe("Union options", () => {
1040 it("preserves the complete options object through recur and flip", () => {
1041 const unionOptions: SchemaAST.UnionOptions = { mode: "oneOf" }
1042 const union = new SchemaAST.Union([Schema.NumberFromString.ast, Schema.String.ast], unionOptions)
1043 strictEqual(union.recur(SchemaAST.toEncoded).options, unionOptions)
1044 strictEqual(union.flip(SchemaAST.flip).options, unionOptions)
1045 })
1046 })
1047
1048 describe("IndexSignature", () => {
1049 it("accepts valid parameters on both type and encoded side", () => {
1050 doesNotThrow(() => new SchemaAST.IndexSignature(Schema.String.ast, Schema.Number.ast))
1051 doesNotThrow(() => new SchemaAST.IndexSignature(Schema.NumberFromString.ast, Schema.Number.ast))
1052 doesNotThrow(() =>
1053 new SchemaAST.IndexSignature(
1054 Schema.Union([Schema.String, Schema.NumberFromString]).ast,
1055 Schema.Number.ast
1056 )
1057 )
1058 })
1059
1060 it("rejects invalid type side parameters", () => {
1061 throws(
1062 () => new SchemaAST.IndexSignature(Schema.Literal("a").ast, Schema.Number.ast),
1063 new Error("Invalid index signature parameter Literal")
1064 )
1065 })
1066
1067 it("rejects invalid encoded side parameters", () => {
1068 const StringFromBoolean = Schema.Boolean.pipe(
1069 Schema.decodeTo(Schema.String, {
1070 decode: SchemaGetter.transform((b: boolean) => globalThis.String(b)),
1071 encode: SchemaGetter.transform((s: string) => s === "true")
1072 })
1073 )
1074 throws(
1075 () => new SchemaAST.IndexSignature(StringFromBoolean.ast, Schema.Number.ast),
1076 new Error("Invalid index signature parameter String")
1077 )
1078 throws(
1079 () =>
1080 new SchemaAST.IndexSignature(
1081 Schema.Union([Schema.String, StringFromBoolean]).ast,
1082 Schema.Number.ast
1083 ),
1084 new Error("Invalid index signature parameter Union")
1085 )
1086
1087 const UnionFromBoolean = Schema.Boolean.pipe(
1088 Schema.decodeTo(Schema.Union([Schema.String, Schema.Number]), {
1089 decode: SchemaGetter.transform((b: boolean): string | number => b ? "true" : 0),
1090 encode: SchemaGetter.transform((_value: string | number) => true)
1091 })
1092 )
1093 throws(
1094 () => new SchemaAST.IndexSignature(UnionFromBoolean.ast, Schema.Number.ast),
1095 new Error("Invalid index signature parameter Union")
1096 )
1097 })
1098 })
1099})