From 6a4947b364465138190ce08f45366bd43907d369 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Mon, 13 Jan 2025 17:53:52 -0500 Subject: [PATCH 1/9] Change object and optional to support exact optional props --- library/src/schemas/object/object.ts | 105 +++++++++++------- library/src/schemas/optional/index.ts | 1 - library/src/schemas/optional/optional.ts | 28 ++--- library/src/schemas/optional/optionalAsync.ts | 28 ++--- library/src/schemas/optional/types.ts | 20 ---- library/src/types/config.ts | 4 +- library/src/types/object.ts | 33 +----- 7 files changed, 86 insertions(+), 133 deletions(-) delete mode 100644 library/src/schemas/optional/types.ts diff --git a/library/src/schemas/object/object.ts b/library/src/schemas/object/object.ts index c632752d5..b56ae01d0 100644 --- a/library/src/schemas/object/object.ts +++ b/library/src/schemas/object/object.ts @@ -110,53 +110,80 @@ export function object( // The reason for this decision is that it reduces the bundle size, and // we also expect that most users will expect this behavior. for (const key in this.entries) { - // Get and parse value of key + // Get value of key const value: unknown = input[key as keyof typeof input]; - const valueDataset = this.entries[key]['~run']({ value }, config); - - // If there are issues, capture them - if (valueDataset.issues) { - // Create object path item - const pathItem: ObjectPathItem = { - type: 'object', - origin: 'value', - input: input as Record, - key, - value, - }; - - // Add modified entry dataset issues to issues - for (const issue of valueDataset.issues) { - if (issue.path) { - issue.path.unshift(pathItem); - } else { + + // If key is missing and optional, continue + if (!(key in input)) { + if (this.entries[key].type === 'optional') { + continue; + + // Otherwise, if key is missing and required, add issue + } else { + _addIssue(this, 'type', dataset, config, { + expected: `"${key}"`, + received: 'undefined', + input: value, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + value, + }, + ], + }); + } + + // Otherwise, parse value of key and continue + } else { + const valueDataset = this.entries[key]['~run']({ value }, config); + + // If there are issues, capture them + if (valueDataset.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + type: 'object', + origin: 'value', + input: input as Record, + key, + value, + }; + + // Add modified entry dataset issues to issues + for (const issue of valueDataset.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + // @ts-expect-error + issue.path = [pathItem]; + } // @ts-expect-error - issue.path = [pathItem]; + dataset.issues?.push(issue); + } + if (!dataset.issues) { + // @ts-expect-error + dataset.issues = valueDataset.issues; + } + + // If necessary, abort early + if (config.abortEarly) { + dataset.typed = false; + break; } - // @ts-expect-error - dataset.issues?.push(issue); - } - if (!dataset.issues) { - // @ts-expect-error - dataset.issues = valueDataset.issues; } - // If necessary, abort early - if (config.abortEarly) { + // If not typed, set typed to `false` + if (!valueDataset.typed) { dataset.typed = false; - break; } - } - // If not typed, set typed to `false` - if (!valueDataset.typed) { - dataset.typed = false; - } - - // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { - // @ts-expect-error - dataset.value[key] = valueDataset.value; + // Add entry to dataset if necessary + if (valueDataset.value !== undefined || key in input) { + // @ts-expect-error + dataset.value[key] = valueDataset.value; + } } } diff --git a/library/src/schemas/optional/index.ts b/library/src/schemas/optional/index.ts index e03bba2d5..48656b5a4 100644 --- a/library/src/schemas/optional/index.ts +++ b/library/src/schemas/optional/index.ts @@ -1,3 +1,2 @@ export * from './optional.ts'; export * from './optionalAsync.ts'; -export * from './types.ts'; diff --git a/library/src/schemas/optional/optional.ts b/library/src/schemas/optional/optional.ts index 076b6b580..c3261ff12 100644 --- a/library/src/schemas/optional/optional.ts +++ b/library/src/schemas/optional/optional.ts @@ -5,10 +5,9 @@ import type { Default, InferInput, InferIssue, - SuccessDataset, + InferOutput, } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; -import type { InferOptionalOutput } from './types.ts'; /** * Optional schema type. @@ -17,8 +16,8 @@ export interface OptionalSchema< TWrapped extends BaseSchema>, TDefault extends Default, > extends BaseSchema< - InferInput | undefined, - InferOptionalOutput, + InferInput, + InferOutput, InferIssue > { /** @@ -32,7 +31,7 @@ export interface OptionalSchema< /** * The expected property. */ - readonly expects: `(${TWrapped['expects']} | undefined)`; + readonly expects: TWrapped['expects']; /** * The wrapped schema. */ @@ -76,7 +75,7 @@ export function optional( kind: 'schema', type: 'optional', reference: optional, - expects: `(${wrapped.expects} | undefined)`, + expects: wrapped.expects, async: false, wrapped, default: default_, @@ -84,20 +83,9 @@ export function optional( return _getStandardProps(this); }, '~run'(dataset, config) { - // If value is `undefined`, override it with default or return dataset - if (dataset.value === undefined) { - // If default is specified, override value of dataset - if (this.default !== undefined) { - dataset.value = getDefault(this, dataset, config); - } - - // If value is still `undefined`, return dataset - if (dataset.value === undefined) { - // @ts-expect-error - dataset.typed = true; - // @ts-expect-error - return dataset as SuccessDataset; - } + // If value is `undefined` and default is specified, override value + if (dataset.value === undefined && this.default !== undefined) { + dataset.value = getDefault(this, dataset, config); } // Otherwise, return dataset of wrapped schema diff --git a/library/src/schemas/optional/optionalAsync.ts b/library/src/schemas/optional/optionalAsync.ts index 529f39c40..b261814fc 100644 --- a/library/src/schemas/optional/optionalAsync.ts +++ b/library/src/schemas/optional/optionalAsync.ts @@ -6,10 +6,9 @@ import type { DefaultAsync, InferInput, InferIssue, - SuccessDataset, + InferOutput, } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; -import type { InferOptionalOutput } from './types.ts'; /** * Optional schema async type. @@ -20,8 +19,8 @@ export interface OptionalSchemaAsync< | BaseSchemaAsync>, TDefault extends DefaultAsync, > extends BaseSchemaAsync< - InferInput | undefined, - InferOptionalOutput, + InferInput, + InferOutput, InferIssue > { /** @@ -35,7 +34,7 @@ export interface OptionalSchemaAsync< /** * The expected property. */ - readonly expects: `(${TWrapped['expects']} | undefined)`; + readonly expects: TWrapped['expects']; /** * The wrapped schema. */ @@ -92,7 +91,7 @@ export function optionalAsync( kind: 'schema', type: 'optional', reference: optionalAsync, - expects: `(${wrapped.expects} | undefined)`, + expects: wrapped.expects, async: true, wrapped, default: default_, @@ -100,20 +99,9 @@ export function optionalAsync( return _getStandardProps(this); }, async '~run'(dataset, config) { - // If value is `undefined`, override it with default or return dataset - if (dataset.value === undefined) { - // If default is specified, override value of dataset - if (this.default !== undefined) { - dataset.value = await getDefault(this, dataset, config); - } - - // If value is still `undefined`, return dataset - if (dataset.value === undefined) { - // @ts-expect-error - dataset.typed = true; - // @ts-expect-error - return dataset as SuccessDataset; - } + // If value is `undefined` and default is specified, override value + if (dataset.value === undefined && this.default !== undefined) { + dataset.value = await getDefault(this, dataset, config); } // Otherwise, return dataset of wrapped schema diff --git a/library/src/schemas/optional/types.ts b/library/src/schemas/optional/types.ts deleted file mode 100644 index 3782f9574..000000000 --- a/library/src/schemas/optional/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { - BaseIssue, - BaseSchema, - BaseSchemaAsync, - DefaultAsync, - DefaultValue, - InferOutput, -} from '../../types/index.ts'; - -/** - * Infer optional output type. - */ -export type InferOptionalOutput< - TWrapped extends - | BaseSchema> - | BaseSchemaAsync>, - TDefault extends DefaultAsync, -> = undefined extends TDefault - ? InferOutput | undefined - : InferOutput | Extract, undefined>; diff --git a/library/src/types/config.ts b/library/src/types/config.ts index 332ee7707..f4afed535 100644 --- a/library/src/types/config.ts +++ b/library/src/types/config.ts @@ -14,11 +14,11 @@ export interface Config> { */ readonly message?: ErrorMessage | undefined; /** - * Whether it was abort early. + * Whether it should be aborted early. */ readonly abortEarly?: boolean | undefined; /** - * Whether the pipe was abort early. + * Whether a pipe should be aborted early. */ readonly abortPipeEarly?: boolean | undefined; } diff --git a/library/src/types/object.ts b/library/src/types/object.ts index cfb15c629..9a4871fe7 100644 --- a/library/src/types/object.ts +++ b/library/src/types/object.ts @@ -21,7 +21,6 @@ import type { import type { InferInput, InferIssue, InferOutput } from './infer.ts'; import type { BaseIssue } from './issue.ts'; import type { ErrorMessage } from './other.ts'; -import type { SchemaWithoutPipe } from './pipe.ts'; import type { BaseSchema, BaseSchemaAsync } from './schema.ts'; import type { MarkOptional, MaybeReadonly, Prettify } from './utils.ts'; @@ -103,46 +102,18 @@ type QuestionMarkSchema = type HasDefault = undefined extends TSchema['default'] ? false : true; -/** - * Exact optional input type. - */ -type ExactOptionalInput< - TSchema extends - | BaseSchema> - | BaseSchemaAsync>, -> = TSchema extends - | OptionalSchema - | OptionalSchemaAsync - ? ExactOptionalInput - : InferInput; - -/** - * Exact optional output type. - */ -type ExactOptionalOutput< - TSchema extends - | BaseSchema> - | BaseSchemaAsync>, -> = TSchema extends - | SchemaWithoutPipe> - | SchemaWithoutPipe> - ? HasDefault extends true - ? InferOutput - : ExactOptionalOutput - : InferOutput; - /** * Infer entries input type. */ type InferEntriesInput = { - -readonly [TKey in keyof TEntries]: ExactOptionalInput; + -readonly [TKey in keyof TEntries]: InferInput; }; /** * Infer entries output type. */ type InferEntriesOutput = { - -readonly [TKey in keyof TEntries]: ExactOptionalOutput; + -readonly [TKey in keyof TEntries]: InferOutput; }; /** From 10ab761871ca539e6f79483aca8cd9353abd370e Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 14 Jan 2025 11:08:18 -0500 Subject: [PATCH 2/9] Fix default value and add support for nullish --- library/src/schemas/object/object.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/library/src/schemas/object/object.ts b/library/src/schemas/object/object.ts index b56ae01d0..323e33ebc 100644 --- a/library/src/schemas/object/object.ts +++ b/library/src/schemas/object/object.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseSchema, ErrorMessage, @@ -113,10 +114,17 @@ export function object( // Get value of key const value: unknown = input[key as keyof typeof input]; - // If key is missing and optional, continue + // If key is missing and optional, use default if available if (!(key in input)) { - if (this.entries[key].type === 'optional') { - continue; + if ( + this.entries[key].type === 'optional' || + this.entries[key].type === 'nullish' + ) { + // @ts-expect-error + if (this.entries[key].default !== undefined) { + // @ts-expect-error + dataset.value[key] = getDefault(this.entries[key]); + } // Otherwise, if key is missing and required, add issue } else { @@ -180,10 +188,8 @@ export function object( } // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { - // @ts-expect-error - dataset.value[key] = valueDataset.value; - } + // @ts-expect-error + dataset.value[key] = valueDataset.value; } } From 3eb4f554f369d963e8ca178408709021f49a0b77 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Wed, 15 Jan 2025 11:54:34 -0500 Subject: [PATCH 3/9] Change implementation of object and reduce bundle size --- library/src/schemas/object/object.ts | 75 +++++++++++++--------------- library/src/types/object.ts | 32 +++++------- 2 files changed, 48 insertions(+), 59 deletions(-) diff --git a/library/src/schemas/object/object.ts b/library/src/schemas/object/object.ts index 323e33ebc..661a0740d 100644 --- a/library/src/schemas/object/object.ts +++ b/library/src/schemas/object/object.ts @@ -107,46 +107,14 @@ export function object( dataset.value = {}; // Parse schema of each entry - // Hint: We do not distinguish between missing and `undefined` entries. - // The reason for this decision is that it reduces the bundle size, and - // we also expect that most users will expect this behavior. for (const key in this.entries) { - // Get value of key - const value: unknown = input[key as keyof typeof input]; - - // If key is missing and optional, use default if available - if (!(key in input)) { - if ( - this.entries[key].type === 'optional' || - this.entries[key].type === 'nullish' - ) { + // If key is not missing, parse value of key + if (key in input) { + const valueDataset = this.entries[key]['~run']( // @ts-expect-error - if (this.entries[key].default !== undefined) { - // @ts-expect-error - dataset.value[key] = getDefault(this.entries[key]); - } - - // Otherwise, if key is missing and required, add issue - } else { - _addIssue(this, 'type', dataset, config, { - expected: `"${key}"`, - received: 'undefined', - input: value, - path: [ - { - type: 'object', - origin: 'key', - input: input as Record, - key, - value, - }, - ], - }); - } - - // Otherwise, parse value of key and continue - } else { - const valueDataset = this.entries[key]['~run']({ value }, config); + { value: input[key] }, + config + ); // If there are issues, capture them if (valueDataset.issues) { @@ -156,7 +124,8 @@ export function object( origin: 'value', input: input as Record, key, - value, + // @ts-expect-error + value: input[key], }; // Add modified entry dataset issues to issues @@ -190,6 +159,34 @@ export function object( // Add entry to dataset if necessary // @ts-expect-error dataset.value[key] = valueDataset.value; + + // Otherwise, if key is missing and optional, use default value + // if available + } else { + if (this.entries[key].type === 'optional') { + // @ts-expect-error + if (this.entries[key].default !== undefined) { + // @ts-expect-error + dataset.value[key] = getDefault(this.entries[key]); + } + + // Otherwise, if key is missing and required, add issue + } else { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + // @ts-expect-error + value: input[key], + }, + ], + }); + } } } diff --git a/library/src/types/object.ts b/library/src/types/object.ts index 9a4871fe7..1eb179177 100644 --- a/library/src/types/object.ts +++ b/library/src/types/object.ts @@ -4,8 +4,6 @@ import type { LooseObjectIssue, LooseObjectSchema, LooseObjectSchemaAsync, - NullishSchema, - NullishSchemaAsync, ObjectIssue, ObjectSchema, ObjectSchemaAsync, @@ -28,7 +26,9 @@ import type { MarkOptional, MaybeReadonly, Prettify } from './utils.ts'; * Object entries type. */ export interface ObjectEntries { - [key: string]: BaseSchema>; + [key: string]: + | BaseSchema> + | OptionalSchema>, unknown>; } /** @@ -37,7 +37,13 @@ export interface ObjectEntries { export interface ObjectEntriesAsync { [key: string]: | BaseSchema> - | BaseSchemaAsync>; + | BaseSchemaAsync> + | OptionalSchema>, unknown> + | OptionalSchemaAsync< + | BaseSchema> + | BaseSchemaAsync>, + unknown + >; } /** @@ -83,12 +89,6 @@ export type ObjectKeys< * Question mark schema type. */ type QuestionMarkSchema = - | NullishSchema>, unknown> - | NullishSchemaAsync< - | BaseSchema> - | BaseSchemaAsync>, - unknown - > | OptionalSchema>, unknown> | OptionalSchemaAsync< | BaseSchema> @@ -96,12 +96,6 @@ type QuestionMarkSchema = unknown >; -/** - * Has default type. - */ -type HasDefault = - undefined extends TSchema['default'] ? false : true; - /** * Infer entries input type. */ @@ -130,10 +124,8 @@ type OptionalInputKeys = { */ type OptionalOutputKeys = { [TKey in keyof TEntries]: TEntries[TKey] extends QuestionMarkSchema - ? undefined extends InferOutput - ? HasDefault extends false - ? TKey - : never + ? undefined extends TEntries[TKey]['default'] + ? TKey : never : never; }[keyof TEntries]; From bc1bd6affcd6e743ebf20cc297db9e8e2b95417d Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Thu, 16 Jan 2025 11:52:26 -0500 Subject: [PATCH 4/9] Fix handling of optional props and change schemas --- library/src/schemas/object/object.ts | 62 +++++++++---------- library/src/schemas/optional/optional.ts | 7 --- library/src/schemas/optional/optionalAsync.ts | 7 --- 3 files changed, 30 insertions(+), 46 deletions(-) diff --git a/library/src/schemas/object/object.ts b/library/src/schemas/object/object.ts index 9e28f2182..7a85471e9 100644 --- a/library/src/schemas/object/object.ts +++ b/library/src/schemas/object/object.ts @@ -106,13 +106,22 @@ export function object( dataset.typed = true; dataset.value = {}; - // Parse schema of each entry + // Process each object entry of schema for (const key in this.entries) { - // If key is not missing, parse value of key - if (key in input) { - const valueDataset = this.entries[key]['~run']( + // If key is present or its an optional schema with a default value, + // parse input or default value of key + if ( + key in input || + (this.entries[key].type === 'optional' && // @ts-expect-error - { value: input[key] }, + this.entries[key].default !== undefined) + ) { + const valueDataset = this.entries[key]['~run']( + { + value: + // @ts-expect-error + key in input ? input[key] : getDefault(this.entries[key]), + }, config ); @@ -160,33 +169,22 @@ export function object( // @ts-expect-error dataset.value[key] = valueDataset.value; - // Otherwise, if key is missing and optional, use default value - // if available - } else { - if (this.entries[key].type === 'optional') { - // @ts-expect-error - if (this.entries[key].default !== undefined) { - // @ts-expect-error - dataset.value[key] = getDefault(this.entries[key]); - } - - // Otherwise, if key is missing and required, add issue - } else { - _addIssue(this, 'key', dataset, config, { - input: undefined, - expected: `"${key}"`, - path: [ - { - type: 'object', - origin: 'key', - input: input as Record, - key, - // @ts-expect-error - value: input[key], - }, - ], - }); - } + // Otherwise, if key is missing and required, add issue + } else if (this.entries[key].type !== 'optional') { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + // @ts-expect-error + value: input[key], + }, + ], + }); } } diff --git a/library/src/schemas/optional/optional.ts b/library/src/schemas/optional/optional.ts index c85227560..0bab7f17b 100644 --- a/library/src/schemas/optional/optional.ts +++ b/library/src/schemas/optional/optional.ts @@ -1,4 +1,3 @@ -import { getDefault } from '../../methods/index.ts'; import type { BaseIssue, BaseSchema, @@ -83,12 +82,6 @@ export function optional( return _getStandardProps(this); }, '~run'(dataset, config) { - // If value is `undefined` and default is specified, override value - if (dataset.value === undefined && this.default !== undefined) { - dataset.value = getDefault(this, dataset, config); - } - - // Otherwise, return dataset of wrapped schema return this.wrapped['~run'](dataset, config); }, }; diff --git a/library/src/schemas/optional/optionalAsync.ts b/library/src/schemas/optional/optionalAsync.ts index 7ce41774c..c8ca2d856 100644 --- a/library/src/schemas/optional/optionalAsync.ts +++ b/library/src/schemas/optional/optionalAsync.ts @@ -1,4 +1,3 @@ -import { getDefault } from '../../methods/index.ts'; import type { BaseIssue, BaseSchema, @@ -99,12 +98,6 @@ export function optionalAsync( return _getStandardProps(this); }, async '~run'(dataset, config) { - // If value is `undefined` and default is specified, override value - if (dataset.value === undefined && this.default !== undefined) { - dataset.value = await getDefault(this, dataset, config); - } - - // Otherwise, return dataset of wrapped schema return this.wrapped['~run'](dataset, config); }, }; From fe4668b2c0006332df9f01f4967bedec4254e472 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Thu, 16 Jan 2025 22:41:49 -0500 Subject: [PATCH 5/9] Finalize new object implementation and update tests --- library/src/schemas/object/object.test-d.ts | 10 +- library/src/schemas/object/object.test.ts | 379 +++++++++++++++-- library/src/schemas/object/object.ts | 24 +- .../src/schemas/object/objectAsync.test-d.ts | 19 +- .../src/schemas/object/objectAsync.test.ts | 382 ++++++++++++++++-- library/src/schemas/object/objectAsync.ts | 121 ++++-- library/src/schemas/object/types.ts | 2 +- library/src/schemas/optional/optional.ts | 4 +- library/src/schemas/optional/optionalAsync.ts | 4 +- 9 files changed, 787 insertions(+), 158 deletions(-) diff --git a/library/src/schemas/object/object.test-d.ts b/library/src/schemas/object/object.test-d.ts index 5dd55beb2..ddbaf7e56 100644 --- a/library/src/schemas/object/object.test-d.ts +++ b/library/src/schemas/object/object.test-d.ts @@ -49,8 +49,8 @@ describe('object', () => { key6: UndefinedableSchema, 'bar'>; key7: SchemaWithPipe< [ - OptionalSchema, undefined>, - TransformAction, + OptionalSchema, () => 'foo'>, + TransformAction, ] >; }, @@ -61,7 +61,7 @@ describe('object', () => { expectTypeOf>().toEqualTypeOf<{ key1: string; key2?: string; - key3?: string | null | undefined; + key3: string | null | undefined; key4: { key: number }; key5: string; key6: string | undefined; @@ -73,11 +73,11 @@ describe('object', () => { expectTypeOf>().toEqualTypeOf<{ key1: string; key2: string; - key3?: string | null | undefined; + key3: string | null | undefined; key4: { key: number }; readonly key5: string; key6: string; - key7: string; + key7: number; }>(); }); diff --git a/library/src/schemas/object/object.test.ts b/library/src/schemas/object/object.test.ts index 0f7eeeffc..47affc64a 100644 --- a/library/src/schemas/object/object.test.ts +++ b/library/src/schemas/object/object.test.ts @@ -4,7 +4,9 @@ import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; import { nullish } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { optional } from '../optional/index.ts'; -import { string, type StringIssue } from '../string/index.ts'; +import { string } from '../string/index.ts'; +import { undefined_ } from '../undefined/index.ts'; +import { union } from '../union/index.ts'; import { object, type ObjectSchema } from './object.ts'; import type { ObjectIssue } from './types.ts'; @@ -145,15 +147,29 @@ describe('object', () => { test('for optional entry', () => { expectNoSchemaIssue(object({ key: optional(string()) }), [ {}, - // @ts-expect-error - { key: undefined }, { key: 'foo' }, ]); }); + test('for optional entry with default', () => { + expect( + object({ key: optional(string(), 'foo') })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + object({ + key: optional(union([string(), undefined_()]), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); + test('for nullish entry', () => { expectNoSchemaIssue(object({ key: nullish(number()) }), [ - {}, { key: undefined }, { key: null }, { key: 123 }, @@ -174,7 +190,11 @@ describe('object', () => { }); describe('should return dataset with nested issues', () => { - const schema = object({ key: string(), nested: object({ key: number() }) }); + const schema = object({ + key1: string(), + key2: number(), + nested: object({ key1: string(), key2: number() }), + }); const baseInfo = { message: expect.any(String), @@ -185,42 +205,41 @@ describe('object', () => { abortPipeEarly: undefined, }; - const stringIssue: StringIssue = { - ...baseInfo, - kind: 'schema', - type: 'string', - input: undefined, - expected: 'string', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'value', - input: {}, - key: 'key', - value: undefined, - }, - ], - }; - test('for missing entries', () => { - expect(schema['~run']({ value: {} }, {})).toStrictEqual({ + const input = { key2: 123 }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: {}, + value: input, issues: [ - stringIssue, { ...baseInfo, kind: 'schema', type: 'object', input: undefined, - expected: 'Object', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', - input: {}, + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"nested"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, key: 'nested', value: undefined, }, @@ -231,31 +250,285 @@ describe('object', () => { }); test('for missing nested entries', () => { + const input = { key1: 'value', nested: {} }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: {}, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: {}, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing entries with abort early', () => { + const input = { key2: 123 }; expect( - schema['~run']({ value: { key: 'value', nested: {} } }, {}) + schema['~run']({ value: input }, { abortEarly: true }) ).toStrictEqual({ typed: false, - value: { key: 'value', nested: {} }, + value: {}, issues: [ { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object', input: undefined, - expected: 'number', + expected: '"key1"', received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries', () => { + const input = { key1: false, key2: 123, nested: null }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: null, + expected: 'Object', + received: 'null', path: [ { type: 'object', origin: 'value', - input: { key: 'value', nested: {} }, + input, key: 'nested', - value: {}, + value: null, }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid nested entries', () => { + const input = { + key1: 'value', + key2: 'value', + nested: { + key1: 123, + key2: null, + }, + }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: 'value', + expected: 'number', + received: '"value"', + path: [ { type: 'object', origin: 'value', - input: {}, + input, + key: 'key2', + value: input.key2, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: 123, + expected: 'string', + received: '123', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key1', + value: input.nested.key1, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: null, + expected: 'number', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key2', + value: input.nested.key2, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries with abort early', () => { + const input = { key1: false, key2: 123, nested: null }; + expect( + schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for undefined optional entry', () => { + const schema = object({ key: optional(string()) }); + const input = { key: undefined }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, key: 'key', value: undefined, }, @@ -265,14 +538,32 @@ describe('object', () => { } satisfies FailureDataset>); }); - test('with abort early', () => { - expect(schema['~run']({ value: {} }, { abortEarly: true })).toStrictEqual( - { - typed: false, - value: {}, - issues: [{ ...stringIssue, abortEarly: true }], - } satisfies FailureDataset> - ); + test('for missing nullish entry', () => { + const schema = object({ key: nullish(string()) }); + const input = {}; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); }); }); }); diff --git a/library/src/schemas/object/object.ts b/library/src/schemas/object/object.ts index 7a85471e9..d50811a8b 100644 --- a/library/src/schemas/object/object.ts +++ b/library/src/schemas/object/object.ts @@ -116,14 +116,12 @@ export function object( // @ts-expect-error this.entries[key].default !== undefined) ) { - const valueDataset = this.entries[key]['~run']( - { - value: - // @ts-expect-error - key in input ? input[key] : getDefault(this.entries[key]), - }, - config - ); + const value: unknown = + key in input + ? // @ts-expect-error + input[key] + : getDefault(this.entries[key]); + const valueDataset = this.entries[key]['~run']({ value }, config); // If there are issues, capture them if (valueDataset.issues) { @@ -133,8 +131,7 @@ export function object( origin: 'value', input: input as Record, key, - // @ts-expect-error - value: input[key], + value, }; // Add modified entry dataset issues to issues @@ -165,7 +162,7 @@ export function object( dataset.typed = false; } - // Add entry to dataset if necessary + // Add entry to dataset // @ts-expect-error dataset.value[key] = valueDataset.value; @@ -185,6 +182,11 @@ export function object( }, ], }); + + // If necessary, abort early + if (config.abortEarly) { + break; + } } } diff --git a/library/src/schemas/object/objectAsync.test-d.ts b/library/src/schemas/object/objectAsync.test-d.ts index 15e5687d5..c9757e693 100644 --- a/library/src/schemas/object/objectAsync.test-d.ts +++ b/library/src/schemas/object/objectAsync.test-d.ts @@ -1,10 +1,13 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { ReadonlyAction, TransformAction } from '../../actions/index.ts'; -import type { SchemaWithPipe } from '../../methods/index.ts'; +import type { + SchemaWithPipe, + SchemaWithPipeAsync, +} from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; import type { NullishSchema } from '../nullish/index.ts'; import type { NumberIssue, NumberSchema } from '../number/index.ts'; -import type { OptionalSchema } from '../optional/index.ts'; +import type { OptionalSchema, OptionalSchemaAsync } from '../optional/index.ts'; import { string, type StringIssue, @@ -47,10 +50,10 @@ describe('objectAsync', () => { key4: ObjectSchemaAsync<{ key: NumberSchema }, undefined>; key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipe< + key7: SchemaWithPipeAsync< [ - OptionalSchema, undefined>, - TransformAction, + OptionalSchemaAsync, () => Promise<'foo'>>, + TransformAction, ] >; }, @@ -61,7 +64,7 @@ describe('objectAsync', () => { expectTypeOf>().toEqualTypeOf<{ key1: string; key2?: string; - key3?: string | null | undefined; + key3: string | null | undefined; key4: { key: number }; key5: string; key6: string | undefined; @@ -73,11 +76,11 @@ describe('objectAsync', () => { expectTypeOf>().toEqualTypeOf<{ key1: string; key2: string; - key3?: string | null | undefined; + key3: string | null | undefined; key4: { key: number }; readonly key5: string; key6: string; - key7: string; + key7: number; }>(); }); diff --git a/library/src/schemas/object/objectAsync.test.ts b/library/src/schemas/object/objectAsync.test.ts index c23979373..537e16ad4 100644 --- a/library/src/schemas/object/objectAsync.test.ts +++ b/library/src/schemas/object/objectAsync.test.ts @@ -4,10 +4,12 @@ import { expectNoSchemaIssueAsync, expectSchemaIssueAsync, } from '../../vitest/index.ts'; -import { nullish } from '../nullish/index.ts'; +import { nullish, nullishAsync } from '../nullish/index.ts'; import { number } from '../number/index.ts'; -import { optional } from '../optional/index.ts'; -import { string, type StringIssue } from '../string/index.ts'; +import { optional, optionalAsync } from '../optional/index.ts'; +import { string } from '../string/index.ts'; +import { undefined_ } from '../undefined/index.ts'; +import { unionAsync } from '../union/index.ts'; import { objectAsync, type ObjectSchemaAsync } from './objectAsync.ts'; import type { ObjectIssue } from './types.ts'; @@ -158,15 +160,35 @@ describe('objectAsync', () => { test('for optional entry', async () => { await expectNoSchemaIssueAsync(objectAsync({ key: optional(string()) }), [ {}, - // @ts-expect-error - { key: undefined }, { key: 'foo' }, ]); }); + test('for optional entry with default', async () => { + expect( + await objectAsync({ key: optional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ + key: optionalAsync( + unionAsync([string(), undefined_()]), + async () => undefined + ), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); + test('for nullish entry', async () => { await expectNoSchemaIssueAsync(objectAsync({ key: nullish(number()) }), [ - {}, { key: undefined }, { key: null }, { key: 123 }, @@ -188,8 +210,9 @@ describe('objectAsync', () => { describe('should return dataset with nested issues', () => { const schema = objectAsync({ - key: string(), - nested: objectAsync({ key: number() }), + key1: string(), + key2: number(), + nested: objectAsync({ key1: string(), key2: number() }), }); const baseInfo = { @@ -201,42 +224,41 @@ describe('objectAsync', () => { abortPipeEarly: undefined, }; - const stringIssue: StringIssue = { - ...baseInfo, - kind: 'schema', - type: 'string', - input: undefined, - expected: 'string', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'value', - input: {}, - key: 'key', - value: undefined, - }, - ], - }; - test('for missing entries', async () => { - expect(await schema['~run']({ value: {} }, {})).toStrictEqual({ + const input = { key2: 123 }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: {}, + value: input, issues: [ - stringIssue, { ...baseInfo, kind: 'schema', type: 'object', input: undefined, - expected: 'Object', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', - input: {}, + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"nested"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, key: 'nested', value: undefined, }, @@ -247,47 +269,319 @@ describe('objectAsync', () => { }); test('for missing nested entries', async () => { - expect( - await schema['~run']({ value: { key: 'value', nested: {} } }, {}) - ).toStrictEqual({ + const input = { key1: 'value', nested: {} }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: { key: 'value', nested: {} }, + value: input, issues: [ { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object', input: undefined, - expected: 'number', + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', received: 'undefined', path: [ { type: 'object', origin: 'value', - input: { key: 'value', nested: {} }, + input, key: 'nested', value: {}, }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ { type: 'object', origin: 'value', - input: {}, - key: 'key', + input, + key: 'nested', + value: {}, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing entries with abort early', async () => { + const input = { key2: 123 }; + expect( + await schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', value: undefined, }, ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries', async () => { + const input = { key1: false, key2: 123, nested: null }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: null, + expected: 'Object', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: null, + }, + ], }, ], } satisfies FailureDataset>); }); - test('with abort early', async () => { + test('for invalid nested entries', async () => { + const input = { + key1: 'value', + key2: 'value', + nested: { + key1: 123, + key2: null, + }, + }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: 'value', + expected: 'number', + received: '"value"', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: input.key2, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: 123, + expected: 'string', + received: '123', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key1', + value: input.nested.key1, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: null, + expected: 'number', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key2', + value: input.nested.key2, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries with abort early', async () => { + const input = { key1: false, key2: 123, nested: null }; expect( - await schema['~run']({ value: {} }, { abortEarly: true }) + await schema['~run']({ value: input }, { abortEarly: true }) ).toStrictEqual({ typed: false, value: {}, - issues: [{ ...stringIssue, abortEarly: true }], + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for undefined optional entry', async () => { + const schema = objectAsync({ key: optionalAsync(string()) }); + const input = { key: undefined }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing nullish entry', async () => { + const schema = objectAsync({ key: nullishAsync(string()) }); + const input = {}; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key', + value: undefined, + }, + ], + }, + ], } satisfies FailureDataset>); }); }); diff --git a/library/src/schemas/object/objectAsync.ts b/library/src/schemas/object/objectAsync.ts index 9ef088eda..68be4f394 100644 --- a/library/src/schemas/object/objectAsync.ts +++ b/library/src/schemas/object/objectAsync.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseSchemaAsync, ErrorMessage, @@ -108,66 +109,104 @@ export function objectAsync( dataset.typed = true; dataset.value = {}; - // Parse schema of each entry - // Hint: We do not distinguish between missing and `undefined` entries. - // The reason for this decision is that it reduces the bundle size, and - // we also expect that most users will expect this behavior. + // If key is present or its an optional schema with a default value, + // parse input or default value of key asynchronously const valueDatasets = await Promise.all( Object.entries(this.entries).map(async ([key, schema]) => { - const value = input[key as keyof typeof input]; + if ( + key in input || + (this.entries[key].type === 'optional' && + // @ts-expect-error + this.entries[key].default !== undefined) + ) { + const value: unknown = + key in input + ? // @ts-expect-error + input[key] + : await getDefault(this.entries[key]); + return [ + key, + value, + await schema['~run']({ value }, config), + ] as const; + } return [ key, - value, - await schema['~run']({ value }, config), + // @ts-expect-error + input[key] as unknown, + null, ] as const; }) ); - // Process each value dataset + // Process each object entry of schema for (const [key, value, valueDataset] of valueDatasets) { - // If there are issues, capture them - if (valueDataset.issues) { - // Create object path item - const pathItem: ObjectPathItem = { - type: 'object', - origin: 'value', - input: input as Record, - key, - value, - }; + // If key is present or its an optional schema with a default value, + // process its value dataset + if (valueDataset) { + // If there are issues, capture them + if (valueDataset.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + type: 'object', + origin: 'value', + input: input as Record, + key, + value, + }; - // Add modified entry dataset issues to issues - for (const issue of valueDataset.issues) { - if (issue.path) { - issue.path.unshift(pathItem); - } else { + // Add modified entry dataset issues to issues + for (const issue of valueDataset.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + // @ts-expect-error + issue.path = [pathItem]; + } // @ts-expect-error - issue.path = [pathItem]; + dataset.issues?.push(issue); + } + if (!dataset.issues) { + // @ts-expect-error + dataset.issues = valueDataset.issues; + } + + // If necessary, abort early + if (config.abortEarly) { + dataset.typed = false; + break; } - // @ts-expect-error - dataset.issues?.push(issue); - } - if (!dataset.issues) { - // @ts-expect-error - dataset.issues = valueDataset.issues; } - // If necessary, abort early - if (config.abortEarly) { + // If not typed, set typed to `false` + if (!valueDataset.typed) { dataset.typed = false; - break; } - } - // If not typed, set typed to `false` - if (!valueDataset.typed) { - dataset.typed = false; - } - - // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { + // Add entry to dataset // @ts-expect-error dataset.value[key] = valueDataset.value; + + // Otherwise, if key is missing and required, add issue + } else if (this.entries[key].type !== 'optional') { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + value, + }, + ], + }); + + // If necessary, abort early + if (config.abortEarly) { + break; + } } } diff --git a/library/src/schemas/object/types.ts b/library/src/schemas/object/types.ts index 24f7a45b5..54cf1acfa 100644 --- a/library/src/schemas/object/types.ts +++ b/library/src/schemas/object/types.ts @@ -15,5 +15,5 @@ export interface ObjectIssue extends BaseIssue { /** * The expected property. */ - readonly expected: 'Object'; + readonly expected: 'Object' | `"${string}"`; } diff --git a/library/src/schemas/optional/optional.ts b/library/src/schemas/optional/optional.ts index 0bab7f17b..95c518d6f 100644 --- a/library/src/schemas/optional/optional.ts +++ b/library/src/schemas/optional/optional.ts @@ -13,7 +13,7 @@ import { _getStandardProps } from '../../utils/index.ts'; */ export interface OptionalSchema< TWrapped extends BaseSchema>, - TDefault extends Default, + TDefault extends Default, > extends BaseSchema< InferInput, InferOutput, @@ -62,7 +62,7 @@ export function optional< */ export function optional< const TWrapped extends BaseSchema>, - const TDefault extends Default, + const TDefault extends Default, >(wrapped: TWrapped, default_: TDefault): OptionalSchema; // @__NO_SIDE_EFFECTS__ diff --git a/library/src/schemas/optional/optionalAsync.ts b/library/src/schemas/optional/optionalAsync.ts index c8ca2d856..78c955822 100644 --- a/library/src/schemas/optional/optionalAsync.ts +++ b/library/src/schemas/optional/optionalAsync.ts @@ -16,7 +16,7 @@ export interface OptionalSchemaAsync< TWrapped extends | BaseSchema> | BaseSchemaAsync>, - TDefault extends DefaultAsync, + TDefault extends DefaultAsync, > extends BaseSchemaAsync< InferInput, InferOutput, @@ -69,7 +69,7 @@ export function optionalAsync< const TWrapped extends | BaseSchema> | BaseSchemaAsync>, - const TDefault extends DefaultAsync, + const TDefault extends DefaultAsync, >( wrapped: TWrapped, default_: TDefault From cfa29693c7115d47ff31cd8b4545468f1573c445 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 21 Jan 2025 13:14:02 -0500 Subject: [PATCH 6/9] Add exactOptional and follow idea of @xcfox in #1022 --- library/src/methods/fallback/fallback.ts | 5 +- library/src/methods/fallback/fallbackAsync.ts | 5 +- .../methods/getDefault/getDefault.test-d.ts | 129 ++++ .../src/methods/getDefault/getDefault.test.ts | 91 +++ library/src/methods/getDefault/getDefault.ts | 102 ++- library/src/methods/omit/omit.test.ts | 17 +- library/src/methods/partial/partial.test.ts | 12 +- .../src/methods/partial/partialAsync.test.ts | 12 +- library/src/methods/pick/pick.test.ts | 18 +- library/src/methods/required/required.test.ts | 297 +++++++- .../methods/required/requiredAsync.test.ts | 297 +++++++- library/src/methods/unwrap/unwrap.test-d.ts | 42 ++ library/src/methods/unwrap/unwrap.test.ts | 34 + library/src/methods/unwrap/unwrap.ts | 11 + library/src/schemas/array/arrayAsync.ts | 3 +- library/src/schemas/custom/customAsync.ts | 3 +- .../exactOptional/exactOptional.test-d.ts | 65 ++ .../exactOptional/exactOptional.test.ts | 106 +++ .../schemas/exactOptional/exactOptional.ts | 94 +++ .../exactOptionalAsync.test-d.ts | 91 +++ .../exactOptional/exactOptionalAsync.test.ts | 132 ++++ .../exactOptional/exactOptionalAsync.ts | 105 +++ library/src/schemas/exactOptional/index.ts | 2 + library/src/schemas/index.ts | 1 + .../src/schemas/intersect/intersectAsync.ts | 3 +- library/src/schemas/lazy/lazyAsync.ts | 3 +- .../schemas/looseObject/looseObject.test-d.ts | 108 ++- .../schemas/looseObject/looseObject.test.ts | 477 +++++++++++-- .../src/schemas/looseObject/looseObject.ts | 122 ++-- .../looseObject/looseObjectAsync.test-d.ts | 228 +++++- .../looseObject/looseObjectAsync.test.ts | 641 +++++++++++++++-- .../schemas/looseObject/looseObjectAsync.ts | 138 ++-- library/src/schemas/looseObject/types.ts | 2 +- .../src/schemas/looseTuple/looseTupleAsync.ts | 3 +- library/src/schemas/map/mapAsync.ts | 3 +- .../schemas/nonNullable/nonNullableAsync.ts | 3 +- .../src/schemas/nonNullish/nonNullishAsync.ts | 3 +- .../schemas/nonOptional/nonOptionalAsync.ts | 3 +- library/src/schemas/nullable/nullable.test.ts | 26 +- .../schemas/nullable/nullableAsync.test.ts | 29 +- library/src/schemas/nullable/nullableAsync.ts | 3 +- library/src/schemas/nullish/nullish.test.ts | 22 +- .../src/schemas/nullish/nullishAsync.test.ts | 25 +- library/src/schemas/nullish/nullishAsync.ts | 3 +- library/src/schemas/object/object.test-d.ts | 110 ++- library/src/schemas/object/object.test.ts | 164 ++++- library/src/schemas/object/object.ts | 20 +- .../src/schemas/object/objectAsync.test-d.ts | 221 +++++- .../src/schemas/object/objectAsync.test.ts | 317 ++++++++- library/src/schemas/object/objectAsync.ts | 27 +- .../objectWithRest/objectWithRest.test-d.ts | 108 ++- .../objectWithRest/objectWithRest.test.ts | 506 +++++++++++-- .../schemas/objectWithRest/objectWithRest.ts | 132 ++-- .../objectWithRestAsync.test-d.ts | 228 +++++- .../objectWithRestAsync.test.ts | 672 ++++++++++++++++-- .../objectWithRest/objectWithRestAsync.ts | 141 ++-- library/src/schemas/objectWithRest/types.ts | 2 +- library/src/schemas/optional/index.ts | 1 + library/src/schemas/optional/optional.test.ts | 26 +- library/src/schemas/optional/optional.ts | 41 +- .../schemas/optional/optionalAsync.test.ts | 29 +- library/src/schemas/optional/optionalAsync.ts | 36 +- library/src/schemas/optional/types.ts | 20 + library/src/schemas/record/recordAsync.ts | 3 +- library/src/schemas/set/setAsync.ts | 3 +- .../strictObject/strictObject.test-d.ts | 108 ++- .../schemas/strictObject/strictObject.test.ts | 493 +++++++++++-- .../src/schemas/strictObject/strictObject.ts | 132 ++-- .../strictObject/strictObjectAsync.test-d.ts | 228 +++++- .../strictObject/strictObjectAsync.test.ts | 651 +++++++++++++++-- .../schemas/strictObject/strictObjectAsync.ts | 148 ++-- library/src/schemas/strictObject/types.ts | 2 +- .../src/schemas/strictTuple/strictTuple.ts | 5 +- .../schemas/strictTuple/strictTupleAsync.ts | 8 +- library/src/schemas/tuple/tupleAsync.ts | 3 +- .../tupleWithRest/tupleWithRestAsync.ts | 3 +- .../undefinedable/undefinedable.test.ts | 26 +- .../schemas/undefinedable/undefinedable.ts | 8 +- .../undefinedable/undefinedableAsync.test.ts | 29 +- .../undefinedable/undefinedableAsync.ts | 3 +- library/src/schemas/union/unionAsync.ts | 3 +- library/src/schemas/variant/types.ts | 2 +- library/src/schemas/variant/variantAsync.ts | 3 +- library/src/types/issue.ts | 29 + library/src/types/object.ts | 64 +- 85 files changed, 7227 insertions(+), 1047 deletions(-) create mode 100644 library/src/schemas/exactOptional/exactOptional.test-d.ts create mode 100644 library/src/schemas/exactOptional/exactOptional.test.ts create mode 100644 library/src/schemas/exactOptional/exactOptional.ts create mode 100644 library/src/schemas/exactOptional/exactOptionalAsync.test-d.ts create mode 100644 library/src/schemas/exactOptional/exactOptionalAsync.test.ts create mode 100644 library/src/schemas/exactOptional/exactOptionalAsync.ts create mode 100644 library/src/schemas/exactOptional/index.ts create mode 100644 library/src/schemas/optional/types.ts diff --git a/library/src/methods/fallback/fallback.ts b/library/src/methods/fallback/fallback.ts index ea559dec3..a57689fb3 100644 --- a/library/src/methods/fallback/fallback.ts +++ b/library/src/methods/fallback/fallback.ts @@ -4,6 +4,7 @@ import type { Config, InferIssue, InferOutput, + MaybeReadonly, OutputDataset, } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; @@ -15,11 +16,11 @@ import { getFallback } from '../getFallback/index.ts'; export type Fallback< TSchema extends BaseSchema>, > = - | InferOutput + | MaybeReadonly> | (( dataset?: OutputDataset, InferIssue>, config?: Config> - ) => InferOutput); + ) => MaybeReadonly>); /** * Schema with fallback type. diff --git a/library/src/methods/fallback/fallbackAsync.ts b/library/src/methods/fallback/fallbackAsync.ts index 40e3b4e9c..b8f4cc686 100644 --- a/library/src/methods/fallback/fallbackAsync.ts +++ b/library/src/methods/fallback/fallbackAsync.ts @@ -7,6 +7,7 @@ import type { InferIssue, InferOutput, MaybePromise, + MaybeReadonly, OutputDataset, StandardProps, UnknownDataset, @@ -22,11 +23,11 @@ export type FallbackAsync< | BaseSchema> | BaseSchemaAsync>, > = - | InferOutput + | MaybeReadonly> | (( dataset?: OutputDataset, InferIssue>, config?: Config> - ) => MaybePromise>); + ) => MaybePromise>>); /** * Schema with fallback async type. diff --git a/library/src/methods/getDefault/getDefault.test-d.ts b/library/src/methods/getDefault/getDefault.test-d.ts index 939f7323e..becade166 100644 --- a/library/src/methods/getDefault/getDefault.test-d.ts +++ b/library/src/methods/getDefault/getDefault.test-d.ts @@ -1,5 +1,7 @@ import { describe, expectTypeOf, test } from 'vitest'; import { + exactOptional, + exactOptionalAsync, nullable, nullableAsync, nullish, @@ -9,7 +11,10 @@ import { optional, optionalAsync, string, + undefinedable, + undefinedableAsync, } from '../../schemas/index.ts'; +import { pipe, pipeAsync } from '../pipe/index.ts'; import { getDefault } from './getDefault.ts'; describe('getDefault', () => { @@ -19,6 +24,47 @@ describe('getDefault', () => { expectTypeOf(getDefault(object({}))).toEqualTypeOf(); }); + describe('should return exact optional default', () => { + test('for undefined value', () => { + expectTypeOf( + getDefault(exactOptional(string())) + ).toEqualTypeOf(); + expectTypeOf( + getDefault(exactOptional(string(), undefined)) + ).toEqualTypeOf(); + }); + + test('for direct value', () => { + expectTypeOf( + getDefault(exactOptional(string(), 'foo')) + ).toEqualTypeOf<'foo'>(); + }); + + test('for value getter', () => { + expectTypeOf( + getDefault(exactOptional(string(), () => 'foo' as const)) + ).toEqualTypeOf<'foo'>(); + }); + + test('for async value getter', () => { + expectTypeOf( + getDefault(exactOptionalAsync(string(), async () => 'foo' as const)) + ).toEqualTypeOf>(); + }); + + test('for schema with pipe', () => { + expectTypeOf( + getDefault(pipe(exactOptional(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(exactOptional(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(exactOptionalAsync(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + }); + }); + describe('should return optional default', () => { test('for undefined value', () => { expectTypeOf(getDefault(optional(string()))).toEqualTypeOf(); @@ -50,6 +96,18 @@ describe('getDefault', () => { getDefault(optionalAsync(string(), async () => 'foo' as const)) ).toEqualTypeOf>(); }); + + test('for schema with pipe', () => { + expectTypeOf( + getDefault(pipe(optional(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(optional(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(optionalAsync(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + }); }); describe('should return nullable default', () => { @@ -84,6 +142,18 @@ describe('getDefault', () => { getDefault(nullableAsync(string(), async () => 'foo' as const)) ).toEqualTypeOf>(); }); + + test('for schema with pipe', () => { + expectTypeOf( + getDefault(pipe(nullable(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(nullable(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(nullableAsync(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + }); }); describe('should return nullish default', () => { @@ -125,5 +195,64 @@ describe('getDefault', () => { getDefault(nullishAsync(string(), async () => 'foo' as const)) ).toEqualTypeOf>(); }); + + test('for schema with pipe', () => { + expectTypeOf( + getDefault(pipe(nullish(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(nullish(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(nullishAsync(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + }); + }); + + describe('should return undefinedable default', () => { + test('for undefined value', () => { + expectTypeOf( + getDefault(undefinedable(string())) + ).toEqualTypeOf(); + expectTypeOf( + getDefault(undefinedable(string(), undefined)) + ).toEqualTypeOf(); + expectTypeOf( + getDefault(undefinedable(string(), () => undefined)) + ).toEqualTypeOf(); + expectTypeOf( + getDefault(undefinedableAsync(string(), async () => undefined)) + ).toEqualTypeOf>(); + }); + + test('for direct value', () => { + expectTypeOf( + getDefault(undefinedable(string(), 'foo')) + ).toEqualTypeOf<'foo'>(); + }); + + test('for value getter', () => { + expectTypeOf( + getDefault(undefinedable(string(), () => 'foo' as const)) + ).toEqualTypeOf<'foo'>(); + }); + + test('for async value getter', () => { + expectTypeOf( + getDefault(undefinedableAsync(string(), async () => 'foo' as const)) + ).toEqualTypeOf>(); + }); + + test('for schema with pipe', () => { + expectTypeOf( + getDefault(pipe(undefinedable(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(undefinedable(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + expectTypeOf( + getDefault(pipeAsync(undefinedableAsync(string(), 'foo'))) + ).toEqualTypeOf<'foo'>(); + }); }); }); diff --git a/library/src/methods/getDefault/getDefault.test.ts b/library/src/methods/getDefault/getDefault.test.ts index 69cf44f31..6aecee310 100644 --- a/library/src/methods/getDefault/getDefault.test.ts +++ b/library/src/methods/getDefault/getDefault.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from 'vitest'; import { + exactOptional, + exactOptionalAsync, nullable, nullableAsync, nullish, @@ -9,7 +11,10 @@ import { optional, optionalAsync, string, + undefinedable, + undefinedableAsync, } from '../../schemas/index.ts'; +import { pipe, pipeAsync } from '../pipe/index.ts'; import { getDefault } from './getDefault.ts'; describe('getDefault', () => { @@ -19,6 +24,37 @@ describe('getDefault', () => { expect(getDefault(object({}))).toBeUndefined(); }); + describe('should return exact optional default', () => { + test('for undefined value', async () => { + expect(getDefault(exactOptional(string()))).toBeUndefined(); + expect(getDefault(exactOptional(string(), undefined))).toBeUndefined(); + }); + + test('for direct value', () => { + expect(getDefault(exactOptional(string(), 'foo'))).toBe('foo'); + expect(getDefault(exactOptionalAsync(string(), 'foo'))).toBe('foo'); + }); + + test('for value getter', () => { + expect(getDefault(exactOptional(string(), () => 'foo'))).toBe('foo'); + expect(getDefault(exactOptionalAsync(string(), () => 'foo'))).toBe('foo'); + }); + + test('for asycn value getter', async () => { + expect( + await getDefault(exactOptionalAsync(string(), async () => 'foo')) + ).toBe('foo'); + }); + + test('for schema with pipe', () => { + expect(getDefault(pipe(exactOptional(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(exactOptional(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(exactOptionalAsync(string(), 'foo')))).toBe( + 'foo' + ); + }); + }); + describe('should return optional default', () => { test('for undefined value', async () => { expect(getDefault(optional(string()))).toBeUndefined(); @@ -44,6 +80,12 @@ describe('getDefault', () => { 'foo' ); }); + + test('for schema with pipe', () => { + expect(getDefault(pipe(optional(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(optional(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(optionalAsync(string(), 'foo')))).toBe('foo'); + }); }); describe('should return nullable default', () => { @@ -74,6 +116,12 @@ describe('getDefault', () => { 'foo' ); }); + + test('for schema with pipe', () => { + expect(getDefault(pipe(nullable(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(nullable(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(nullableAsync(string(), 'foo')))).toBe('foo'); + }); }); describe('should return nullish default', () => { @@ -109,5 +157,48 @@ describe('getDefault', () => { 'foo' ); }); + + test('for schema with pipe', () => { + expect(getDefault(pipe(nullish(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(nullish(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(nullishAsync(string(), 'foo')))).toBe('foo'); + }); + }); + + describe('should return undefinedable default', () => { + test('for undefined value', async () => { + expect(getDefault(undefinedable(string()))).toBeUndefined(); + expect(getDefault(undefinedable(string(), undefined))).toBeUndefined(); + expect( + getDefault(undefinedable(string(), () => undefined)) + ).toBeUndefined(); + expect( + await getDefault(undefinedableAsync(string(), async () => undefined)) + ).toBeUndefined(); + }); + + test('for direct value', () => { + expect(getDefault(undefinedable(string(), 'foo'))).toBe('foo'); + expect(getDefault(undefinedableAsync(string(), 'foo'))).toBe('foo'); + }); + + test('for value getter', () => { + expect(getDefault(undefinedable(string(), () => 'foo'))).toBe('foo'); + expect(getDefault(undefinedableAsync(string(), () => 'foo'))).toBe('foo'); + }); + + test('for asycn value getter', async () => { + expect( + await getDefault(undefinedableAsync(string(), async () => 'foo')) + ).toBe('foo'); + }); + + test('for schema with pipe', () => { + expect(getDefault(pipe(undefinedable(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(undefinedable(string(), 'foo')))).toBe('foo'); + expect(getDefault(pipeAsync(undefinedableAsync(string(), 'foo')))).toBe( + 'foo' + ); + }); }); }); diff --git a/library/src/methods/getDefault/getDefault.ts b/library/src/methods/getDefault/getDefault.ts index 4c69d697f..1467a7029 100644 --- a/library/src/methods/getDefault/getDefault.ts +++ b/library/src/methods/getDefault/getDefault.ts @@ -1,4 +1,6 @@ import type { + ExactOptionalSchema, + ExactOptionalSchemaAsync, NullableSchema, NullableSchemaAsync, NullishSchema, @@ -13,88 +15,70 @@ import type { BaseSchema, BaseSchemaAsync, Config, - InferInput, InferIssue, - MaybePromise, UnknownDataset, } from '../../types/index.ts'; /** - * Infer default type. + * Schema with default type. */ -export type InferDefault< - TSchema extends - | BaseSchema> - | BaseSchemaAsync> - | NullableSchema>, unknown> - | NullableSchemaAsync< - | BaseSchema> - | BaseSchemaAsync>, - unknown - > - | NullishSchema>, unknown> - | NullishSchemaAsync< - | BaseSchema> - | BaseSchemaAsync>, - unknown - > - | OptionalSchema>, unknown> - | OptionalSchemaAsync< - | BaseSchema> - | BaseSchemaAsync>, - unknown - > - | UndefinedableSchema< - BaseSchema>, - unknown - > - | UndefinedableSchemaAsync< - | BaseSchema> - | BaseSchemaAsync>, - unknown - >, -> = TSchema extends - | NullableSchema< +type SchemaWithDefault = + | ExactOptionalSchema< BaseSchema>, - infer TDefault + unknown > - | NullableSchemaAsync< + | NullableSchema>, unknown> + | NullishSchema>, unknown> + | OptionalSchema>, unknown> + | UndefinedableSchema< + BaseSchema>, + unknown + >; + +/** + * Schema with default async type. + */ +type SchemaWithDefaultAsync = + | ExactOptionalSchemaAsync< | BaseSchema> | BaseSchemaAsync>, - infer TDefault + unknown > - | NullishSchema< - BaseSchema>, - infer TDefault + | NullableSchemaAsync< + | BaseSchema> + | BaseSchemaAsync>, + unknown > | NullishSchemaAsync< | BaseSchema> | BaseSchemaAsync>, - infer TDefault - > - | OptionalSchema< - BaseSchema>, - infer TDefault + unknown > | OptionalSchemaAsync< | BaseSchema> | BaseSchemaAsync>, - infer TDefault - > - | UndefinedableSchema< - BaseSchema>, - infer TDefault + unknown > | UndefinedableSchemaAsync< | BaseSchema> | BaseSchemaAsync>, - infer TDefault - > - ? [TDefault] extends [never] - ? undefined - : TDefault extends () => MaybePromise> - ? ReturnType - : TDefault + unknown + >; + +/** + * Infer default type. + */ +export type InferDefault< + TSchema extends + | BaseSchema> + | BaseSchemaAsync> + | SchemaWithDefault + | SchemaWithDefaultAsync, +> = TSchema extends SchemaWithDefault | SchemaWithDefaultAsync + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + TSchema['default'] extends (...args: any) => any + ? ReturnType + : TSchema['default'] : undefined; /** diff --git a/library/src/methods/omit/omit.test.ts b/library/src/methods/omit/omit.test.ts index db5ae064a..a5a47dcb8 100644 --- a/library/src/methods/omit/omit.test.ts +++ b/library/src/methods/omit/omit.test.ts @@ -3,7 +3,6 @@ import { boolean, type BooleanIssue, number, - type NumberIssue, object, objectWithRest, string, @@ -95,20 +94,20 @@ describe('omit', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object', input: undefined, - expected: 'number', + expected: '"key4"', received: 'undefined', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: { key2: 123 }, key: 'key4', value: undefined, }, ], - } satisfies NumberIssue, + }, ], } satisfies FailureDataset>); }); @@ -185,20 +184,20 @@ describe('omit', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object_with_rest', input: undefined, - expected: 'number', + expected: '"key4"', received: 'undefined', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: { key1: 'foo' }, key: 'key4', value: undefined, }, ], - } satisfies NumberIssue, + }, ], } satisfies FailureDataset>); }); diff --git a/library/src/methods/partial/partial.test.ts b/library/src/methods/partial/partial.test.ts index b11726bb1..8f0a92db6 100644 --- a/library/src/methods/partial/partial.test.ts +++ b/library/src/methods/partial/partial.test.ts @@ -152,14 +152,14 @@ describe('partial', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object', input: undefined, - expected: 'number', + expected: '"key2"', received: 'undefined', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: input, key: 'key2', value: undefined, @@ -309,14 +309,14 @@ describe('partial', () => { { ...baseInfo, kind: 'schema', - type: 'string', + type: 'object_with_rest', input: undefined, - expected: 'string', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: input, key: 'key1', value: undefined, diff --git a/library/src/methods/partial/partialAsync.test.ts b/library/src/methods/partial/partialAsync.test.ts index a378898e9..d925e5817 100644 --- a/library/src/methods/partial/partialAsync.test.ts +++ b/library/src/methods/partial/partialAsync.test.ts @@ -152,14 +152,14 @@ describe('partialAsync', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object', input: undefined, - expected: 'number', + expected: '"key2"', received: 'undefined', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: input, key: 'key2', value: undefined, @@ -311,14 +311,14 @@ describe('partialAsync', () => { { ...baseInfo, kind: 'schema', - type: 'string', + type: 'object_with_rest', input: undefined, - expected: 'string', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: input, key: 'key1', value: undefined, diff --git a/library/src/methods/pick/pick.test.ts b/library/src/methods/pick/pick.test.ts index 2aacc21b9..57a4b3bf2 100644 --- a/library/src/methods/pick/pick.test.ts +++ b/library/src/methods/pick/pick.test.ts @@ -3,11 +3,9 @@ import { boolean, type BooleanIssue, number, - type NumberIssue, object, objectWithRest, string, - type StringIssue, } from '../../schemas/index.ts'; import type { FailureDataset, InferIssue } from '../../types/index.ts'; import { expectNoSchemaIssue } from '../../vitest/index.ts'; @@ -96,20 +94,20 @@ describe('pick', () => { { ...baseInfo, kind: 'schema', - type: 'string', + type: 'object', input: undefined, - expected: 'string', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: { key3: 'bar' }, key: 'key1', value: undefined, }, ], - } satisfies StringIssue, + }, ], } satisfies FailureDataset>); }); @@ -186,20 +184,20 @@ describe('pick', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object_with_rest', input: undefined, - expected: 'number', + expected: '"key2"', received: 'undefined', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: { key3: 'foo' }, key: 'key2', value: undefined, }, ], - } satisfies NumberIssue, + }, ], } satisfies FailureDataset>); }); diff --git a/library/src/methods/required/required.test.ts b/library/src/methods/required/required.test.ts index fbcec9a86..6c16d5b03 100644 --- a/library/src/methods/required/required.test.ts +++ b/library/src/methods/required/required.test.ts @@ -297,6 +297,131 @@ describe('required', () => { expect(schema1['~run']({ value: {} }, {})).toStrictEqual({ typed: false, value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key3"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key3', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key4"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key4', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + + const input = { key2: 123, key4: null }; + expect(schema2['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: { ...input, key4: 123 }, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key3"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key3', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('if required keys are undefined', () => { + const input1 = { + key1: undefined, + key2: undefined, + key3: undefined, + key4: undefined, + }; + expect(schema1['~run']({ value: input1 }, {})).toStrictEqual({ + typed: false, + value: input1, issues: [ { ...baseInfo, @@ -309,7 +434,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key1', value: undefined, }, @@ -326,7 +451,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key2', value: undefined, }, @@ -343,7 +468,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key3', value: undefined, }, @@ -360,7 +485,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key4', value: undefined, }, @@ -369,10 +494,15 @@ describe('required', () => { ], } satisfies FailureDataset>); - const input = { key2: 123, key4: null }; - expect(schema2['~run']({ value: input }, {})).toStrictEqual({ + const input2 = { + key1: undefined, + key2: 123, + key3: undefined, + key4: null, + }; + expect(schema2['~run']({ value: input2 }, {})).toStrictEqual({ typed: false, - value: { ...input, key4: 123 }, + value: { ...input2, key4: 123 }, issues: [ { ...baseInfo, @@ -385,7 +515,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input, + input: input2, key: 'key1', value: undefined, }, @@ -402,7 +532,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input, + input: input2, key: 'key3', value: undefined, }, @@ -692,6 +822,131 @@ describe('required', () => { expect(schema1['~run']({ value: {} }, {})).toStrictEqual({ typed: false, value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key3"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key3', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key4"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key4', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + + const input = { key1: 'foo', key4: null, other: true }; + expect(schema2['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: { ...input, key4: 123 }, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key3"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key3', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('if required keys are undefined', () => { + const input1 = { + key1: undefined, + key2: undefined, + key3: undefined, + key4: undefined, + }; + expect(schema1['~run']({ value: input1 }, {})).toStrictEqual({ + typed: false, + value: input1, issues: [ { ...baseInfo, @@ -704,7 +959,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key1', value: undefined, }, @@ -721,7 +976,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key2', value: undefined, }, @@ -738,7 +993,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key3', value: undefined, }, @@ -755,7 +1010,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key4', value: undefined, }, @@ -764,10 +1019,16 @@ describe('required', () => { ], } satisfies FailureDataset>); - const input = { key1: 'foo', key4: null, other: true }; - expect(schema2['~run']({ value: input }, {})).toStrictEqual({ + const input2 = { + key1: 'foo', + key2: undefined, + key3: undefined, + key4: null, + other: true, + }; + expect(schema2['~run']({ value: input2 }, {})).toStrictEqual({ typed: false, - value: { ...input, key4: 123 }, + value: { ...input2, key4: 123 }, issues: [ { ...baseInfo, @@ -780,7 +1041,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input, + input: input2, key: 'key2', value: undefined, }, @@ -797,7 +1058,7 @@ describe('required', () => { { type: 'object', origin: 'value', - input, + input: input2, key: 'key3', value: undefined, }, diff --git a/library/src/methods/required/requiredAsync.test.ts b/library/src/methods/required/requiredAsync.test.ts index 740347148..99878cb9d 100644 --- a/library/src/methods/required/requiredAsync.test.ts +++ b/library/src/methods/required/requiredAsync.test.ts @@ -268,6 +268,131 @@ describe('requiredAsync', () => { expect(await schema1['~run']({ value: {} }, {})).toStrictEqual({ typed: false, value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key3"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key3', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key4"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key4', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + + const input = { key2: 123, key4: null }; + expect(await schema2['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: { ...input, key4: 123 }, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key3"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key3', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('if required keys are undefined', async () => { + const input1 = { + key1: undefined, + key2: undefined, + key3: undefined, + key4: undefined, + }; + expect(await schema1['~run']({ value: input1 }, {})).toStrictEqual({ + typed: false, + value: input1, issues: [ { ...baseInfo, @@ -280,7 +405,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key1', value: undefined, }, @@ -297,7 +422,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key2', value: undefined, }, @@ -314,7 +439,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key3', value: undefined, }, @@ -331,7 +456,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key4', value: undefined, }, @@ -340,10 +465,15 @@ describe('requiredAsync', () => { ], } satisfies FailureDataset>); - const input = { key2: 123, key4: null }; - expect(await schema2['~run']({ value: input }, {})).toStrictEqual({ + const input2 = { + key1: undefined, + key2: 123, + key3: undefined, + key4: null, + }; + expect(await schema2['~run']({ value: input2 }, {})).toStrictEqual({ typed: false, - value: { ...input, key4: 123 }, + value: { ...input2, key4: 123 }, issues: [ { ...baseInfo, @@ -356,7 +486,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input, + input: input2, key: 'key1', value: undefined, }, @@ -373,7 +503,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input, + input: input2, key: 'key3', value: undefined, }, @@ -672,6 +802,131 @@ describe('requiredAsync', () => { expect(await schema1['~run']({ value: {} }, {})).toStrictEqual({ typed: false, value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key3"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key3', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key4"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key4', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + + const input = { key1: 'foo', key4: null, other: true }; + expect(await schema2['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: { ...input, key4: 123 }, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key3"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key3', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('if required keys are undefined', async () => { + const input1 = { + key1: undefined, + key2: undefined, + key3: undefined, + key4: undefined, + }; + expect(await schema1['~run']({ value: input1 }, {})).toStrictEqual({ + typed: false, + value: input1, issues: [ { ...baseInfo, @@ -684,7 +939,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key1', value: undefined, }, @@ -701,7 +956,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key2', value: undefined, }, @@ -718,7 +973,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key3', value: undefined, }, @@ -735,7 +990,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input: {}, + input: input1, key: 'key4', value: undefined, }, @@ -744,10 +999,16 @@ describe('requiredAsync', () => { ], } satisfies FailureDataset>); - const input = { key1: 'foo', key4: null, other: true }; - expect(await schema2['~run']({ value: input }, {})).toStrictEqual({ + const input2 = { + key1: 'foo', + key2: undefined, + key3: undefined, + key4: null, + other: true, + }; + expect(await schema2['~run']({ value: input2 }, {})).toStrictEqual({ typed: false, - value: { ...input, key4: 123 }, + value: { ...input2, key4: 123 }, issues: [ { ...baseInfo, @@ -760,7 +1021,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input, + input: input2, key: 'key2', value: undefined, }, @@ -777,7 +1038,7 @@ describe('requiredAsync', () => { { type: 'object', origin: 'value', - input, + input: input2, key: 'key3', value: undefined, }, diff --git a/library/src/methods/unwrap/unwrap.test-d.ts b/library/src/methods/unwrap/unwrap.test-d.ts index 7986e58c6..3b4a83654 100644 --- a/library/src/methods/unwrap/unwrap.test-d.ts +++ b/library/src/methods/unwrap/unwrap.test-d.ts @@ -1,5 +1,7 @@ import { describe, expectTypeOf, test } from 'vitest'; import { + exactOptional, + exactOptionalAsync, nonNullable, nonNullableAsync, nonNullish, @@ -13,13 +15,25 @@ import { optional, optionalAsync, string, + undefinedable, + undefinedableAsync, } from '../../schemas/index.ts'; +import { fallback, fallbackAsync } from '../fallback/index.ts'; +import { pipe, pipeAsync } from '../pipe/index.ts'; import { unwrap } from './unwrap.ts'; describe('unwrap', () => { const wrapped = string(); type Wrapped = typeof wrapped; + test('should unwrap exactOptional', () => { + expectTypeOf(unwrap(exactOptional(wrapped))).toEqualTypeOf(); + }); + + test('should unwrap exactOptionalAsync', () => { + expectTypeOf(unwrap(exactOptionalAsync(wrapped))).toEqualTypeOf(); + }); + test('should unwrap nonNullable', () => { expectTypeOf(unwrap(nonNullable(wrapped))).toEqualTypeOf(); }); @@ -67,4 +81,32 @@ describe('unwrap', () => { test('should unwrap optionalAsync', () => { expectTypeOf(unwrap(optionalAsync(wrapped))).toEqualTypeOf(); }); + + test('should unwrap undefinedable', () => { + expectTypeOf(unwrap(undefinedable(wrapped))).toEqualTypeOf(); + }); + + test('should unwrap undefinedableAsync', () => { + expectTypeOf(unwrap(undefinedableAsync(wrapped))).toEqualTypeOf(); + }); + + test('should unwrap schema with pipe', () => { + expectTypeOf(unwrap(pipe(optional(wrapped)))).toEqualTypeOf(); + expectTypeOf(unwrap(pipeAsync(optional(wrapped)))).toEqualTypeOf(); + expectTypeOf( + unwrap(pipeAsync(optionalAsync(wrapped))) + ).toEqualTypeOf(); + }); + + test('should unwrap schema with fallback', () => { + expectTypeOf( + unwrap(fallback(optional(wrapped), 'foo')) + ).toEqualTypeOf(); + expectTypeOf( + unwrap(fallbackAsync(optional(wrapped), 'foo')) + ).toEqualTypeOf(); + expectTypeOf( + unwrap(fallbackAsync(optionalAsync(wrapped), 'foo')) + ).toEqualTypeOf(); + }); }); diff --git a/library/src/methods/unwrap/unwrap.test.ts b/library/src/methods/unwrap/unwrap.test.ts index 7c6cb686c..87c293197 100644 --- a/library/src/methods/unwrap/unwrap.test.ts +++ b/library/src/methods/unwrap/unwrap.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from 'vitest'; import { + exactOptional, + exactOptionalAsync, nonNullable, nonNullableAsync, nonNullish, @@ -13,12 +15,24 @@ import { optional, optionalAsync, string, + undefinedable, + undefinedableAsync, } from '../../schemas/index.ts'; +import { fallback, fallbackAsync } from '../fallback/index.ts'; +import { pipe, pipeAsync } from '../pipe/index.ts'; import { unwrap } from './unwrap.ts'; describe('unwrap', () => { const wrapped = string(); + test('should unwrap exactOptional', () => { + expect(unwrap(exactOptional(wrapped))).toBe(wrapped); + }); + + test('should unwrap exactOptionalAsync', () => { + expect(unwrap(exactOptionalAsync(wrapped))).toBe(wrapped); + }); + test('should unwrap nonNullable', () => { expect(unwrap(nonNullable(wrapped))).toBe(wrapped); }); @@ -66,4 +80,24 @@ describe('unwrap', () => { test('should unwrap optionalAsync', () => { expect(unwrap(optionalAsync(wrapped))).toBe(wrapped); }); + + test('should unwrap undefinedable', () => { + expect(unwrap(undefinedable(wrapped))).toBe(wrapped); + }); + + test('should unwrap undefinedableAsync', () => { + expect(unwrap(undefinedableAsync(wrapped))).toBe(wrapped); + }); + + test('should unwrap schema with pipe', () => { + expect(unwrap(pipe(optional(wrapped)))).toBe(wrapped); + expect(unwrap(pipeAsync(optional(wrapped)))).toBe(wrapped); + expect(unwrap(pipeAsync(optionalAsync(wrapped)))).toBe(wrapped); + }); + + test('should unwrap schema with fallback', () => { + expect(unwrap(fallback(optional(wrapped), 'foo'))).toBe(wrapped); + expect(unwrap(fallbackAsync(optional(wrapped), 'foo'))).toBe(wrapped); + expect(unwrap(fallbackAsync(optionalAsync(wrapped), 'foo'))).toBe(wrapped); + }); }); diff --git a/library/src/methods/unwrap/unwrap.ts b/library/src/methods/unwrap/unwrap.ts index dcbaac3e8..3c1bd6cfb 100644 --- a/library/src/methods/unwrap/unwrap.ts +++ b/library/src/methods/unwrap/unwrap.ts @@ -1,4 +1,6 @@ import type { + ExactOptionalSchema, + ExactOptionalSchemaAsync, NonNullableIssue, NonNullableSchema, NonNullableSchemaAsync, @@ -34,6 +36,15 @@ import type { // @__NO_SIDE_EFFECTS__ export function unwrap< TSchema extends + | ExactOptionalSchema< + BaseSchema>, + unknown + > + | ExactOptionalSchemaAsync< + | BaseSchema> + | BaseSchemaAsync>, + unknown + > | NonNullableSchema< BaseSchema>, ErrorMessage | undefined diff --git a/library/src/schemas/array/arrayAsync.ts b/library/src/schemas/array/arrayAsync.ts index 4a749cbfe..aa44bb02e 100644 --- a/library/src/schemas/array/arrayAsync.ts +++ b/library/src/schemas/array/arrayAsync.ts @@ -10,6 +10,7 @@ import type { OutputDataset, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { array } from './array.ts'; import type { ArrayIssue } from './types.ts'; /** @@ -32,7 +33,7 @@ export interface ArraySchemaAsync< /** * The schema reference. */ - readonly reference: typeof arrayAsync; + readonly reference: typeof array | typeof arrayAsync; /** * The expected property. */ diff --git a/library/src/schemas/custom/customAsync.ts b/library/src/schemas/custom/customAsync.ts index be71bb446..1d880e2bb 100644 --- a/library/src/schemas/custom/customAsync.ts +++ b/library/src/schemas/custom/customAsync.ts @@ -5,6 +5,7 @@ import type { OutputDataset, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { custom } from './custom.ts'; import type { CustomIssue } from './types.ts'; /** @@ -26,7 +27,7 @@ export interface CustomSchemaAsync< /** * The schema reference. */ - readonly reference: typeof customAsync; + readonly reference: typeof custom | typeof customAsync; /** * The expected property. */ diff --git a/library/src/schemas/exactOptional/exactOptional.test-d.ts b/library/src/schemas/exactOptional/exactOptional.test-d.ts new file mode 100644 index 000000000..50cd94dbc --- /dev/null +++ b/library/src/schemas/exactOptional/exactOptional.test-d.ts @@ -0,0 +1,65 @@ +import { describe, expectTypeOf, test } from 'vitest'; +import type { TransformAction } from '../../actions/index.ts'; +import type { SchemaWithPipe } from '../../methods/index.ts'; +import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; +import { exactOptional, type ExactOptionalSchema } from './exactOptional.ts'; + +describe('exactOptional', () => { + describe('should return schema object', () => { + test('with undefined default', () => { + type Schema = ExactOptionalSchema, undefined>; + expectTypeOf(exactOptional(string())).toEqualTypeOf(); + expectTypeOf(exactOptional(string(), undefined)).toEqualTypeOf(); + }); + + test('with value default', () => { + expectTypeOf(exactOptional(string(), 'foo')).toEqualTypeOf< + ExactOptionalSchema, 'foo'> + >(); + }); + + test('with value getter default', () => { + expectTypeOf(exactOptional(string(), () => 'foo')).toEqualTypeOf< + ExactOptionalSchema, () => string> + >(); + }); + }); + + describe('should infer correct types', () => { + type Schema1 = ExactOptionalSchema, undefined>; + type Schema2 = ExactOptionalSchema, 'foo'>; + type Schema3 = ExactOptionalSchema, () => 'foo'>; + type Schema4 = ExactOptionalSchema< + SchemaWithPipe< + [StringSchema, TransformAction] + >, + 'foo' + >; + + test('of input', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('of output', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('of issue', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + }); +}); diff --git a/library/src/schemas/exactOptional/exactOptional.test.ts b/library/src/schemas/exactOptional/exactOptional.test.ts new file mode 100644 index 000000000..b92e921a5 --- /dev/null +++ b/library/src/schemas/exactOptional/exactOptional.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, test } from 'vitest'; +import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; +import { exactOptional, type ExactOptionalSchema } from './exactOptional.ts'; + +describe('exactOptional', () => { + describe('should return schema object', () => { + const baseSchema: Omit< + ExactOptionalSchema, string>, + 'default' + > = { + kind: 'schema', + type: 'exact_optional', + reference: exactOptional, + expects: 'string', + wrapped: { + ...string(), + '~standard': { + version: 1, + vendor: 'valibot', + validate: expect.any(Function), + }, + '~run': expect.any(Function), + }, + async: false, + '~standard': { + version: 1, + vendor: 'valibot', + validate: expect.any(Function), + }, + '~run': expect.any(Function), + }; + + test('with undefined default', () => { + const expected: ExactOptionalSchema< + StringSchema, + undefined + > = { + ...baseSchema, + default: undefined, + }; + expect(exactOptional(string())).toStrictEqual(expected); + expect(exactOptional(string(), undefined)).toStrictEqual(expected); + }); + + test('with value default', () => { + expect(exactOptional(string(), 'foo')).toStrictEqual({ + ...baseSchema, + default: 'foo', + } satisfies ExactOptionalSchema, 'foo'>); + }); + + test('with value getter default', () => { + const getter = () => 'foo'; + expect(exactOptional(string(), getter)).toStrictEqual({ + ...baseSchema, + default: getter, + } satisfies ExactOptionalSchema, typeof getter>); + }); + }); + + describe('should return dataset without issues', () => { + const schema = exactOptional(string()); + + test('for wrapper type', () => { + expectNoSchemaIssue(schema, ['', 'foo', '#$%']); + }); + }); + + describe('should return dataset with issues', () => { + const schema = exactOptional(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', () => { + expectSchemaIssue(schema, baseIssue, [123, true, {}]); + }); + + test('for null', () => { + expectSchemaIssue(schema, baseIssue, [null]); + }); + + test('for undefined', () => { + expectSchemaIssue(schema, baseIssue, [undefined]); + }); + }); + + describe('should return dataset without default', () => { + test('for undefined default', () => { + expectNoSchemaIssue(exactOptional(string()), ['foo']); + expectNoSchemaIssue(exactOptional(string(), undefined), ['foo']); + }); + + test('for wrapper type', () => { + expectNoSchemaIssue(exactOptional(string(), 'foo'), ['', 'bar', '#$%']); + }); + }); +}); diff --git a/library/src/schemas/exactOptional/exactOptional.ts b/library/src/schemas/exactOptional/exactOptional.ts new file mode 100644 index 000000000..9c443fceb --- /dev/null +++ b/library/src/schemas/exactOptional/exactOptional.ts @@ -0,0 +1,94 @@ +import type { + BaseIssue, + BaseSchema, + Default, + InferInput, + InferIssue, + InferOutput, +} from '../../types/index.ts'; +import { _getStandardProps } from '../../utils/index.ts'; + +/** + * Exact optional schema interface. + */ +export interface ExactOptionalSchema< + TWrapped extends BaseSchema>, + TDefault extends Default, +> extends BaseSchema< + InferInput, + InferOutput, + InferIssue + > { + /** + * The schema type. + */ + readonly type: 'exact_optional'; + /** + * The schema reference. + */ + readonly reference: typeof exactOptional; + /** + * The expected property. + */ + readonly expects: TWrapped['expects']; + /** + * The wrapped schema. + */ + readonly wrapped: TWrapped; + /** + * The default value. + */ + readonly default: TDefault; +} + +/** + * Creates an exact optional schema. + * + * @param wrapped The wrapped schema. + * + * @returns An exact optional schema. + */ +export function exactOptional< + const TWrapped extends BaseSchema>, +>(wrapped: TWrapped): ExactOptionalSchema; + +/** + * Creates an exact optional schema. + * + * @param wrapped The wrapped schema. + * @param default_ The default value. + * + * @returns An exact optional schema. + */ +export function exactOptional< + const TWrapped extends BaseSchema>, + const TDefault extends Default, +>( + wrapped: TWrapped, + default_: TDefault +): ExactOptionalSchema; + +// @__NO_SIDE_EFFECTS__ +export function exactOptional( + wrapped: BaseSchema>, + default_?: unknown +): ExactOptionalSchema< + BaseSchema>, + unknown +> { + return { + kind: 'schema', + type: 'exact_optional', + reference: exactOptional, + expects: wrapped.expects, + async: false, + wrapped, + default: default_, + get '~standard'() { + return _getStandardProps(this); + }, + '~run'(dataset, config) { + return this.wrapped['~run'](dataset, config); + }, + }; +} diff --git a/library/src/schemas/exactOptional/exactOptionalAsync.test-d.ts b/library/src/schemas/exactOptional/exactOptionalAsync.test-d.ts new file mode 100644 index 000000000..271ef85e3 --- /dev/null +++ b/library/src/schemas/exactOptional/exactOptionalAsync.test-d.ts @@ -0,0 +1,91 @@ +import { describe, expectTypeOf, test } from 'vitest'; +import type { TransformActionAsync } from '../../actions/index.ts'; +import type { SchemaWithPipeAsync } from '../../methods/index.ts'; +import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; +import { + exactOptionalAsync, + type ExactOptionalSchemaAsync, +} from './exactOptionalAsync.ts'; + +describe('exactOptionalAsync', () => { + describe('should return schema object', () => { + test('with undefined default', () => { + type Schema = ExactOptionalSchemaAsync< + StringSchema, + undefined + >; + expectTypeOf(exactOptionalAsync(string())).toEqualTypeOf(); + expectTypeOf( + exactOptionalAsync(string(), undefined) + ).toEqualTypeOf(); + }); + + test('with value default', () => { + expectTypeOf(exactOptionalAsync(string(), 'foo')).toEqualTypeOf< + ExactOptionalSchemaAsync, 'foo'> + >(); + }); + + test('with value getter default', () => { + expectTypeOf(exactOptionalAsync(string(), () => 'foo')).toEqualTypeOf< + ExactOptionalSchemaAsync, () => string> + >(); + }); + + test('with async value getter default', () => { + expectTypeOf( + exactOptionalAsync(string(), async () => 'foo') + ).toEqualTypeOf< + ExactOptionalSchemaAsync, () => Promise> + >(); + }); + }); + + describe('should infer correct types', () => { + type Schema1 = ExactOptionalSchemaAsync, undefined>; + type Schema2 = ExactOptionalSchemaAsync, 'foo'>; + type Schema3 = ExactOptionalSchemaAsync< + StringSchema, + () => 'foo' + >; + type Schema4 = ExactOptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + type Schema5 = ExactOptionalSchemaAsync< + SchemaWithPipeAsync< + [StringSchema, TransformActionAsync] + >, + 'foo' + >; + + test('of input', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('of output', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + + test('of issue', () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); + }); +}); diff --git a/library/src/schemas/exactOptional/exactOptionalAsync.test.ts b/library/src/schemas/exactOptional/exactOptionalAsync.test.ts new file mode 100644 index 000000000..670791d05 --- /dev/null +++ b/library/src/schemas/exactOptional/exactOptionalAsync.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, test } from 'vitest'; +import { + expectNoSchemaIssueAsync, + expectSchemaIssueAsync, +} from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; +import { + exactOptionalAsync, + type ExactOptionalSchemaAsync, +} from './exactOptionalAsync.ts'; + +describe('exactOptionalAsync', () => { + describe('should return schema object', () => { + const baseSchema: Omit< + ExactOptionalSchemaAsync, string>, + 'default' + > = { + kind: 'schema', + type: 'exact_optional', + reference: exactOptionalAsync, + expects: 'string', + wrapped: { + ...string(), + '~standard': { + version: 1, + vendor: 'valibot', + validate: expect.any(Function), + }, + '~run': expect.any(Function), + }, + async: true, + '~standard': { + version: 1, + vendor: 'valibot', + validate: expect.any(Function), + }, + '~run': expect.any(Function), + }; + + test('with undefined default', () => { + const expected: ExactOptionalSchemaAsync< + StringSchema, + undefined + > = { + ...baseSchema, + default: undefined, + }; + expect(exactOptionalAsync(string())).toStrictEqual(expected); + expect(exactOptionalAsync(string(), undefined)).toStrictEqual(expected); + }); + + test('with value default', () => { + expect(exactOptionalAsync(string(), 'foo')).toStrictEqual({ + ...baseSchema, + default: 'foo', + } satisfies ExactOptionalSchemaAsync, 'foo'>); + }); + + test('with value getter default', () => { + const getter = () => 'foo'; + expect(exactOptionalAsync(string(), getter)).toStrictEqual({ + ...baseSchema, + default: getter, + } satisfies ExactOptionalSchemaAsync< + StringSchema, + typeof getter + >); + }); + + test('with async value getter default', () => { + const getter = async () => 'foo'; + expect(exactOptionalAsync(string(), getter)).toStrictEqual({ + ...baseSchema, + default: getter, + } satisfies ExactOptionalSchemaAsync< + StringSchema, + typeof getter + >); + }); + }); + + describe('should return dataset without issues', () => { + const schema = exactOptionalAsync(string()); + + test('for wrapper type', async () => { + await expectNoSchemaIssueAsync(schema, ['', 'foo', '#$%']); + }); + }); + + describe('should return dataset with issues', () => { + const schema = exactOptionalAsync(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [123, true, {}]); + }); + + test('for null', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [null]); + }); + + test('for undefined', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [undefined]); + }); + }); + + describe('should return dataset without default', () => { + test('for undefined default', async () => { + await expectNoSchemaIssueAsync(exactOptionalAsync(string()), ['foo']); + await expectNoSchemaIssueAsync(exactOptionalAsync(string(), undefined), [ + 'foo', + ]); + }); + + test('for wrapper type', async () => { + await expectNoSchemaIssueAsync(exactOptionalAsync(string(), 'foo'), [ + '', + 'bar', + '#$%', + ]); + }); + }); +}); diff --git a/library/src/schemas/exactOptional/exactOptionalAsync.ts b/library/src/schemas/exactOptional/exactOptionalAsync.ts new file mode 100644 index 000000000..80919f204 --- /dev/null +++ b/library/src/schemas/exactOptional/exactOptionalAsync.ts @@ -0,0 +1,105 @@ +import type { + BaseIssue, + BaseSchema, + BaseSchemaAsync, + DefaultAsync, + InferInput, + InferIssue, + InferOutput, +} from '../../types/index.ts'; +import { _getStandardProps } from '../../utils/index.ts'; +import type { exactOptional } from './exactOptional.ts'; + +/** + * Exact optional schema async interface. + */ +export interface ExactOptionalSchemaAsync< + TWrapped extends + | BaseSchema> + | BaseSchemaAsync>, + TDefault extends DefaultAsync, +> extends BaseSchemaAsync< + InferInput, + InferOutput, + InferIssue + > { + /** + * The schema type. + */ + readonly type: 'exact_optional'; + /** + * The schema reference. + */ + readonly reference: typeof exactOptional | typeof exactOptionalAsync; + /** + * The expected property. + */ + readonly expects: TWrapped['expects']; + /** + * The wrapped schema. + */ + readonly wrapped: TWrapped; + /** + * The default value. + */ + readonly default: TDefault; +} + +/** + * Creates an exact optional schema. + * + * @param wrapped The wrapped schema. + * + * @returns An exact optional schema. + */ +export function exactOptionalAsync< + const TWrapped extends + | BaseSchema> + | BaseSchemaAsync>, +>(wrapped: TWrapped): ExactOptionalSchemaAsync; + +/** + * Creates an exact optional schema. + * + * @param wrapped The wrapped schema. + * @param default_ The default value. + * + * @returns An exact optional schema. + */ +export function exactOptionalAsync< + const TWrapped extends + | BaseSchema> + | BaseSchemaAsync>, + const TDefault extends DefaultAsync, +>( + wrapped: TWrapped, + default_: TDefault +): ExactOptionalSchemaAsync; + +// @__NO_SIDE_EFFECTS__ +export function exactOptionalAsync( + wrapped: + | BaseSchema> + | BaseSchemaAsync>, + default_?: unknown +): ExactOptionalSchemaAsync< + | BaseSchema> + | BaseSchemaAsync>, + unknown +> { + return { + kind: 'schema', + type: 'exact_optional', + reference: exactOptionalAsync, + expects: wrapped.expects, + async: true, + wrapped, + default: default_, + get '~standard'() { + return _getStandardProps(this); + }, + async '~run'(dataset, config) { + return this.wrapped['~run'](dataset, config); + }, + }; +} diff --git a/library/src/schemas/exactOptional/index.ts b/library/src/schemas/exactOptional/index.ts new file mode 100644 index 000000000..d30aa476c --- /dev/null +++ b/library/src/schemas/exactOptional/index.ts @@ -0,0 +1,2 @@ +export * from './exactOptional.ts'; +export * from './exactOptionalAsync.ts'; diff --git a/library/src/schemas/index.ts b/library/src/schemas/index.ts index 9bb5c495c..1d43af2a2 100644 --- a/library/src/schemas/index.ts +++ b/library/src/schemas/index.ts @@ -6,6 +6,7 @@ export * from './boolean/index.ts'; export * from './custom/index.ts'; export * from './date/index.ts'; export * from './enum/index.ts'; +export * from './exactOptional/index.ts'; export * from './file/index.ts'; export * from './function/index.ts'; export * from './instance/index.ts'; diff --git a/library/src/schemas/intersect/intersectAsync.ts b/library/src/schemas/intersect/intersectAsync.ts index 2cd0a7299..992997ccb 100644 --- a/library/src/schemas/intersect/intersectAsync.ts +++ b/library/src/schemas/intersect/intersectAsync.ts @@ -10,6 +10,7 @@ import { _getStandardProps, _joinExpects, } from '../../utils/index.ts'; +import type { intersect } from './intersect.ts'; import type { InferIntersectInput, InferIntersectOutput, @@ -36,7 +37,7 @@ export interface IntersectSchemaAsync< /** * The schema reference. */ - readonly reference: typeof intersectAsync; + readonly reference: typeof intersect | typeof intersectAsync; /** * The intersect options. */ diff --git a/library/src/schemas/lazy/lazyAsync.ts b/library/src/schemas/lazy/lazyAsync.ts index 14d933661..5955802eb 100644 --- a/library/src/schemas/lazy/lazyAsync.ts +++ b/library/src/schemas/lazy/lazyAsync.ts @@ -8,6 +8,7 @@ import type { MaybePromise, } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; +import type { lazy } from './lazy.ts'; /** * Lazy schema async interface. @@ -28,7 +29,7 @@ export interface LazySchemaAsync< /** * The schema reference. */ - readonly reference: typeof lazyAsync; + readonly reference: typeof lazy | typeof lazyAsync; /** * The expected property. */ diff --git a/library/src/schemas/looseObject/looseObject.test-d.ts b/library/src/schemas/looseObject/looseObject.test-d.ts index 2d6652c70..fc83df752 100644 --- a/library/src/schemas/looseObject/looseObject.test-d.ts +++ b/library/src/schemas/looseObject/looseObject.test-d.ts @@ -2,6 +2,8 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { ReadonlyAction, TransformAction } from '../../actions/index.ts'; import type { SchemaWithPipe } from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import type { AnySchema } from '../any/index.ts'; +import type { ExactOptionalSchema } from '../exactOptional/index.ts'; import type { NullishSchema } from '../nullish/index.ts'; import { type NumberIssue, type NumberSchema } from '../number/index.ts'; import type { ObjectIssue, ObjectSchema } from '../object/index.ts'; @@ -12,6 +14,7 @@ import { type StringSchema, } from '../string/index.ts'; import type { UndefinedableSchema } from '../undefinedable/index.ts'; +import type { UnknownSchema } from '../unknown/index.ts'; import { looseObject, type LooseObjectSchema } from './looseObject.ts'; import type { LooseObjectIssue } from './types.ts'; @@ -42,18 +45,39 @@ describe('looseObject', () => { describe('should infer correct types', () => { type Schema = LooseObjectSchema< { - key1: StringSchema; - key2: OptionalSchema, 'foo'>; - key3: NullishSchema, undefined>; - key4: ObjectSchema<{ key: NumberSchema }, undefined>; - key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; - key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipe< + key00: StringSchema; + key01: AnySchema; + key02: UnknownSchema; + key03: ObjectSchema<{ key: NumberSchema }, undefined>; + key04: SchemaWithPipe< + [StringSchema, ReadonlyAction] + >; + key05: UndefinedableSchema, 'bar'>; + key06: SchemaWithPipe< [ OptionalSchema, undefined>, - TransformAction, + TransformAction, ] >; + + // ExactOptionalSchema + key10: ExactOptionalSchema, undefined>; + key11: ExactOptionalSchema, 'foo'>; + key12: ExactOptionalSchema, () => 'foo'>; + + // OptionalSchema + key20: OptionalSchema, undefined>; + key21: OptionalSchema, 'foo'>; + key22: OptionalSchema, () => undefined>; + key23: OptionalSchema, () => 'foo'>; + + // NullishSchema + key30: NullishSchema, undefined>; + key31: NullishSchema, null>; + key32: NullishSchema, 'foo'>; + key33: NullishSchema, () => undefined>; + key34: NullishSchema, () => null>; + key35: NullishSchema, () => 'foo'>; }, undefined >; @@ -61,13 +85,33 @@ describe('looseObject', () => { test('of input', () => { expectTypeOf>().toEqualTypeOf< { - key1: string; - key2?: string; - key3?: string | null | undefined; - key4: { key: number }; - key5: string; - key6: string | undefined; - key7?: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + key04: string; + key05: string | undefined; + key06?: string | undefined; + + // ExactOptionalSchema + key10?: string; + key11?: string; + key12?: string; + + // OptionalSchema + key20?: string | undefined; + key21?: string | undefined; + key22?: string | undefined; + key23?: string | undefined; + + // NullishSchema + key30?: string | null | undefined; + key31?: string | null | undefined; + key32?: string | null | undefined; + key33?: string | null | undefined; + key34?: string | null | undefined; + key35?: string | null | undefined; } & { [key: string]: unknown } >(); }); @@ -75,13 +119,33 @@ describe('looseObject', () => { test('of output', () => { expectTypeOf>().toEqualTypeOf< { - key1: string; - key2: string; - key3?: string | null | undefined; - key4: { key: number }; - readonly key5: string; - key6: string; - key7: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + readonly key04: string; + key05: string; + key06?: number; + + // ExactOptionalSchema + key10?: string; + key11: string; + key12: string; + + // OptionalSchema + key20?: string | undefined; + key21: string; + key22: string | undefined; + key23: string; + + // NullishSchema + key30?: string | null | undefined; + key31: string | null; + key32: string; + key33: string | undefined; + key34: string | null; + key35: string; } & { [key: string]: unknown } >(); }); diff --git a/library/src/schemas/looseObject/looseObject.test.ts b/library/src/schemas/looseObject/looseObject.test.ts index d3cf410a0..4004fdfd0 100644 --- a/library/src/schemas/looseObject/looseObject.test.ts +++ b/library/src/schemas/looseObject/looseObject.test.ts @@ -1,11 +1,14 @@ import { describe, expect, test } from 'vitest'; import type { FailureDataset, InferIssue } from '../../types/index.ts'; import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { any } from '../any/index.ts'; +import { exactOptional } from '../exactOptional/index.ts'; import { nullish } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { object } from '../object/index.ts'; import { optional } from '../optional/index.ts'; -import { string, type StringIssue } from '../string/index.ts'; +import { string } from '../string/index.ts'; +import { unknown } from '../unknown/index.ts'; import { looseObject, type LooseObjectSchema } from './looseObject.ts'; import type { LooseObjectIssue } from './types.ts'; @@ -137,15 +140,71 @@ describe('looseObject', () => { ]); }); + test('for exact optional entry', () => { + expectNoSchemaIssue(looseObject({ key: exactOptional(string()) }), [ + {}, + { key: 'foo' }, + ]); + }); + + test('for exact optional entry with default', () => { + expect( + looseObject({ key: exactOptional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + looseObject({ key: exactOptional(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + }); + test('for optional entry', () => { expectNoSchemaIssue(looseObject({ key: optional(string()) }), [ {}, - // @ts-expect-error { key: undefined }, { key: 'foo' }, ]); }); + test('for optional entry with default', () => { + expect( + looseObject({ key: optional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + looseObject({ key: optional(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + looseObject({ + key: optional(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); + test('for nullish entry', () => { expectNoSchemaIssue(looseObject({ key: nullish(number()) }), [ {}, @@ -155,6 +214,51 @@ describe('looseObject', () => { ]); }); + test('for nullish entry with default', () => { + expect( + looseObject({ key: nullish(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + looseObject({ key: nullish(string(), null) })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + looseObject({ key: nullish(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + looseObject({ key: nullish(string(), () => null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + looseObject({ key: nullish(string(), () => undefined) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); + test('for unknown entries', () => { expectNoSchemaIssue(looseObject({ key1: string(), key2: number() }), [ { key1: 'foo', key2: 123, other1: 'bar', other2: null }, @@ -164,8 +268,9 @@ describe('looseObject', () => { describe('should return dataset with nested issues', () => { const schema = looseObject({ - key: string(), - nested: object({ key: number() }), + key1: string(), + key2: number(), + nested: looseObject({ key1: string(), key2: number() }), }); const baseInfo = { @@ -177,42 +282,41 @@ describe('looseObject', () => { abortPipeEarly: undefined, }; - const stringIssue: StringIssue = { - ...baseInfo, - kind: 'schema', - type: 'string', - input: undefined, - expected: 'string', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'value', - input: {}, - key: 'key', - value: undefined, - }, - ], - }; - test('for missing entries', () => { - expect(schema['~run']({ value: {} }, {})).toStrictEqual({ + const input = { key2: 123 }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: {}, + value: input, issues: [ - stringIssue, { ...baseInfo, kind: 'schema', - type: 'object', + type: 'loose_object', input: undefined, - expected: 'Object', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', - input: {}, + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"nested"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, key: 'nested', value: undefined, }, @@ -223,7 +327,7 @@ describe('looseObject', () => { }); test('for missing nested entries', () => { - const input = { key: 'value', nested: {} }; + const input = { key1: 'value', nested: {} }; expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, value: input, @@ -231,23 +335,138 @@ describe('looseObject', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'loose_object', input: undefined, - expected: 'number', + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key1"', received: 'undefined', path: [ { type: 'object', origin: 'value', - input: { key: 'value', nested: {} }, + input, key: 'nested', value: {}, }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ { type: 'object', origin: 'value', + input, + key: 'nested', + value: {}, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing entries with abort early', () => { + const input = { key2: 123 }; + expect( + schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing any and unknown entry', () => { + const schema = looseObject({ key1: any(), key2: unknown() }); + expect(schema['~run']({ value: {} }, {})).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', input: {}, - key: 'key', + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', value: undefined, }, ], @@ -256,14 +475,188 @@ describe('looseObject', () => { } satisfies FailureDataset>); }); - test('with abort early', () => { - expect(schema['~run']({ value: {} }, { abortEarly: true })).toStrictEqual( - { - typed: false, - value: {}, - issues: [{ ...stringIssue, abortEarly: true }], - } satisfies FailureDataset> - ); + test('for invalid entries', () => { + const input = { key1: false, key2: 123, nested: null }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: null, + expected: 'Object', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: null, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid nested entries', () => { + const input = { + key1: 'value', + key2: 'value', + nested: { + key1: 123, + key2: null, + }, + }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: 'value', + expected: 'number', + received: '"value"', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: input.key2, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: 123, + expected: 'string', + received: '123', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key1', + value: input.nested.key1, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: null, + expected: 'number', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key2', + value: input.nested.key2, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries with abort early', () => { + const input = { key1: false, key2: 123, nested: null }; + expect( + schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid exact optional entry', () => { + const schema = looseObject({ key: exactOptional(string()) }); + const input = { key: undefined }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); }); }); }); diff --git a/library/src/schemas/looseObject/looseObject.ts b/library/src/schemas/looseObject/looseObject.ts index b107e9654..9219e88c3 100644 --- a/library/src/schemas/looseObject/looseObject.ts +++ b/library/src/schemas/looseObject/looseObject.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseSchema, ErrorMessage, @@ -102,58 +103,95 @@ export function looseObject( dataset.typed = true; dataset.value = {}; - // Parse schema of each entry - // Hint: We do not distinguish between missing and `undefined` entries. - // The reason for this decision is that it reduces the bundle size, and - // we also expect that most users will expect this behavior. + // Process each object entry of schema for (const key in this.entries) { - // Get and parse value of key - const value = input[key as keyof typeof input]; - const valueDataset = this.entries[key]['~run']({ value }, config); - - // If there are issues, capture them - if (valueDataset.issues) { - // Create object path item - const pathItem: ObjectPathItem = { - type: 'object', - origin: 'value', - input: input as Record, - key, - value, - }; - - // Add modified entry dataset issues to issues - for (const issue of valueDataset.issues) { - if (issue.path) { - issue.path.unshift(pathItem); - } else { + const valueSchema = this.entries[key]; + + // If key is present or its an optional schema with a default value, + // parse input of key or default value + if ( + key in input || + ((valueSchema.type === 'exact_optional' || + valueSchema.type === 'optional' || + valueSchema.type === 'nullish') && + // @ts-expect-error + valueSchema.default !== undefined) + ) { + const value: unknown = + key in input + ? // @ts-expect-error + input[key] + : getDefault(valueSchema); + const valueDataset = valueSchema['~run']({ value }, config); + + // If there are issues, capture them + if (valueDataset.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + type: 'object', + origin: 'value', + input: input as Record, + key, + value, + }; + + // Add modified entry dataset issues to issues + for (const issue of valueDataset.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + // @ts-expect-error + issue.path = [pathItem]; + } // @ts-expect-error - issue.path = [pathItem]; + dataset.issues?.push(issue); + } + if (!dataset.issues) { + // @ts-expect-error + dataset.issues = valueDataset.issues; + } + + // If necessary, abort early + if (config.abortEarly) { + dataset.typed = false; + break; } - // @ts-expect-error - dataset.issues?.push(issue); - } - if (!dataset.issues) { - // @ts-expect-error - dataset.issues = valueDataset.issues; } - // If necessary, abort early - if (config.abortEarly) { + // If not typed, set typed to `false` + if (!valueDataset.typed) { dataset.typed = false; - break; } - } - // If not typed, set typed to `false` - if (!valueDataset.typed) { - dataset.typed = false; - } - - // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { + // Add entry to dataset // @ts-expect-error dataset.value[key] = valueDataset.value; + + // Otherwise, if key is missing and required, add issue + } else if ( + valueSchema.type !== 'exact_optional' && + valueSchema.type !== 'optional' && + valueSchema.type !== 'nullish' + ) { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + // @ts-expect-error + value: input[key], + }, + ], + }); + + // If necessary, abort early + if (config.abortEarly) { + break; + } } } diff --git a/library/src/schemas/looseObject/looseObjectAsync.test-d.ts b/library/src/schemas/looseObject/looseObjectAsync.test-d.ts index ec1ec7e5b..d4bf284f0 100644 --- a/library/src/schemas/looseObject/looseObjectAsync.test-d.ts +++ b/library/src/schemas/looseObject/looseObjectAsync.test-d.ts @@ -1,17 +1,26 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { ReadonlyAction, TransformAction } from '../../actions/index.ts'; -import type { SchemaWithPipe } from '../../methods/index.ts'; +import type { + SchemaWithPipe, + SchemaWithPipeAsync, +} from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; -import type { NullishSchema } from '../nullish/index.ts'; +import type { AnySchema } from '../any/index.ts'; +import type { + ExactOptionalSchema, + ExactOptionalSchemaAsync, +} from '../exactOptional/index.ts'; +import type { NullishSchema, NullishSchemaAsync } from '../nullish/index.ts'; import { type NumberIssue, type NumberSchema } from '../number/index.ts'; -import type { ObjectIssue, ObjectSchema } from '../object/index.ts'; -import type { OptionalSchema } from '../optional/index.ts'; +import type { ObjectIssue, ObjectSchemaAsync } from '../object/index.ts'; +import type { OptionalSchema, OptionalSchemaAsync } from '../optional/index.ts'; import { string, type StringIssue, type StringSchema, } from '../string/index.ts'; import type { UndefinedableSchema } from '../undefinedable/index.ts'; +import type { UnknownSchema } from '../unknown/index.ts'; import { looseObjectAsync, type LooseObjectSchemaAsync, @@ -47,18 +56,91 @@ describe('looseObjectAsync', () => { describe('should infer correct types', () => { type Schema = LooseObjectSchemaAsync< { - key1: StringSchema; - key2: OptionalSchema, 'foo'>; - key3: NullishSchema, undefined>; - key4: ObjectSchema<{ key: NumberSchema }, undefined>; - key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; - key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipe< + key00: StringSchema; + key01: AnySchema; + key02: UnknownSchema; + key03: ObjectSchemaAsync<{ key: NumberSchema }, undefined>; + key04: SchemaWithPipe< + [StringSchema, ReadonlyAction] + >; + key05: UndefinedableSchema, 'bar'>; + key06: SchemaWithPipe< + [ + OptionalSchema, undefined>, + TransformAction, + ] + >; + key07: SchemaWithPipeAsync< [ OptionalSchema, undefined>, - TransformAction, + TransformAction, + ] + >; + key08: SchemaWithPipeAsync< + [ + OptionalSchemaAsync, undefined>, + TransformAction, ] >; + + // ExactOptionalSchema + key10: ExactOptionalSchema, undefined>; + key11: ExactOptionalSchema, 'foo'>; + key12: ExactOptionalSchema, () => 'foo'>; + + // ExactOptionalSchemaAsync + key20: ExactOptionalSchemaAsync, undefined>; + key21: ExactOptionalSchemaAsync, 'foo'>; + key22: ExactOptionalSchemaAsync, () => 'foo'>; + key23: ExactOptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + + // OptionalSchema + key30: OptionalSchema, undefined>; + key31: OptionalSchema, 'foo'>; + key32: OptionalSchema, () => undefined>; + key33: OptionalSchema, () => 'foo'>; + + // OptionalSchemaAsync + key40: OptionalSchemaAsync, undefined>; + key41: OptionalSchemaAsync, 'foo'>; + key42: OptionalSchemaAsync, () => undefined>; + key43: OptionalSchemaAsync, () => 'foo'>; + key44: OptionalSchemaAsync< + StringSchema, + () => Promise + >; + key45: OptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + + // NullishSchema + key50: NullishSchema, undefined>; + key51: NullishSchema, null>; + key52: NullishSchema, 'foo'>; + key53: NullishSchema, () => undefined>; + key54: NullishSchema, () => null>; + key55: NullishSchema, () => 'foo'>; + + // NullishSchemaAsync + key60: NullishSchemaAsync, undefined>; + key61: NullishSchemaAsync, null>; + key62: NullishSchemaAsync, 'foo'>; + key63: NullishSchemaAsync, () => undefined>; + key64: NullishSchemaAsync, () => null>; + key65: NullishSchemaAsync, () => 'foo'>; + key66: NullishSchemaAsync< + StringSchema, + () => Promise + >; + key67: NullishSchemaAsync, () => Promise>; + key68: NullishSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; }, undefined >; @@ -66,13 +148,60 @@ describe('looseObjectAsync', () => { test('of input', () => { expectTypeOf>().toEqualTypeOf< { - key1: string; - key2?: string; - key3?: string | null | undefined; - key4: { key: number }; - key5: string; - key6: string | undefined; - key7?: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + key04: string; + key05: string | undefined; + key06?: string | undefined; + key07?: string | undefined; + key08?: string | undefined; + + // ExactOptionalSchema + key10?: string; + key11?: string; + key12?: string; + + // ExactOptionalSchemaAsync + key20?: string; + key21?: string; + key22?: string; + key23?: string; + + // OptionalSchema + key30?: string | undefined; + key31?: string | undefined; + key32?: string | undefined; + key33?: string | undefined; + + // OptionalSchemaAsync + key40?: string | undefined; + key41?: string | undefined; + key42?: string | undefined; + key43?: string | undefined; + key44?: string | undefined; + key45?: string | undefined; + + // NullishSchema + key50?: string | null | undefined; + key51?: string | null | undefined; + key52?: string | null | undefined; + key53?: string | null | undefined; + key54?: string | null | undefined; + key55?: string | null | undefined; + + // NullishSchemaAsync + key60?: string | null | undefined; + key61?: string | null | undefined; + key62?: string | null | undefined; + key63?: string | null | undefined; + key64?: string | null | undefined; + key65?: string | null | undefined; + key66?: string | null | undefined; + key67?: string | null | undefined; + key68?: string | null | undefined; } & { [key: string]: unknown } >(); }); @@ -80,13 +209,60 @@ describe('looseObjectAsync', () => { test('of output', () => { expectTypeOf>().toEqualTypeOf< { - key1: string; - key2: string; - key3?: string | null | undefined; - key4: { key: number }; - readonly key5: string; - key6: string; - key7: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + readonly key04: string; + key05: string; + key06?: number; + key07?: number; + key08?: number; + + // ExactOptionalSchema + key10?: string; + key11: string; + key12: string; + + // ExactOptionalSchemaAsync + key20?: string; + key21: string; + key22: string; + key23: string; + + // OptionalSchema + key30?: string | undefined; + key31: string; + key32: string | undefined; + key33: string; + + // OptionalSchemaAsync + key40?: string | undefined; + key41: string; + key42: string | undefined; + key43: string; + key44: string | undefined; + key45: string; + + // NullishSchema + key50?: string | null | undefined; + key51: string | null; + key52: string; + key53: string | undefined; + key54: string | null; + key55: string; + + // NullishSchemaAsync + key60?: string | null | undefined; + key61: string | null; + key62: string; + key63: string | undefined; + key64: string | null; + key65: string; + key66: string | undefined; + key67: string | null; + key68: string; } & { [key: string]: unknown } >(); }); diff --git a/library/src/schemas/looseObject/looseObjectAsync.test.ts b/library/src/schemas/looseObject/looseObjectAsync.test.ts index e29777262..27b908fdc 100644 --- a/library/src/schemas/looseObject/looseObjectAsync.test.ts +++ b/library/src/schemas/looseObject/looseObjectAsync.test.ts @@ -4,11 +4,14 @@ import { expectNoSchemaIssueAsync, expectSchemaIssueAsync, } from '../../vitest/index.ts'; -import { nullish } from '../nullish/index.ts'; +import { any } from '../any/index.ts'; +import { exactOptional, exactOptionalAsync } from '../exactOptional/index.ts'; +import { nullish, nullishAsync } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { objectAsync } from '../object/index.ts'; -import { optional } from '../optional/index.ts'; -import { string, type StringIssue } from '../string/index.ts'; +import { optional, optionalAsync } from '../optional/index.ts'; +import { string } from '../string/index.ts'; +import { unknown } from '../unknown/index.ts'; import { looseObjectAsync, type LooseObjectSchemaAsync, @@ -157,23 +160,264 @@ describe('looseObjectAsync', () => { ); }); + test('for exact optional entry', async () => { + await expectNoSchemaIssueAsync( + looseObjectAsync({ key: exactOptional(string()) }), + [{}, { key: 'foo' }] + ); + await expectNoSchemaIssueAsync( + looseObjectAsync({ key: exactOptionalAsync(string()) }), + [{}, { key: 'foo' }] + ); + }); + + test('for exact optional entry with default', async () => { + // Sync + expect( + await looseObjectAsync({ key: exactOptional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ key: exactOptional(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + + // Async + expect( + await looseObjectAsync({ key: exactOptionalAsync(string(), 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ + key: exactOptionalAsync(string(), () => 'foo'), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + }); + test('for optional entry', async () => { await expectNoSchemaIssueAsync( looseObjectAsync({ key: optional(string()) }), - [ - {}, - // @ts-expect-error - { key: undefined }, - { key: 'foo' }, - ] + [{}, { key: undefined }, { key: 'foo' }] + ); + await expectNoSchemaIssueAsync( + looseObjectAsync({ key: optionalAsync(string()) }), + [{}, { key: undefined }, { key: 'foo' }] ); }); + test('for optional entry with default', async () => { + // Sync + expect( + await looseObjectAsync({ key: optional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ key: optional(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ + key: optional(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + + // Async + expect( + await looseObjectAsync({ key: optionalAsync(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ key: optionalAsync(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ + key: optionalAsync(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + expect( + await looseObjectAsync({ + key: optionalAsync(string(), async () => 'foo'), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ + key: optionalAsync(string(), async () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); + test('for nullish entry', async () => { await expectNoSchemaIssueAsync( looseObjectAsync({ key: nullish(number()) }), [{}, { key: undefined }, { key: null }, { key: 123 }] ); + await expectNoSchemaIssueAsync( + looseObjectAsync({ key: nullishAsync(number()) }), + [{}, { key: undefined }, { key: null }, { key: 123 }] + ); + }); + + test('for nullish entry with default', async () => { + // Sync + expect( + await looseObjectAsync({ key: nullish(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ key: nullish(string(), null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await looseObjectAsync({ key: nullish(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ key: nullish(string(), () => null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await looseObjectAsync({ key: nullish(string(), () => undefined) })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + + // Async + expect( + await looseObjectAsync({ key: nullishAsync(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ key: nullishAsync(string(), null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await looseObjectAsync({ key: nullishAsync(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ key: nullishAsync(string(), () => null) })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await looseObjectAsync({ + key: nullishAsync(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + expect( + await looseObjectAsync({ + key: nullishAsync(string(), async () => 'foo'), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await looseObjectAsync({ + key: nullishAsync(string(), async () => null), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await looseObjectAsync({ + key: nullishAsync(string(), async () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); }); test('for unknown entries', async () => { @@ -186,8 +430,9 @@ describe('looseObjectAsync', () => { describe('should return dataset with nested issues', () => { const schema = looseObjectAsync({ - key: string(), - nested: objectAsync({ key: number() }), + key1: string(), + key2: number(), + nested: looseObjectAsync({ key1: string(), key2: number() }), }); const baseInfo = { @@ -199,42 +444,41 @@ describe('looseObjectAsync', () => { abortPipeEarly: undefined, }; - const stringIssue: StringIssue = { - ...baseInfo, - kind: 'schema', - type: 'string', - input: undefined, - expected: 'string', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'value', - input: {}, - key: 'key', - value: undefined, - }, - ], - }; - test('for missing entries', async () => { - expect(await schema['~run']({ value: {} }, {})).toStrictEqual({ + const input = { key2: 123 }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: {}, + value: input, issues: [ - stringIssue, { ...baseInfo, kind: 'schema', - type: 'object', + type: 'loose_object', input: undefined, - expected: 'Object', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', - input: {}, + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"nested"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, key: 'nested', value: undefined, }, @@ -245,7 +489,7 @@ describe('looseObjectAsync', () => { }); test('for missing nested entries', async () => { - const input = { key: 'value', nested: {} }; + const input = { key1: 'value', nested: {} }; expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, value: input, @@ -253,38 +497,347 @@ describe('looseObjectAsync', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'loose_object', input: undefined, - expected: 'number', + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key1"', received: 'undefined', path: [ { type: 'object', origin: 'value', - input: { key: 'value', nested: {} }, + input, key: 'nested', value: {}, }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ { type: 'object', origin: 'value', + input, + key: 'nested', + value: {}, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing entries with abort early', async () => { + const input = { key2: 123 }; + expect( + await schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing any and unknown entry', async () => { + const schema = looseObjectAsync({ key1: any(), key2: unknown() }); + expect(await schema['~run']({ value: {} }, {})).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', input: {}, - key: 'key', + key: 'key1', value: undefined, }, ], }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries', async () => { + const input = { key1: false, key2: 123, nested: null }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'loose_object', + input: null, + expected: 'Object', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: null, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid nested entries', async () => { + const input = { + key1: 'value', + key2: 'value', + nested: { + key1: 123, + key2: null, + }, + }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: 'value', + expected: 'number', + received: '"value"', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: input.key2, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: 123, + expected: 'string', + received: '123', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key1', + value: input.nested.key1, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: null, + expected: 'number', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key2', + value: input.nested.key2, + }, + ], + }, ], } satisfies FailureDataset>); }); - test('with abort early', async () => { + test('for invalid entries with abort early', async () => { + const input = { key1: false, key2: 123, nested: null }; expect( - await schema['~run']({ value: {} }, { abortEarly: true }) + await schema['~run']({ value: input }, { abortEarly: true }) ).toStrictEqual({ typed: false, value: {}, - issues: [{ ...stringIssue, abortEarly: true }], + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid exact optional entry', async () => { + const schema = looseObjectAsync({ + key1: exactOptional(string()), + key2: exactOptionalAsync(string()), + }); + const input = { key1: undefined, key2: undefined }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: undefined, + }, + ], + }, + ], } satisfies FailureDataset>); }); }); diff --git a/library/src/schemas/looseObject/looseObjectAsync.ts b/library/src/schemas/looseObject/looseObjectAsync.ts index 46c275a9e..661e12b81 100644 --- a/library/src/schemas/looseObject/looseObjectAsync.ts +++ b/library/src/schemas/looseObject/looseObjectAsync.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseSchemaAsync, ErrorMessage, @@ -13,6 +14,7 @@ import { _getStandardProps, _isValidObjectKey, } from '../../utils/index.ts'; +import type { looseObject } from './looseObject.ts'; import type { LooseObjectIssue } from './types.ts'; /** @@ -33,7 +35,7 @@ export interface LooseObjectSchemaAsync< /** * The schema reference. */ - readonly reference: typeof looseObjectAsync; + readonly reference: typeof looseObject | typeof looseObjectAsync; /** * The expected property. */ @@ -105,66 +107,112 @@ export function looseObjectAsync( dataset.typed = true; dataset.value = {}; - // Parse schema of each entry - // Hint: We do not distinguish between missing and `undefined` entries. - // The reason for this decision is that it reduces the bundle size, and - // we also expect that most users will expect this behavior. + // If key is present or its an optional schema with a default value, + // parse input of key or default value asynchronously const valueDatasets = await Promise.all( - Object.entries(this.entries).map(async ([key, schema]) => { - const value = input[key as keyof typeof input]; + Object.entries(this.entries).map(async ([key, valueSchema]) => { + if ( + key in input || + ((valueSchema.type === 'exact_optional' || + valueSchema.type === 'optional' || + valueSchema.type === 'nullish') && + // @ts-expect-error + valueSchema.default !== undefined) + ) { + const value: unknown = + key in input + ? // @ts-expect-error + input[key] + : await getDefault(valueSchema); + return [ + key, + value, + valueSchema, + await valueSchema['~run']({ value }, config), + ] as const; + } return [ key, - value, - await schema['~run']({ value }, config), + // @ts-expect-error + input[key] as unknown, + valueSchema, + null, ] as const; }) ); - // Process each value dataset - for (const [key, value, valueDataset] of valueDatasets) { - // If there are issues, capture them - if (valueDataset.issues) { - // Create object path item - const pathItem: ObjectPathItem = { - type: 'object', - origin: 'value', - input: input as Record, - key, - value, - }; - - // Add modified entry dataset issues to issues - for (const issue of valueDataset.issues) { - if (issue.path) { - issue.path.unshift(pathItem); - } else { + // Process each object entry of schema + for (const [key, value, valueSchema, valueDataset] of valueDatasets) { + // If key is present or its an optional schema with a default value, + // process its value dataset + if (valueDataset) { + // If there are issues, capture them + if (valueDataset.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + type: 'object', + origin: 'value', + input: input as Record, + key, + value, + }; + + // Add modified entry dataset issues to issues + for (const issue of valueDataset.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + // @ts-expect-error + issue.path = [pathItem]; + } // @ts-expect-error - issue.path = [pathItem]; + dataset.issues?.push(issue); + } + if (!dataset.issues) { + // @ts-expect-error + dataset.issues = valueDataset.issues; + } + + // If necessary, abort early + if (config.abortEarly) { + dataset.typed = false; + break; } - // @ts-expect-error - dataset.issues?.push(issue); - } - if (!dataset.issues) { - // @ts-expect-error - dataset.issues = valueDataset.issues; } - // If necessary, abort early - if (config.abortEarly) { + // If not typed, set typed to `false` + if (!valueDataset.typed) { dataset.typed = false; - break; } - } - - // If not typed, set typed to `false` - if (!valueDataset.typed) { - dataset.typed = false; - } - // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { + // Add entry to dataset // @ts-expect-error dataset.value[key] = valueDataset.value; + + // Otherwise, if key is missing and required, add issue + } else if ( + valueSchema.type !== 'exact_optional' && + valueSchema.type !== 'optional' && + valueSchema.type !== 'nullish' + ) { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + value, + }, + ], + }); + + // If necessary, abort early + if (config.abortEarly) { + break; + } } } diff --git a/library/src/schemas/looseObject/types.ts b/library/src/schemas/looseObject/types.ts index 52bd59101..ccfa50f02 100644 --- a/library/src/schemas/looseObject/types.ts +++ b/library/src/schemas/looseObject/types.ts @@ -15,5 +15,5 @@ export interface LooseObjectIssue extends BaseIssue { /** * The expected property. */ - readonly expected: 'Object'; + readonly expected: 'Object' | `"${string}"`; } diff --git a/library/src/schemas/looseTuple/looseTupleAsync.ts b/library/src/schemas/looseTuple/looseTupleAsync.ts index 06e05046e..865dc8692 100644 --- a/library/src/schemas/looseTuple/looseTupleAsync.ts +++ b/library/src/schemas/looseTuple/looseTupleAsync.ts @@ -10,6 +10,7 @@ import type { TupleItemsAsync, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { looseTuple } from './looseTuple.ts'; import type { LooseTupleIssue } from './types.ts'; /** @@ -30,7 +31,7 @@ export interface LooseTupleSchemaAsync< /** * The schema reference. */ - readonly reference: typeof looseTupleAsync; + readonly reference: typeof looseTuple | typeof looseTupleAsync; /** * The expected property. */ diff --git a/library/src/schemas/map/mapAsync.ts b/library/src/schemas/map/mapAsync.ts index fc684d1b3..0da49dfd2 100644 --- a/library/src/schemas/map/mapAsync.ts +++ b/library/src/schemas/map/mapAsync.ts @@ -8,6 +8,7 @@ import type { OutputDataset, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { map } from './map.ts'; import type { InferMapInput, InferMapOutput, MapIssue } from './types.ts'; /** @@ -33,7 +34,7 @@ export interface MapSchemaAsync< /** * The schema reference. */ - readonly reference: typeof mapAsync; + readonly reference: typeof map | typeof mapAsync; /** * The expected property. */ diff --git a/library/src/schemas/nonNullable/nonNullableAsync.ts b/library/src/schemas/nonNullable/nonNullableAsync.ts index a91a08de7..3bd67b0be 100644 --- a/library/src/schemas/nonNullable/nonNullableAsync.ts +++ b/library/src/schemas/nonNullable/nonNullableAsync.ts @@ -6,6 +6,7 @@ import type { OutputDataset, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { nonNullable } from './nonNullable.ts'; import type { InferNonNullableInput, InferNonNullableIssue, @@ -33,7 +34,7 @@ export interface NonNullableSchemaAsync< /** * The schema reference. */ - readonly reference: typeof nonNullableAsync; + readonly reference: typeof nonNullable | typeof nonNullableAsync; /** * The expected property. */ diff --git a/library/src/schemas/nonNullish/nonNullishAsync.ts b/library/src/schemas/nonNullish/nonNullishAsync.ts index 54f9f672d..3e82b0ec8 100644 --- a/library/src/schemas/nonNullish/nonNullishAsync.ts +++ b/library/src/schemas/nonNullish/nonNullishAsync.ts @@ -6,6 +6,7 @@ import type { OutputDataset, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { nonNullish } from './nonNullish.ts'; import type { InferNonNullishInput, InferNonNullishIssue, @@ -33,7 +34,7 @@ export interface NonNullishSchemaAsync< /** * The schema reference. */ - readonly reference: typeof nonNullishAsync; + readonly reference: typeof nonNullish | typeof nonNullishAsync; /** * The expected property. */ diff --git a/library/src/schemas/nonOptional/nonOptionalAsync.ts b/library/src/schemas/nonOptional/nonOptionalAsync.ts index f7a75b433..1ae66b16c 100644 --- a/library/src/schemas/nonOptional/nonOptionalAsync.ts +++ b/library/src/schemas/nonOptional/nonOptionalAsync.ts @@ -6,6 +6,7 @@ import type { OutputDataset, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { nonOptional } from './nonOptional.ts'; import type { InferNonOptionalInput, InferNonOptionalIssue, @@ -33,7 +34,7 @@ export interface NonOptionalSchemaAsync< /** * The schema reference. */ - readonly reference: typeof nonOptionalAsync; + readonly reference: typeof nonOptional | typeof nonOptionalAsync; /** * The expected property. */ diff --git a/library/src/schemas/nullable/nullable.test.ts b/library/src/schemas/nullable/nullable.test.ts index 2e0b126aa..a83450a9d 100644 --- a/library/src/schemas/nullable/nullable.test.ts +++ b/library/src/schemas/nullable/nullable.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from 'vitest'; -import { expectNoSchemaIssue } from '../../vitest/index.ts'; -import { string, type StringSchema } from '../string/index.ts'; +import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; import { nullable, type NullableSchema } from './nullable.ts'; describe('nullable', () => { @@ -83,6 +87,24 @@ describe('nullable', () => { }); }); + describe('should return dataset with issues', () => { + const schema = nullable(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', () => { + expectSchemaIssue(schema, baseIssue, [123, true, {}]); + }); + + test('for undefined', () => { + expectSchemaIssue(schema, baseIssue, [undefined]); + }); + }); + describe('should return dataset without default', () => { test('for undefined default', () => { expectNoSchemaIssue(nullable(string()), [null, 'foo']); diff --git a/library/src/schemas/nullable/nullableAsync.test.ts b/library/src/schemas/nullable/nullableAsync.test.ts index 1fed617f0..bdbd0e373 100644 --- a/library/src/schemas/nullable/nullableAsync.test.ts +++ b/library/src/schemas/nullable/nullableAsync.test.ts @@ -1,6 +1,13 @@ import { describe, expect, test } from 'vitest'; -import { expectNoSchemaIssueAsync } from '../../vitest/index.ts'; -import { string, type StringSchema } from '../string/index.ts'; +import { + expectNoSchemaIssueAsync, + expectSchemaIssueAsync, +} from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; import { nullableAsync, type NullableSchemaAsync } from './nullableAsync.ts'; describe('nullableAsync', () => { @@ -102,6 +109,24 @@ describe('nullableAsync', () => { }); }); + describe('should return dataset with issues', () => { + const schema = nullableAsync(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [123, true, {}]); + }); + + test('for undefined', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [undefined]); + }); + }); + describe('should return dataset without default', () => { test('for undefined default', async () => { await expectNoSchemaIssueAsync(nullableAsync(string()), [null, 'foo']); diff --git a/library/src/schemas/nullable/nullableAsync.ts b/library/src/schemas/nullable/nullableAsync.ts index fa493dc36..1cb0d9ca2 100644 --- a/library/src/schemas/nullable/nullableAsync.ts +++ b/library/src/schemas/nullable/nullableAsync.ts @@ -9,6 +9,7 @@ import type { SuccessDataset, } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; +import type { nullable } from './nullable.ts'; import type { InferNullableOutput } from './types.ts'; /** @@ -31,7 +32,7 @@ export interface NullableSchemaAsync< /** * The schema reference. */ - readonly reference: typeof nullableAsync; + readonly reference: typeof nullable | typeof nullableAsync; /** * The expected property. */ diff --git a/library/src/schemas/nullish/nullish.test.ts b/library/src/schemas/nullish/nullish.test.ts index fe3666325..54641fc43 100644 --- a/library/src/schemas/nullish/nullish.test.ts +++ b/library/src/schemas/nullish/nullish.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from 'vitest'; -import { expectNoSchemaIssue } from '../../vitest/index.ts'; -import { string, type StringSchema } from '../string/index.ts'; +import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; import { nullish, type NullishSchema } from './nullish.ts'; describe('nullish', () => { @@ -95,6 +99,20 @@ describe('nullish', () => { }); }); + describe('should return dataset with issues', () => { + const schema = nullish(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', () => { + expectSchemaIssue(schema, baseIssue, [123, true, {}]); + }); + }); + describe('should return dataset without default', () => { test('for undefined default', () => { expectNoSchemaIssue(nullish(string()), [undefined, null, 'foo']); diff --git a/library/src/schemas/nullish/nullishAsync.test.ts b/library/src/schemas/nullish/nullishAsync.test.ts index 665d5c08d..a1e3c4b29 100644 --- a/library/src/schemas/nullish/nullishAsync.test.ts +++ b/library/src/schemas/nullish/nullishAsync.test.ts @@ -1,6 +1,13 @@ import { describe, expect, test } from 'vitest'; -import { expectNoSchemaIssueAsync } from '../../vitest/index.ts'; -import { string, type StringSchema } from '../string/index.ts'; +import { + expectNoSchemaIssueAsync, + expectSchemaIssueAsync, +} from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; import { nullishAsync, type NullishSchemaAsync } from './nullishAsync.ts'; describe('nullishAsync', () => { @@ -119,6 +126,20 @@ describe('nullishAsync', () => { }); }); + describe('should return dataset with issues', () => { + const schema = nullishAsync(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [123, true, {}]); + }); + }); + describe('should return dataset without default', () => { test('for undefined default', async () => { await expectNoSchemaIssueAsync(nullishAsync(string()), [ diff --git a/library/src/schemas/nullish/nullishAsync.ts b/library/src/schemas/nullish/nullishAsync.ts index 30c5c7eb9..c60b36941 100644 --- a/library/src/schemas/nullish/nullishAsync.ts +++ b/library/src/schemas/nullish/nullishAsync.ts @@ -9,6 +9,7 @@ import type { SuccessDataset, } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; +import type { nullish } from './nullish.ts'; import type { InferNullishOutput } from './types.ts'; /** @@ -31,7 +32,7 @@ export interface NullishSchemaAsync< /** * The schema reference. */ - readonly reference: typeof nullishAsync; + readonly reference: typeof nullish | typeof nullishAsync; /** * The expected property. */ diff --git a/library/src/schemas/object/object.test-d.ts b/library/src/schemas/object/object.test-d.ts index ddbaf7e56..344d73f30 100644 --- a/library/src/schemas/object/object.test-d.ts +++ b/library/src/schemas/object/object.test-d.ts @@ -2,6 +2,8 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { ReadonlyAction, TransformAction } from '../../actions/index.ts'; import type { SchemaWithPipe } from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import type { AnySchema } from '../any/index.ts'; +import type { ExactOptionalSchema } from '../exactOptional/index.ts'; import type { NullishSchema } from '../nullish/index.ts'; import type { NumberIssue, NumberSchema } from '../number/index.ts'; import type { OptionalSchema } from '../optional/index.ts'; @@ -11,6 +13,7 @@ import { type StringSchema, } from '../string/index.ts'; import type { UndefinedableSchema } from '../undefinedable/index.ts'; +import type { UnknownSchema } from '../unknown/index.ts'; import { object, type ObjectSchema } from './object.ts'; import type { ObjectIssue } from './types.ts'; @@ -41,43 +44,104 @@ describe('object', () => { describe('should infer correct types', () => { type Schema = ObjectSchema< { - key1: StringSchema; - key2: OptionalSchema, 'foo'>; - key3: NullishSchema, undefined>; - key4: ObjectSchema<{ key: NumberSchema }, undefined>; - key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; - key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipe< + key00: StringSchema; + key01: AnySchema; + key02: UnknownSchema; + key03: ObjectSchema<{ key: NumberSchema }, undefined>; + key04: SchemaWithPipe< + [StringSchema, ReadonlyAction] + >; + key05: UndefinedableSchema, 'bar'>; + key06: SchemaWithPipe< [ - OptionalSchema, () => 'foo'>, - TransformAction, + OptionalSchema, undefined>, + TransformAction, ] >; + + // ExactOptionalSchema + key10: ExactOptionalSchema, undefined>; + key11: ExactOptionalSchema, 'foo'>; + key12: ExactOptionalSchema, () => 'foo'>; + + // OptionalSchema + key20: OptionalSchema, undefined>; + key21: OptionalSchema, 'foo'>; + key22: OptionalSchema, () => undefined>; + key23: OptionalSchema, () => 'foo'>; + + // NullishSchema + key30: NullishSchema, undefined>; + key31: NullishSchema, null>; + key32: NullishSchema, 'foo'>; + key33: NullishSchema, () => undefined>; + key34: NullishSchema, () => null>; + key35: NullishSchema, () => 'foo'>; }, undefined >; test('of input', () => { expectTypeOf>().toEqualTypeOf<{ - key1: string; - key2?: string; - key3: string | null | undefined; - key4: { key: number }; - key5: string; - key6: string | undefined; - key7?: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + key04: string; + key05: string | undefined; + key06?: string | undefined; + + // ExactOptionalSchema + key10?: string; + key11?: string; + key12?: string; + + // OptionalSchema + key20?: string | undefined; + key21?: string | undefined; + key22?: string | undefined; + key23?: string | undefined; + + // NullishSchema + key30?: string | null | undefined; + key31?: string | null | undefined; + key32?: string | null | undefined; + key33?: string | null | undefined; + key34?: string | null | undefined; + key35?: string | null | undefined; }>(); }); test('of output', () => { expectTypeOf>().toEqualTypeOf<{ - key1: string; - key2: string; - key3: string | null | undefined; - key4: { key: number }; - readonly key5: string; - key6: string; - key7: number; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + readonly key04: string; + key05: string; + key06?: number; + + // ExactOptionalSchema + key10?: string; + key11: string; + key12: string; + + // OptionalSchema + key20?: string | undefined; + key21: string; + key22: string | undefined; + key23: string; + + // NullishSchema + key30?: string | null | undefined; + key31: string | null; + key32: string; + key33: string | undefined; + key34: string | null; + key35: string; }>(); }); diff --git a/library/src/schemas/object/object.test.ts b/library/src/schemas/object/object.test.ts index 47affc64a..dc6eedd9a 100644 --- a/library/src/schemas/object/object.test.ts +++ b/library/src/schemas/object/object.test.ts @@ -1,12 +1,13 @@ import { describe, expect, test } from 'vitest'; import type { FailureDataset, InferIssue } from '../../types/index.ts'; import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { any } from '../any/index.ts'; +import { exactOptional } from '../exactOptional/exactOptional.ts'; import { nullish } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { optional } from '../optional/index.ts'; import { string } from '../string/index.ts'; -import { undefined_ } from '../undefined/index.ts'; -import { union } from '../union/index.ts'; +import { unknown } from '../unknown/index.ts'; import { object, type ObjectSchema } from './object.ts'; import type { ObjectIssue } from './types.ts'; @@ -144,9 +145,38 @@ describe('object', () => { ]); }); + test('for exact optional entry', () => { + expectNoSchemaIssue(object({ key: exactOptional(string()) }), [ + {}, + { key: 'foo' }, + ]); + }); + + test('for exact optional entry with default', () => { + expect( + object({ key: exactOptional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + object({ key: exactOptional(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + }); + test('for optional entry', () => { expectNoSchemaIssue(object({ key: optional(string()) }), [ {}, + { key: undefined }, { key: 'foo' }, ]); }); @@ -158,9 +188,18 @@ describe('object', () => { typed: true, value: { key: 'foo' }, }); + expect( + object({ key: optional(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); expect( object({ - key: optional(union([string(), undefined_()]), () => undefined), + key: optional(string(), () => undefined), })['~run']({ value: {} }, {}) ).toStrictEqual({ typed: true, @@ -170,12 +209,55 @@ describe('object', () => { test('for nullish entry', () => { expectNoSchemaIssue(object({ key: nullish(number()) }), [ + {}, { key: undefined }, { key: null }, { key: 123 }, ]); }); + test('for nullish entry with default', () => { + expect( + object({ key: nullish(string(), 'foo') })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + object({ key: nullish(string(), null) })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + object({ key: nullish(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + object({ key: nullish(string(), () => null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + object({ key: nullish(string(), () => undefined) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); + test('for unknown entries', () => { expect( object({ key1: string() })['~run']( @@ -354,6 +436,50 @@ describe('object', () => { } satisfies FailureDataset>); }); + test('for missing any and unknown entry', () => { + const schema = object({ key1: any(), key2: unknown() }); + expect(schema['~run']({ value: {} }, {})).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + test('for invalid entries', () => { const input = { key1: false, key2: 123, nested: null }; expect(schema['~run']({ value: input }, {})).toStrictEqual({ @@ -510,8 +636,8 @@ describe('object', () => { } satisfies FailureDataset>); }); - test('for undefined optional entry', () => { - const schema = object({ key: optional(string()) }); + test('for invalid exact optional entry', () => { + const schema = object({ key: exactOptional(string()) }); const input = { key: undefined }; expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, @@ -537,33 +663,5 @@ describe('object', () => { ], } satisfies FailureDataset>); }); - - test('for missing nullish entry', () => { - const schema = object({ key: nullish(string()) }); - const input = {}; - expect(schema['~run']({ value: input }, {})).toStrictEqual({ - typed: false, - value: input, - issues: [ - { - ...baseInfo, - kind: 'schema', - type: 'object', - input: undefined, - expected: '"key"', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'key', - input, - key: 'key', - value: undefined, - }, - ], - }, - ], - } satisfies FailureDataset>); - }); }); }); diff --git a/library/src/schemas/object/object.ts b/library/src/schemas/object/object.ts index d50811a8b..84a0beecc 100644 --- a/library/src/schemas/object/object.ts +++ b/library/src/schemas/object/object.ts @@ -108,20 +108,24 @@ export function object( // Process each object entry of schema for (const key in this.entries) { + const valueSchema = this.entries[key]; + // If key is present or its an optional schema with a default value, - // parse input or default value of key + // parse input of key or default value if ( key in input || - (this.entries[key].type === 'optional' && + ((valueSchema.type === 'exact_optional' || + valueSchema.type === 'optional' || + valueSchema.type === 'nullish') && // @ts-expect-error - this.entries[key].default !== undefined) + valueSchema.default !== undefined) ) { const value: unknown = key in input ? // @ts-expect-error input[key] - : getDefault(this.entries[key]); - const valueDataset = this.entries[key]['~run']({ value }, config); + : getDefault(valueSchema); + const valueDataset = valueSchema['~run']({ value }, config); // If there are issues, capture them if (valueDataset.issues) { @@ -167,7 +171,11 @@ export function object( dataset.value[key] = valueDataset.value; // Otherwise, if key is missing and required, add issue - } else if (this.entries[key].type !== 'optional') { + } else if ( + valueSchema.type !== 'exact_optional' && + valueSchema.type !== 'optional' && + valueSchema.type !== 'nullish' + ) { _addIssue(this, 'key', dataset, config, { input: undefined, expected: `"${key}"`, diff --git a/library/src/schemas/object/objectAsync.test-d.ts b/library/src/schemas/object/objectAsync.test-d.ts index c9757e693..f3faad9f0 100644 --- a/library/src/schemas/object/objectAsync.test-d.ts +++ b/library/src/schemas/object/objectAsync.test-d.ts @@ -5,7 +5,12 @@ import type { SchemaWithPipeAsync, } from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; -import type { NullishSchema } from '../nullish/index.ts'; +import type { AnySchema } from '../any/index.ts'; +import type { + ExactOptionalSchema, + ExactOptionalSchemaAsync, +} from '../exactOptional/index.ts'; +import type { NullishSchema, NullishSchemaAsync } from '../nullish/index.ts'; import type { NumberIssue, NumberSchema } from '../number/index.ts'; import type { OptionalSchema, OptionalSchemaAsync } from '../optional/index.ts'; import { @@ -14,6 +19,7 @@ import { type StringSchema, } from '../string/index.ts'; import type { UndefinedableSchema } from '../undefinedable/index.ts'; +import type { UnknownSchema } from '../unknown/index.ts'; import { objectAsync, type ObjectSchemaAsync } from './objectAsync.ts'; import type { ObjectIssue } from './types.ts'; @@ -44,43 +50,210 @@ describe('objectAsync', () => { describe('should infer correct types', () => { type Schema = ObjectSchemaAsync< { - key1: StringSchema; - key2: OptionalSchema, 'foo'>; - key3: NullishSchema, undefined>; - key4: ObjectSchemaAsync<{ key: NumberSchema }, undefined>; - key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; - key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipeAsync< + key00: StringSchema; + key01: AnySchema; + key02: UnknownSchema; + key03: ObjectSchemaAsync<{ key: NumberSchema }, undefined>; + key04: SchemaWithPipe< + [StringSchema, ReadonlyAction] + >; + key05: UndefinedableSchema, 'bar'>; + key06: SchemaWithPipe< + [ + OptionalSchema, undefined>, + TransformAction, + ] + >; + key07: SchemaWithPipeAsync< + [ + OptionalSchema, undefined>, + TransformAction, + ] + >; + key08: SchemaWithPipeAsync< [ - OptionalSchemaAsync, () => Promise<'foo'>>, - TransformAction, + OptionalSchemaAsync, undefined>, + TransformAction, ] >; + + // ExactOptionalSchema + key10: ExactOptionalSchema, undefined>; + key11: ExactOptionalSchema, 'foo'>; + key12: ExactOptionalSchema, () => 'foo'>; + + // ExactOptionalSchemaAsync + key20: ExactOptionalSchemaAsync, undefined>; + key21: ExactOptionalSchemaAsync, 'foo'>; + key22: ExactOptionalSchemaAsync, () => 'foo'>; + key23: ExactOptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + + // OptionalSchema + key30: OptionalSchema, undefined>; + key31: OptionalSchema, 'foo'>; + key32: OptionalSchema, () => undefined>; + key33: OptionalSchema, () => 'foo'>; + + // OptionalSchemaAsync + key40: OptionalSchemaAsync, undefined>; + key41: OptionalSchemaAsync, 'foo'>; + key42: OptionalSchemaAsync, () => undefined>; + key43: OptionalSchemaAsync, () => 'foo'>; + key44: OptionalSchemaAsync< + StringSchema, + () => Promise + >; + key45: OptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + + // NullishSchema + key50: NullishSchema, undefined>; + key51: NullishSchema, null>; + key52: NullishSchema, 'foo'>; + key53: NullishSchema, () => undefined>; + key54: NullishSchema, () => null>; + key55: NullishSchema, () => 'foo'>; + + // NullishSchemaAsync + key60: NullishSchemaAsync, undefined>; + key61: NullishSchemaAsync, null>; + key62: NullishSchemaAsync, 'foo'>; + key63: NullishSchemaAsync, () => undefined>; + key64: NullishSchemaAsync, () => null>; + key65: NullishSchemaAsync, () => 'foo'>; + key66: NullishSchemaAsync< + StringSchema, + () => Promise + >; + key67: NullishSchemaAsync, () => Promise>; + key68: NullishSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; }, undefined >; test('of input', () => { expectTypeOf>().toEqualTypeOf<{ - key1: string; - key2?: string; - key3: string | null | undefined; - key4: { key: number }; - key5: string; - key6: string | undefined; - key7?: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + key04: string; + key05: string | undefined; + key06?: string | undefined; + key07?: string | undefined; + key08?: string | undefined; + + // ExactOptionalSchema + key10?: string; + key11?: string; + key12?: string; + + // ExactOptionalSchemaAsync + key20?: string; + key21?: string; + key22?: string; + key23?: string; + + // OptionalSchema + key30?: string | undefined; + key31?: string | undefined; + key32?: string | undefined; + key33?: string | undefined; + + // OptionalSchemaAsync + key40?: string | undefined; + key41?: string | undefined; + key42?: string | undefined; + key43?: string | undefined; + key44?: string | undefined; + key45?: string | undefined; + + // NullishSchema + key50?: string | null | undefined; + key51?: string | null | undefined; + key52?: string | null | undefined; + key53?: string | null | undefined; + key54?: string | null | undefined; + key55?: string | null | undefined; + + // NullishSchemaAsync + key60?: string | null | undefined; + key61?: string | null | undefined; + key62?: string | null | undefined; + key63?: string | null | undefined; + key64?: string | null | undefined; + key65?: string | null | undefined; + key66?: string | null | undefined; + key67?: string | null | undefined; + key68?: string | null | undefined; }>(); }); test('of output', () => { expectTypeOf>().toEqualTypeOf<{ - key1: string; - key2: string; - key3: string | null | undefined; - key4: { key: number }; - readonly key5: string; - key6: string; - key7: number; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + readonly key04: string; + key05: string; + key06?: number; + key07?: number; + key08?: number; + + // ExactOptionalSchema + key10?: string; + key11: string; + key12: string; + + // ExactOptionalSchemaAsync + key20?: string; + key21: string; + key22: string; + key23: string; + + // OptionalSchema + key30?: string | undefined; + key31: string; + key32: string | undefined; + key33: string; + + // OptionalSchemaAsync + key40?: string | undefined; + key41: string; + key42: string | undefined; + key43: string; + key44: string | undefined; + key45: string; + + // NullishSchema + key50?: string | null | undefined; + key51: string | null; + key52: string; + key53: string | undefined; + key54: string | null; + key55: string; + + // NullishSchemaAsync + key60?: string | null | undefined; + key61: string | null; + key62: string; + key63: string | undefined; + key64: string | null; + key65: string; + key66: string | undefined; + key67: string | null; + key68: string; }>(); }); diff --git a/library/src/schemas/object/objectAsync.test.ts b/library/src/schemas/object/objectAsync.test.ts index 537e16ad4..7da2c6abe 100644 --- a/library/src/schemas/object/objectAsync.test.ts +++ b/library/src/schemas/object/objectAsync.test.ts @@ -4,12 +4,13 @@ import { expectNoSchemaIssueAsync, expectSchemaIssueAsync, } from '../../vitest/index.ts'; +import { any } from '../any/index.ts'; +import { exactOptional, exactOptionalAsync } from '../exactOptional/index.ts'; import { nullish, nullishAsync } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { optional, optionalAsync } from '../optional/index.ts'; import { string } from '../string/index.ts'; -import { undefined_ } from '../undefined/index.ts'; -import { unionAsync } from '../union/index.ts'; +import { unknown } from '../unknown/index.ts'; import { objectAsync, type ObjectSchemaAsync } from './objectAsync.ts'; import type { ObjectIssue } from './types.ts'; @@ -157,14 +158,71 @@ describe('objectAsync', () => { ); }); + test('for exact optional entry', async () => { + await expectNoSchemaIssueAsync( + objectAsync({ key: exactOptional(string()) }), + [{}, { key: 'foo' }] + ); + await expectNoSchemaIssueAsync( + objectAsync({ key: exactOptionalAsync(string()) }), + [{}, { key: 'foo' }] + ); + }); + + test('for exact optional entry with default', async () => { + // Sync + expect( + await objectAsync({ key: exactOptional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ key: exactOptional(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + + // Async + expect( + await objectAsync({ key: exactOptionalAsync(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ key: exactOptionalAsync(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + }); + test('for optional entry', async () => { await expectNoSchemaIssueAsync(objectAsync({ key: optional(string()) }), [ {}, + { key: undefined }, { key: 'foo' }, ]); + await expectNoSchemaIssueAsync( + objectAsync({ key: optionalAsync(string()) }), + [{}, { key: undefined }, { key: 'foo' }] + ); }); test('for optional entry with default', async () => { + // Sync expect( await objectAsync({ key: optional(string(), 'foo') })['~run']( { value: {} }, @@ -174,12 +232,61 @@ describe('objectAsync', () => { typed: true, value: { key: 'foo' }, }); + expect( + await objectAsync({ key: optional(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); expect( await objectAsync({ - key: optionalAsync( - unionAsync([string(), undefined_()]), - async () => undefined - ), + key: optional(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + + // Async + expect( + await objectAsync({ key: optionalAsync(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ key: optionalAsync(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ + key: optionalAsync(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + expect( + await objectAsync({ key: optionalAsync(string(), async () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ + key: optionalAsync(string(), async () => undefined), })['~run']({ value: {} }, {}) ).toStrictEqual({ typed: true, @@ -189,10 +296,134 @@ describe('objectAsync', () => { test('for nullish entry', async () => { await expectNoSchemaIssueAsync(objectAsync({ key: nullish(number()) }), [ + {}, { key: undefined }, { key: null }, { key: 123 }, ]); + await expectNoSchemaIssueAsync( + objectAsync({ key: nullishAsync(number()) }), + [{}, { key: undefined }, { key: null }, { key: 123 }] + ); + }); + + test('for nullish entry with default', async () => { + // Sync + expect( + await objectAsync({ key: nullish(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ key: nullish(string(), null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await objectAsync({ key: nullish(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ key: nullish(string(), () => null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await objectAsync({ key: nullish(string(), () => undefined) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + + // Async + expect( + await objectAsync({ key: nullishAsync(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ key: nullishAsync(string(), null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await objectAsync({ key: nullishAsync(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ key: nullishAsync(string(), () => null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await objectAsync({ key: nullishAsync(string(), () => undefined) })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + expect( + await objectAsync({ key: nullishAsync(string(), async () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectAsync({ key: nullishAsync(string(), async () => null) })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await objectAsync({ + key: nullishAsync(string(), async () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); }); test('for unknown entries', async () => { @@ -373,6 +604,50 @@ describe('objectAsync', () => { } satisfies FailureDataset>); }); + test('for missing any and unknown entry', async () => { + const schema = objectAsync({ key1: any(), key2: unknown() }); + expect(await schema['~run']({ value: {} }, {})).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + test('for invalid entries', async () => { const input = { key1: false, key2: 123, nested: null }; expect(await schema['~run']({ value: input }, {})).toStrictEqual({ @@ -529,9 +804,12 @@ describe('objectAsync', () => { } satisfies FailureDataset>); }); - test('for undefined optional entry', async () => { - const schema = objectAsync({ key: optionalAsync(string()) }); - const input = { key: undefined }; + test('for invalid exact optional entry', async () => { + const schema = objectAsync({ + key1: exactOptional(string()), + key2: exactOptionalAsync(string()), + }); + const input = { key1: undefined, key2: undefined }; expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, value: input, @@ -548,35 +826,24 @@ describe('objectAsync', () => { type: 'object', origin: 'value', input, - key: 'key', + key: 'key1', value: undefined, }, ], }, - ], - } satisfies FailureDataset>); - }); - - test('for missing nullish entry', async () => { - const schema = objectAsync({ key: nullishAsync(string()) }); - const input = {}; - expect(await schema['~run']({ value: input }, {})).toStrictEqual({ - typed: false, - value: input, - issues: [ { ...baseInfo, kind: 'schema', - type: 'object', + type: 'string', input: undefined, - expected: '"key"', + expected: 'string', received: 'undefined', path: [ { type: 'object', - origin: 'key', + origin: 'value', input, - key: 'key', + key: 'key2', value: undefined, }, ], diff --git a/library/src/schemas/object/objectAsync.ts b/library/src/schemas/object/objectAsync.ts index 68be4f394..863ef49f3 100644 --- a/library/src/schemas/object/objectAsync.ts +++ b/library/src/schemas/object/objectAsync.ts @@ -10,6 +10,7 @@ import type { OutputDataset, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { object } from './object.ts'; import type { ObjectIssue } from './types.ts'; /** @@ -30,7 +31,7 @@ export interface ObjectSchemaAsync< /** * The schema reference. */ - readonly reference: typeof objectAsync; + readonly reference: typeof object | typeof objectAsync; /** * The expected property. */ @@ -110,37 +111,41 @@ export function objectAsync( dataset.value = {}; // If key is present or its an optional schema with a default value, - // parse input or default value of key asynchronously + // parse input of key or default value asynchronously const valueDatasets = await Promise.all( - Object.entries(this.entries).map(async ([key, schema]) => { + Object.entries(this.entries).map(async ([key, valueSchema]) => { if ( key in input || - (this.entries[key].type === 'optional' && + ((valueSchema.type === 'exact_optional' || + valueSchema.type === 'optional' || + valueSchema.type === 'nullish') && // @ts-expect-error - this.entries[key].default !== undefined) + valueSchema.default !== undefined) ) { const value: unknown = key in input ? // @ts-expect-error input[key] - : await getDefault(this.entries[key]); + : await getDefault(valueSchema); return [ key, value, - await schema['~run']({ value }, config), + valueSchema, + await valueSchema['~run']({ value }, config), ] as const; } return [ key, // @ts-expect-error input[key] as unknown, + valueSchema, null, ] as const; }) ); // Process each object entry of schema - for (const [key, value, valueDataset] of valueDatasets) { + for (const [key, value, valueSchema, valueDataset] of valueDatasets) { // If key is present or its an optional schema with a default value, // process its value dataset if (valueDataset) { @@ -188,7 +193,11 @@ export function objectAsync( dataset.value[key] = valueDataset.value; // Otherwise, if key is missing and required, add issue - } else if (this.entries[key].type !== 'optional') { + } else if ( + valueSchema.type !== 'exact_optional' && + valueSchema.type !== 'optional' && + valueSchema.type !== 'nullish' + ) { _addIssue(this, 'key', dataset, config, { input: undefined, expected: `"${key}"`, diff --git a/library/src/schemas/objectWithRest/objectWithRest.test-d.ts b/library/src/schemas/objectWithRest/objectWithRest.test-d.ts index 3c5825146..8a35d67a9 100644 --- a/library/src/schemas/objectWithRest/objectWithRest.test-d.ts +++ b/library/src/schemas/objectWithRest/objectWithRest.test-d.ts @@ -2,7 +2,9 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { ReadonlyAction, TransformAction } from '../../actions/index.ts'; import type { SchemaWithPipe } from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import type { AnySchema } from '../any/index.ts'; import type { BooleanIssue, BooleanSchema } from '../boolean/index.ts'; +import type { ExactOptionalSchema } from '../exactOptional/index.ts'; import type { NullishSchema } from '../nullish/index.ts'; import { number, @@ -17,6 +19,7 @@ import { type StringSchema, } from '../string/index.ts'; import type { UndefinedableSchema } from '../undefinedable/index.ts'; +import type { UnknownSchema } from '../unknown/index.ts'; import { objectWithRest, type ObjectWithRestSchema } from './objectWithRest.ts'; import type { ObjectWithRestIssue } from './types.ts'; @@ -51,18 +54,39 @@ describe('objectWithRest', () => { describe('should infer correct types', () => { type Schema = ObjectWithRestSchema< { - key1: StringSchema; - key2: OptionalSchema, 'foo'>; - key3: NullishSchema, undefined>; - key4: ObjectSchema<{ key: NumberSchema }, undefined>; - key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; - key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipe< + key00: StringSchema; + key01: AnySchema; + key02: UnknownSchema; + key03: ObjectSchema<{ key: NumberSchema }, undefined>; + key04: SchemaWithPipe< + [StringSchema, ReadonlyAction] + >; + key05: UndefinedableSchema, 'bar'>; + key06: SchemaWithPipe< [ OptionalSchema, undefined>, - TransformAction, + TransformAction, ] >; + + // ExactOptionalSchema + key10: ExactOptionalSchema, undefined>; + key11: ExactOptionalSchema, 'foo'>; + key12: ExactOptionalSchema, () => 'foo'>; + + // OptionalSchema + key20: OptionalSchema, undefined>; + key21: OptionalSchema, 'foo'>; + key22: OptionalSchema, () => undefined>; + key23: OptionalSchema, () => 'foo'>; + + // NullishSchema + key30: NullishSchema, undefined>; + key31: NullishSchema, null>; + key32: NullishSchema, 'foo'>; + key33: NullishSchema, () => undefined>; + key34: NullishSchema, () => null>; + key35: NullishSchema, () => 'foo'>; }, BooleanSchema, undefined @@ -71,13 +95,33 @@ describe('objectWithRest', () => { test('of input', () => { expectTypeOf>().toEqualTypeOf< { - key1: string; - key2?: string; - key3?: string | null | undefined; - key4: { key: number }; - key5: string; - key6: string | undefined; - key7?: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + key04: string; + key05: string | undefined; + key06?: string | undefined; + + // ExactOptionalSchema + key10?: string; + key11?: string; + key12?: string; + + // OptionalSchema + key20?: string | undefined; + key21?: string | undefined; + key22?: string | undefined; + key23?: string | undefined; + + // NullishSchema + key30?: string | null | undefined; + key31?: string | null | undefined; + key32?: string | null | undefined; + key33?: string | null | undefined; + key34?: string | null | undefined; + key35?: string | null | undefined; } & { [key: string]: boolean } >(); }); @@ -85,13 +129,33 @@ describe('objectWithRest', () => { test('of output', () => { expectTypeOf>().toEqualTypeOf< { - key1: string; - key2: string; - key3?: string | null | undefined; - key4: { key: number }; - readonly key5: string; - key6: string; - key7: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + readonly key04: string; + key05: string; + key06?: number; + + // ExactOptionalSchema + key10?: string; + key11: string; + key12: string; + + // OptionalSchema + key20?: string | undefined; + key21: string; + key22: string | undefined; + key23: string; + + // NullishSchema + key30?: string | null | undefined; + key31: string | null; + key32: string; + key33: string | undefined; + key34: string | null; + key35: string; } & { [key: string]: boolean } >(); }); diff --git a/library/src/schemas/objectWithRest/objectWithRest.test.ts b/library/src/schemas/objectWithRest/objectWithRest.test.ts index 046da740a..a378caac8 100644 --- a/library/src/schemas/objectWithRest/objectWithRest.test.ts +++ b/library/src/schemas/objectWithRest/objectWithRest.test.ts @@ -1,15 +1,18 @@ import { describe, expect, test } from 'vitest'; import type { FailureDataset, InferIssue } from '../../types/index.ts'; import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { any } from '../any/index.ts'; import { array } from '../array/array.ts'; import type { ArrayIssue } from '../array/types.ts'; import { boolean } from '../boolean/index.ts'; +import { exactOptional } from '../exactOptional/index.ts'; import { never } from '../never/index.ts'; import { nullish } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { object } from '../object/index.ts'; import { optional } from '../optional/index.ts'; -import { string, type StringIssue } from '../string/index.ts'; +import { string } from '../string/index.ts'; +import { unknown } from '../unknown/index.ts'; import { objectWithRest, type ObjectWithRestSchema } from './objectWithRest.ts'; import type { ObjectWithRestIssue } from './types.ts'; @@ -150,6 +153,33 @@ describe('objectWithRest', () => { ); }); + test('for exact optional entry', () => { + expectNoSchemaIssue( + objectWithRest({ key: exactOptional(string()) }, number()), + // @ts-expect-error + [{}, { key: 'foo' }] + ); + }); + + test('for exact optional entry with default', () => { + expect( + objectWithRest({ key: exactOptional(string(), 'foo') }, number())[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + objectWithRest({ key: exactOptional(string(), () => 'foo') }, number())[ + '~run' + ]({ value: { other: 123 } }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo', other: 123 }, + }); + }); + test('for optional entry', () => { expectNoSchemaIssue( objectWithRest({ key: optional(string()) }, number()), @@ -158,20 +188,94 @@ describe('objectWithRest', () => { ); }); + test('for optional entry with default', () => { + expect( + objectWithRest({ key: optional(string(), 'foo') }, number())['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + objectWithRest({ key: optional(string(), () => 'foo') }, number())[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + objectWithRest({ key: optional(string(), () => undefined) }, number())[ + '~run' + ]({ value: { other: 123 } }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined, other: 123 }, + }); + }); + test('for nullish entry', () => { expectNoSchemaIssue( objectWithRest({ key: nullish(number()) }, number()), // @ts-expect-error - [{}, { key: undefined, other: 123 }, { key: null }, { key: 123 }] + [{}, { key: undefined }, { key: null, other: 123 }, { key: 123 }] ); }); + + test('for nullish entry with default', () => { + expect( + objectWithRest({ key: nullish(string(), 'foo') }, number())['~run']( + { value: { other: 123 } }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo', other: 123 }, + }); + expect( + objectWithRest({ key: nullish(string(), null) }, number())['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + objectWithRest({ key: nullish(string(), () => 'foo') }, number())[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + objectWithRest({ key: nullish(string(), () => null) }, number())[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + objectWithRest({ key: nullish(string(), () => undefined) }, number())[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); }); describe('should return dataset with nested issues', () => { const schema = objectWithRest( { - key: string(), - nested: object({ key: number() }), + key1: string(), + key2: number(), + nested: objectWithRest({ key1: string(), key2: number() }, number()), }, array(boolean()) ); @@ -185,42 +289,41 @@ describe('objectWithRest', () => { abortPipeEarly: undefined, }; - const stringIssue: StringIssue = { - ...baseInfo, - kind: 'schema', - type: 'string', - input: undefined, - expected: 'string', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'value', - input: {}, - key: 'key', - value: undefined, - }, - ], - }; - test('for missing entries', () => { - expect(schema['~run']({ value: {} }, {})).toStrictEqual({ + const input = { key2: 123, other: [true, false] }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: {}, + value: input, issues: [ - stringIssue, { ...baseInfo, kind: 'schema', - type: 'object', + type: 'object_with_rest', input: undefined, - expected: 'Object', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', - input: {}, + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"nested"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, key: 'nested', value: undefined, }, @@ -231,7 +334,7 @@ describe('objectWithRest', () => { }); test('for missing nested entries', () => { - const input = { key: 'value', nested: {} }; + const input = { key1: 'value', nested: { other: 123 } }; expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, value: input, @@ -239,23 +342,138 @@ describe('objectWithRest', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object_with_rest', input: undefined, - expected: 'number', + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key1"', received: 'undefined', path: [ { type: 'object', origin: 'value', - input: { key: 'value', nested: {} }, + input, key: 'nested', - value: {}, + value: input.nested, }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ { type: 'object', origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing entries with abort early', () => { + const input = { key2: 123 }; + expect( + schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing any and unknown entry', () => { + const schema = objectWithRest({ key1: any(), key2: unknown() }, number()); + expect(schema['~run']({ value: {} }, {})).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', input: {}, - key: 'key', + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', value: undefined, }, ], @@ -264,14 +482,189 @@ describe('objectWithRest', () => { } satisfies FailureDataset>); }); - test('with abort early', () => { - expect(schema['~run']({ value: {} }, { abortEarly: true })).toStrictEqual( - { - typed: false, - value: {}, - issues: [{ ...stringIssue, abortEarly: true }], - } satisfies FailureDataset> - ); + test('for invalid entries', () => { + const input = { key1: false, key2: 123, nested: null, other: [false] }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: null, + expected: 'Object', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: null, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid nested entries', () => { + const input = { + key1: 'value', + key2: 'value', + nested: { + key1: 123, + key2: null, + other: 123, + }, + }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: 'value', + expected: 'number', + received: '"value"', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: input.key2, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: 123, + expected: 'string', + received: '123', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key1', + value: input.nested.key1, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: null, + expected: 'number', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key2', + value: input.nested.key2, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries with abort early', () => { + const input = { key1: false, key2: 123, nested: null }; + expect( + schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid exact optional entry', () => { + const schema = objectWithRest({ key: exactOptional(string()) }, number()); + const input = { key: undefined }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); }); const arrayIssue: ArrayIssue = { @@ -286,8 +679,9 @@ describe('objectWithRest', () => { type: 'object', origin: 'value', input: { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other1: null, other2: 'bar', }, @@ -299,8 +693,9 @@ describe('objectWithRest', () => { test('for wrong rest', () => { const input = { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other1: null, other2: 'bar', }; @@ -330,13 +725,14 @@ describe('objectWithRest', () => { } satisfies FailureDataset>); }); - test('for worng rest with abort early', () => { + test('for wrong rest with abort early', () => { expect( schema['~run']( { value: { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other1: null, other2: 'bar', }, @@ -346,8 +742,9 @@ describe('objectWithRest', () => { ).toStrictEqual({ typed: false, value: { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, }, issues: [{ ...arrayIssue, abortEarly: true }], } satisfies FailureDataset>); @@ -355,8 +752,9 @@ describe('objectWithRest', () => { test('for wrong nested rest', () => { const input = { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other: ['true'], }; expect(schema['~run']({ value: input }, {})).toStrictEqual({ diff --git a/library/src/schemas/objectWithRest/objectWithRest.ts b/library/src/schemas/objectWithRest/objectWithRest.ts index d32087a39..489a0a277 100644 --- a/library/src/schemas/objectWithRest/objectWithRest.ts +++ b/library/src/schemas/objectWithRest/objectWithRest.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseIssue, BaseSchema, @@ -125,58 +126,95 @@ export function objectWithRest( dataset.typed = true; dataset.value = {}; - // Parse schema of each entry - // Hint: We do not distinguish between missing and `undefined` entries. - // The reason for this decision is that it reduces the bundle size, and - // we also expect that most users will expect this behavior. + // Process each object entry of schema for (const key in this.entries) { - // Get and parse value of key - const value = input[key as keyof typeof input]; - const valueDataset = this.entries[key]['~run']({ value }, config); - - // If there are issues, capture them - if (valueDataset.issues) { - // Create object path item - const pathItem: ObjectPathItem = { - type: 'object', - origin: 'value', - input: input as Record, - key, - value, - }; - - // Add modified entry dataset issues to issues - for (const issue of valueDataset.issues) { - if (issue.path) { - issue.path.unshift(pathItem); - } else { + const valueSchema = this.entries[key]; + + // If key is present or its an optional schema with a default value, + // parse input of key or default value + if ( + key in input || + ((valueSchema.type === 'exact_optional' || + valueSchema.type === 'optional' || + valueSchema.type === 'nullish') && + // @ts-expect-error + valueSchema.default !== undefined) + ) { + const value: unknown = + key in input + ? // @ts-expect-error + input[key] + : getDefault(valueSchema); + const valueDataset = valueSchema['~run']({ value }, config); + + // If there are issues, capture them + if (valueDataset.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + type: 'object', + origin: 'value', + input: input as Record, + key, + value, + }; + + // Add modified entry dataset issues to issues + for (const issue of valueDataset.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + // @ts-expect-error + issue.path = [pathItem]; + } // @ts-expect-error - issue.path = [pathItem]; + dataset.issues?.push(issue); + } + if (!dataset.issues) { + // @ts-expect-error + dataset.issues = valueDataset.issues; + } + + // If necessary, abort early + if (config.abortEarly) { + dataset.typed = false; + break; } - // @ts-expect-error - dataset.issues?.push(issue); - } - if (!dataset.issues) { - // @ts-expect-error - dataset.issues = valueDataset.issues; } - // If necessary, abort early - if (config.abortEarly) { + // If not typed, set typed to `false` + if (!valueDataset.typed) { dataset.typed = false; - break; } - } - - // If not typed, set typed to `false` - if (!valueDataset.typed) { - dataset.typed = false; - } - // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { + // Add entry to dataset // @ts-expect-error dataset.value[key] = valueDataset.value; + + // Otherwise, if key is missing and required, add issue + } else if ( + valueSchema.type !== 'exact_optional' && + valueSchema.type !== 'optional' && + valueSchema.type !== 'nullish' + ) { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + // @ts-expect-error + value: input[key], + }, + ], + }); + + // If necessary, abort early + if (config.abortEarly) { + break; + } } } @@ -185,8 +223,11 @@ export function objectWithRest( if (!dataset.issues || !config.abortEarly) { for (const key in input) { if (_isValidObjectKey(input, key) && !(key in this.entries)) { - const value: unknown = input[key as keyof typeof input]; - const valueDataset = this.rest['~run']({ value }, config); + const valueDataset = this.rest['~run']( + // @ts-expect-error + { value: input[key] }, + config + ); // If there are issues, capture them if (valueDataset.issues) { @@ -196,7 +237,8 @@ export function objectWithRest( origin: 'value', input: input as Record, key, - value, + // @ts-expect-error + value: input[key], }; // Add modified entry dataset issues to issues diff --git a/library/src/schemas/objectWithRest/objectWithRestAsync.test-d.ts b/library/src/schemas/objectWithRest/objectWithRestAsync.test-d.ts index 5e122d95c..7934e4b11 100644 --- a/library/src/schemas/objectWithRest/objectWithRestAsync.test-d.ts +++ b/library/src/schemas/objectWithRest/objectWithRestAsync.test-d.ts @@ -1,22 +1,31 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { ReadonlyAction, TransformAction } from '../../actions/index.ts'; -import type { SchemaWithPipe } from '../../methods/index.ts'; +import type { + SchemaWithPipe, + SchemaWithPipeAsync, +} from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import type { AnySchema } from '../any/index.ts'; import type { BooleanIssue, BooleanSchema } from '../boolean/index.ts'; -import type { NullishSchema } from '../nullish/index.ts'; +import type { + ExactOptionalSchema, + ExactOptionalSchemaAsync, +} from '../exactOptional/index.ts'; +import type { NullishSchema, NullishSchemaAsync } from '../nullish/index.ts'; import { number, type NumberIssue, type NumberSchema, } from '../number/index.ts'; -import type { ObjectIssue, ObjectSchema } from '../object/index.ts'; -import type { OptionalSchema } from '../optional/index.ts'; +import type { ObjectIssue, ObjectSchemaAsync } from '../object/index.ts'; +import type { OptionalSchema, OptionalSchemaAsync } from '../optional/index.ts'; import { string, type StringIssue, type StringSchema, } from '../string/index.ts'; import type { UndefinedableSchema } from '../undefinedable/index.ts'; +import type { UnknownSchema } from '../unknown/index.ts'; import { objectWithRestAsync, type ObjectWithRestSchemaAsync, @@ -54,18 +63,91 @@ describe('objectWithRestAsync', () => { describe('should infer correct types', () => { type Schema = ObjectWithRestSchemaAsync< { - key1: StringSchema; - key2: OptionalSchema, 'foo'>; - key3: NullishSchema, undefined>; - key4: ObjectSchema<{ key: NumberSchema }, undefined>; - key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; - key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipe< + key00: StringSchema; + key01: AnySchema; + key02: UnknownSchema; + key03: ObjectSchemaAsync<{ key: NumberSchema }, undefined>; + key04: SchemaWithPipe< + [StringSchema, ReadonlyAction] + >; + key05: UndefinedableSchema, 'bar'>; + key06: SchemaWithPipe< + [ + OptionalSchema, undefined>, + TransformAction, + ] + >; + key07: SchemaWithPipeAsync< [ OptionalSchema, undefined>, - TransformAction, + TransformAction, + ] + >; + key08: SchemaWithPipeAsync< + [ + OptionalSchemaAsync, undefined>, + TransformAction, ] >; + + // ExactOptionalSchema + key10: ExactOptionalSchema, undefined>; + key11: ExactOptionalSchema, 'foo'>; + key12: ExactOptionalSchema, () => 'foo'>; + + // ExactOptionalSchemaAsync + key20: ExactOptionalSchemaAsync, undefined>; + key21: ExactOptionalSchemaAsync, 'foo'>; + key22: ExactOptionalSchemaAsync, () => 'foo'>; + key23: ExactOptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + + // OptionalSchema + key30: OptionalSchema, undefined>; + key31: OptionalSchema, 'foo'>; + key32: OptionalSchema, () => undefined>; + key33: OptionalSchema, () => 'foo'>; + + // OptionalSchemaAsync + key40: OptionalSchemaAsync, undefined>; + key41: OptionalSchemaAsync, 'foo'>; + key42: OptionalSchemaAsync, () => undefined>; + key43: OptionalSchemaAsync, () => 'foo'>; + key44: OptionalSchemaAsync< + StringSchema, + () => Promise + >; + key45: OptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + + // NullishSchema + key50: NullishSchema, undefined>; + key51: NullishSchema, null>; + key52: NullishSchema, 'foo'>; + key53: NullishSchema, () => undefined>; + key54: NullishSchema, () => null>; + key55: NullishSchema, () => 'foo'>; + + // NullishSchemaAsync + key60: NullishSchemaAsync, undefined>; + key61: NullishSchemaAsync, null>; + key62: NullishSchemaAsync, 'foo'>; + key63: NullishSchemaAsync, () => undefined>; + key64: NullishSchemaAsync, () => null>; + key65: NullishSchemaAsync, () => 'foo'>; + key66: NullishSchemaAsync< + StringSchema, + () => Promise + >; + key67: NullishSchemaAsync, () => Promise>; + key68: NullishSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; }, BooleanSchema, undefined @@ -74,13 +156,60 @@ describe('objectWithRestAsync', () => { test('of input', () => { expectTypeOf>().toEqualTypeOf< { - key1: string; - key2?: string; - key3?: string | null | undefined; - key4: { key: number }; - key5: string; - key6: string | undefined; - key7?: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + key04: string; + key05: string | undefined; + key06?: string | undefined; + key07?: string | undefined; + key08?: string | undefined; + + // ExactOptionalSchema + key10?: string; + key11?: string; + key12?: string; + + // ExactOptionalSchemaAsync + key20?: string; + key21?: string; + key22?: string; + key23?: string; + + // OptionalSchema + key30?: string | undefined; + key31?: string | undefined; + key32?: string | undefined; + key33?: string | undefined; + + // OptionalSchemaAsync + key40?: string | undefined; + key41?: string | undefined; + key42?: string | undefined; + key43?: string | undefined; + key44?: string | undefined; + key45?: string | undefined; + + // NullishSchema + key50?: string | null | undefined; + key51?: string | null | undefined; + key52?: string | null | undefined; + key53?: string | null | undefined; + key54?: string | null | undefined; + key55?: string | null | undefined; + + // NullishSchemaAsync + key60?: string | null | undefined; + key61?: string | null | undefined; + key62?: string | null | undefined; + key63?: string | null | undefined; + key64?: string | null | undefined; + key65?: string | null | undefined; + key66?: string | null | undefined; + key67?: string | null | undefined; + key68?: string | null | undefined; } & { [key: string]: boolean } >(); }); @@ -88,13 +217,60 @@ describe('objectWithRestAsync', () => { test('of output', () => { expectTypeOf>().toEqualTypeOf< { - key1: string; - key2: string; - key3?: string | null | undefined; - key4: { key: number }; - readonly key5: string; - key6: string; - key7: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + readonly key04: string; + key05: string; + key06?: number; + key07?: number; + key08?: number; + + // ExactOptionalSchema + key10?: string; + key11: string; + key12: string; + + // ExactOptionalSchemaAsync + key20?: string; + key21: string; + key22: string; + key23: string; + + // OptionalSchema + key30?: string | undefined; + key31: string; + key32: string | undefined; + key33: string; + + // OptionalSchemaAsync + key40?: string | undefined; + key41: string; + key42: string | undefined; + key43: string; + key44: string | undefined; + key45: string; + + // NullishSchema + key50?: string | null | undefined; + key51: string | null; + key52: string; + key53: string | undefined; + key54: string | null; + key55: string; + + // NullishSchemaAsync + key60?: string | null | undefined; + key61: string | null; + key62: string; + key63: string | undefined; + key64: string | null; + key65: string; + key66: string | undefined; + key67: string | null; + key68: string; } & { [key: string]: boolean } >(); }); diff --git a/library/src/schemas/objectWithRest/objectWithRestAsync.test.ts b/library/src/schemas/objectWithRest/objectWithRestAsync.test.ts index dc128a2ba..0ac0440ba 100644 --- a/library/src/schemas/objectWithRest/objectWithRestAsync.test.ts +++ b/library/src/schemas/objectWithRest/objectWithRestAsync.test.ts @@ -4,15 +4,18 @@ import { expectNoSchemaIssueAsync, expectSchemaIssueAsync, } from '../../vitest/index.ts'; +import { any } from '../any/index.ts'; import { array } from '../array/array.ts'; import type { ArrayIssue } from '../array/types.ts'; import { boolean } from '../boolean/index.ts'; +import { exactOptional, exactOptionalAsync } from '../exactOptional/index.ts'; import { never } from '../never/index.ts'; -import { nullish } from '../nullish/index.ts'; +import { nullish, nullishAsync } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { object, objectAsync } from '../object/index.ts'; -import { optional } from '../optional/index.ts'; -import { string, type StringIssue } from '../string/index.ts'; +import { optional, optionalAsync } from '../optional/index.ts'; +import { string } from '../string/index.ts'; +import { unknown } from '../unknown/index.ts'; import { objectWithRestAsync, type ObjectWithRestSchemaAsync, @@ -165,28 +168,295 @@ describe('objectWithRestAsync', () => { ); }); + test('for exact optional entry', async () => { + await expectNoSchemaIssueAsync( + objectWithRestAsync({ key: exactOptional(string()) }, number()), + // @ts-expect-error + [{}, { key: 'foo', other: 123 }] + ); + await expectNoSchemaIssueAsync( + objectWithRestAsync({ key: exactOptionalAsync(string()) }, number()), + // @ts-expect-error + [{}, { key: 'foo' }] + ); + }); + + test('for exact optional entry with default', async () => { + // Sync + expect( + await objectWithRestAsync( + { key: exactOptional(string(), 'foo') }, + number() + )['~run']({ value: { other: 123 } }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo', other: 123 }, + }); + expect( + await objectWithRestAsync( + { key: exactOptional(string(), () => 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + + // Async + expect( + await objectWithRestAsync( + { key: exactOptionalAsync(string(), 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: exactOptionalAsync(string(), () => 'foo') }, + number() + )['~run']({ value: { other: 123 } }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo', other: 123 }, + }); + }); + test('for optional entry', async () => { await expectNoSchemaIssueAsync( objectWithRestAsync({ key: optional(string()) }, number()), // @ts-expect-error [{}, { key: undefined, other: 123 }, { key: 'foo' }] ); + await expectNoSchemaIssueAsync( + objectWithRestAsync({ key: optionalAsync(string()) }, number()), + // @ts-expect-error + [{}, { key: undefined, other: 123 }, { key: 'foo' }] + ); + }); + + test('for optional entry with default', async () => { + // Sync + expect( + await objectWithRestAsync({ key: optional(string(), 'foo') }, number())[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: optional(string(), () => 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: optional(string(), () => undefined) }, + number() + )['~run']({ value: { other: 123 } }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined, other: 123 }, + }); + + // Async + expect( + await objectWithRestAsync( + { key: optionalAsync(string(), 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: optionalAsync(string(), () => 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: optionalAsync(string(), () => undefined) }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + expect( + await objectWithRestAsync( + { key: optionalAsync(string(), async () => 'foo') }, + number() + )['~run']({ value: { other: 123 } }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo', other: 123 }, + }); + expect( + await objectWithRestAsync( + { key: optionalAsync(string(), async () => undefined) }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); }); test('for nullish entry', async () => { await expectNoSchemaIssueAsync( objectWithRestAsync({ key: nullish(number()) }, number()), // @ts-expect-error - [{}, { key: undefined, other: 123 }, { key: null }, { key: 123 }] + [{}, { key: undefined }, { key: null, other: 123 }, { key: 123 }] ); + await expectNoSchemaIssueAsync( + objectWithRestAsync({ key: nullishAsync(number()) }, number()), + // @ts-expect-error + [{ other: 123 }, { key: undefined }, { key: null }, { key: 123 }] + ); + }); + + test('for nullish entry with default', async () => { + // Sync + expect( + await objectWithRestAsync({ key: nullish(string(), 'foo') }, number())[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync({ key: nullish(string(), null) }, number())[ + '~run' + ]({ value: { other: 123 } }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null, other: 123 }, + }); + expect( + await objectWithRestAsync( + { key: nullish(string(), () => 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: nullish(string(), () => null) }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await objectWithRestAsync( + { key: nullish(string(), () => undefined) }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + + // Async + expect( + await objectWithRestAsync( + { key: nullishAsync(string(), 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: nullishAsync(string(), null) }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await objectWithRestAsync( + { key: nullishAsync(string(), () => 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: nullishAsync(string(), () => null) }, + number() + )['~run']({ value: { other: 123 } }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null, other: 123 }, + }); + expect( + await objectWithRestAsync( + { key: nullishAsync(string(), () => undefined) }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + expect( + await objectWithRestAsync( + { key: nullishAsync(string(), async () => 'foo') }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await objectWithRestAsync( + { key: nullishAsync(string(), async () => null) }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await objectWithRestAsync( + { key: nullishAsync(string(), async () => undefined) }, + number() + )['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); }); }); describe('should return dataset with nested issues', () => { const schema = objectWithRestAsync( { - key: string(), - nested: objectAsync({ key: number() }), + key1: string(), + key2: number(), + nested: objectWithRestAsync( + { key1: string(), key2: number() }, + number() + ), }, array(boolean()) ); @@ -200,42 +470,41 @@ describe('objectWithRestAsync', () => { abortPipeEarly: undefined, }; - const stringIssue: StringIssue = { - ...baseInfo, - kind: 'schema', - type: 'string', - input: undefined, - expected: 'string', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'value', - input: {}, - key: 'key', - value: undefined, - }, - ], - }; - test('for missing entries', async () => { - expect(await schema['~run']({ value: {} }, {})).toStrictEqual({ + const input = { key2: 123, other: [true, false] }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: {}, + value: input, issues: [ - stringIssue, { ...baseInfo, kind: 'schema', - type: 'object', + type: 'object_with_rest', input: undefined, - expected: 'Object', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', - input: {}, + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"nested"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, key: 'nested', value: undefined, }, @@ -246,7 +515,7 @@ describe('objectWithRestAsync', () => { }); test('for missing nested entries', async () => { - const input = { key: 'value', nested: {} }; + const input = { key1: 'value', nested: { other: 123 } }; expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, value: input, @@ -254,23 +523,141 @@ describe('objectWithRestAsync', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'object_with_rest', input: undefined, - expected: 'number', + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key1"', received: 'undefined', path: [ { type: 'object', origin: 'value', - input: { key: 'value', nested: {} }, + input, key: 'nested', - value: {}, + value: input.nested, }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ { type: 'object', origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing entries with abort early', async () => { + const input = { key2: 123 }; + expect( + await schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing any and unknown entry', async () => { + const schema = objectWithRestAsync( + { key1: any(), key2: unknown() }, + number() + ); + expect(await schema['~run']({ value: {} }, {})).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', input: {}, - key: 'key', + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', value: undefined, }, ], @@ -279,13 +666,191 @@ describe('objectWithRestAsync', () => { } satisfies FailureDataset>); }); - test('with abort early', async () => { + test('for invalid entries', async () => { + const input = { key1: false, key2: 123, nested: null, other: [false] }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'object_with_rest', + input: null, + expected: 'Object', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: null, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid nested entries', async () => { + const input = { + key1: 'value', + key2: 'value', + nested: { + key1: 123, + key2: null, + other: 123, + }, + }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: 'value', + expected: 'number', + received: '"value"', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: input.key2, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: 123, + expected: 'string', + received: '123', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key1', + value: input.nested.key1, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: null, + expected: 'number', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key2', + value: input.nested.key2, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries with abort early', async () => { + const input = { key1: false, key2: 123, nested: null }; expect( - await schema['~run']({ value: {} }, { abortEarly: true }) + await schema['~run']({ value: input }, { abortEarly: true }) ).toStrictEqual({ typed: false, value: {}, - issues: [{ ...stringIssue, abortEarly: true }], + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid exact optional entry', async () => { + const schema = objectWithRestAsync( + { key: exactOptional(string()) }, + number() + ); + const input = { key: undefined }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key', + value: undefined, + }, + ], + }, + ], } satisfies FailureDataset>); }); @@ -301,8 +866,9 @@ describe('objectWithRestAsync', () => { type: 'object', origin: 'value', input: { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other1: null, other2: 'bar', }, @@ -314,8 +880,9 @@ describe('objectWithRestAsync', () => { test('for wrong rest', async () => { const input = { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other1: null, other2: 'bar', }; @@ -345,13 +912,14 @@ describe('objectWithRestAsync', () => { } satisfies FailureDataset>); }); - test('for worng rest with abort early', async () => { + test('for wrong rest with abort early', async () => { expect( await schema['~run']( { value: { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other1: null, other2: 'bar', }, @@ -361,8 +929,9 @@ describe('objectWithRestAsync', () => { ).toStrictEqual({ typed: false, value: { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, }, issues: [{ ...arrayIssue, abortEarly: true }], } satisfies FailureDataset>); @@ -370,8 +939,9 @@ describe('objectWithRestAsync', () => { test('for wrong nested rest', async () => { const input = { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other: ['true'], }; expect(await schema['~run']({ value: input }, {})).toStrictEqual({ diff --git a/library/src/schemas/objectWithRest/objectWithRestAsync.ts b/library/src/schemas/objectWithRest/objectWithRestAsync.ts index 4b7b3b0a8..4c6ba0678 100644 --- a/library/src/schemas/objectWithRest/objectWithRestAsync.ts +++ b/library/src/schemas/objectWithRest/objectWithRestAsync.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseIssue, BaseSchema, @@ -18,6 +19,7 @@ import { _getStandardProps, _isValidObjectKey, } from '../../utils/index.ts'; +import type { objectWithRest } from './objectWithRest.ts'; import type { ObjectWithRestIssue } from './types.ts'; /** @@ -41,7 +43,7 @@ export interface ObjectWithRestSchemaAsync< /** * The schema reference. */ - readonly reference: typeof objectWithRestAsync; + readonly reference: typeof objectWithRest | typeof objectWithRestAsync; /** * The expected property. */ @@ -137,22 +139,41 @@ export function objectWithRestAsync( // Parse each normal and rest entry const [normalDatasets, restDatasets] = await Promise.all([ - // Parse schema of each normal entry - // Hint: We do not distinguish between missing and `undefined` entries. - // The reason for this decision is that it reduces the bundle size, and - // we also expect that most users will expect this behavior. + // If key is present or its an optional schema with a default value, + // parse input of key or default value asynchronously Promise.all( - Object.entries(this.entries).map(async ([key, schema]) => { - const value = input[key as keyof typeof input]; + Object.entries(this.entries).map(async ([key, valueSchema]) => { + if ( + key in input || + ((valueSchema.type === 'exact_optional' || + valueSchema.type === 'optional' || + valueSchema.type === 'nullish') && + // @ts-expect-error + valueSchema.default !== undefined) + ) { + const value: unknown = + key in input + ? // @ts-expect-error + input[key] + : await getDefault(valueSchema); + return [ + key, + value, + valueSchema, + await valueSchema['~run']({ value }, config), + ] as const; + } return [ key, - value, - await schema['~run']({ value }, config), + // @ts-expect-error + input[key] as unknown, + valueSchema, + null, ] as const; }) ), - // Parse other entries with rest schema + // Parse other entries with rest schema asynchronously // Hint: We exclude specific keys for security reasons Promise.all( Object.entries(input) @@ -171,57 +192,83 @@ export function objectWithRestAsync( ), ]); - // Process each normal dataset - for (const [key, value, valueDataset] of normalDatasets) { - // If there are issues, capture them - if (valueDataset.issues) { - // Create object path item - const pathItem: ObjectPathItem = { - type: 'object', - origin: 'value', - input: input as Record, - key, - value, - }; + // Process each normal object entry of schema + for (const [key, value, valueSchema, valueDataset] of normalDatasets) { + // If key is present or its an optional schema with a default value, + // process its value dataset + if (valueDataset) { + // If there are issues, capture them + if (valueDataset.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + type: 'object', + origin: 'value', + input: input as Record, + key, + value, + }; - // Add modified entry dataset issues to issues - for (const issue of valueDataset.issues) { - if (issue.path) { - issue.path.unshift(pathItem); - } else { + // Add modified entry dataset issues to issues + for (const issue of valueDataset.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + // @ts-expect-error + issue.path = [pathItem]; + } // @ts-expect-error - issue.path = [pathItem]; + dataset.issues?.push(issue); + } + if (!dataset.issues) { + // @ts-expect-error + dataset.issues = valueDataset.issues; + } + + // If necessary, abort early + if (config.abortEarly) { + dataset.typed = false; + break; } - // @ts-expect-error - dataset.issues?.push(issue); - } - if (!dataset.issues) { - // @ts-expect-error - dataset.issues = valueDataset.issues; } - // If necessary, abort early - if (config.abortEarly) { + // If not typed, set typed to `false` + if (!valueDataset.typed) { dataset.typed = false; - break; } - } - // If not typed, set typed to `false` - if (!valueDataset.typed) { - dataset.typed = false; - } - - // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { + // Add entry to dataset // @ts-expect-error dataset.value[key] = valueDataset.value; + + // Otherwise, if key is missing and required, add issue + } else if ( + valueSchema.type !== 'exact_optional' && + valueSchema.type !== 'optional' && + valueSchema.type !== 'nullish' + ) { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + value, + }, + ], + }); + + // If necessary, abort early + if (config.abortEarly) { + break; + } } } - // Parse schema of each rest entry if necessary + // Process each rest entry of schema if necessary if (!dataset.issues || !config.abortEarly) { - // Process each normal dataset for (const [key, value, valueDataset] of restDatasets) { // If there are issues, capture them if (valueDataset.issues) { diff --git a/library/src/schemas/objectWithRest/types.ts b/library/src/schemas/objectWithRest/types.ts index 1bbf7f53a..638f96fef 100644 --- a/library/src/schemas/objectWithRest/types.ts +++ b/library/src/schemas/objectWithRest/types.ts @@ -15,5 +15,5 @@ export interface ObjectWithRestIssue extends BaseIssue { /** * The expected property. */ - readonly expected: 'Object'; + readonly expected: 'Object' | `"${string}"`; } diff --git a/library/src/schemas/optional/index.ts b/library/src/schemas/optional/index.ts index 48656b5a4..e03bba2d5 100644 --- a/library/src/schemas/optional/index.ts +++ b/library/src/schemas/optional/index.ts @@ -1,2 +1,3 @@ export * from './optional.ts'; export * from './optionalAsync.ts'; +export * from './types.ts'; diff --git a/library/src/schemas/optional/optional.test.ts b/library/src/schemas/optional/optional.test.ts index 442e2d729..72fc023b1 100644 --- a/library/src/schemas/optional/optional.test.ts +++ b/library/src/schemas/optional/optional.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from 'vitest'; -import { expectNoSchemaIssue } from '../../vitest/index.ts'; -import { string, type StringSchema } from '../string/index.ts'; +import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; import { optional, type OptionalSchema } from './optional.ts'; describe('optional', () => { @@ -76,6 +80,24 @@ describe('optional', () => { }); }); + describe('should return dataset with issues', () => { + const schema = optional(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', () => { + expectSchemaIssue(schema, baseIssue, [123, true, {}]); + }); + + test('for null', () => { + expectSchemaIssue(schema, baseIssue, [null]); + }); + }); + describe('should return dataset without default', () => { test('for undefined default', () => { expectNoSchemaIssue(optional(string()), [undefined, 'foo']); diff --git a/library/src/schemas/optional/optional.ts b/library/src/schemas/optional/optional.ts index 95c518d6f..f42a597dd 100644 --- a/library/src/schemas/optional/optional.ts +++ b/library/src/schemas/optional/optional.ts @@ -1,22 +1,24 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseIssue, BaseSchema, Default, InferInput, InferIssue, - InferOutput, + SuccessDataset, } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; +import type { InferOptionalOutput } from './types.ts'; /** * Optional schema interface. */ export interface OptionalSchema< TWrapped extends BaseSchema>, - TDefault extends Default, + TDefault extends Default, > extends BaseSchema< - InferInput, - InferOutput, + InferInput | undefined, + InferOptionalOutput, InferIssue > { /** @@ -30,7 +32,7 @@ export interface OptionalSchema< /** * The expected property. */ - readonly expects: TWrapped['expects']; + readonly expects: `(${TWrapped['expects']} | undefined)`; /** * The wrapped schema. */ @@ -42,27 +44,27 @@ export interface OptionalSchema< } /** - * Creates a optional schema. + * Creates an optional schema. * * @param wrapped The wrapped schema. * - * @returns A optional schema. + * @returns An optional schema. */ export function optional< const TWrapped extends BaseSchema>, >(wrapped: TWrapped): OptionalSchema; /** - * Creates a optional schema. + * Creates an optional schema. * * @param wrapped The wrapped schema. * @param default_ The default value. * - * @returns A optional schema. + * @returns An optional schema. */ export function optional< const TWrapped extends BaseSchema>, - const TDefault extends Default, + const TDefault extends Default, >(wrapped: TWrapped, default_: TDefault): OptionalSchema; // @__NO_SIDE_EFFECTS__ @@ -74,7 +76,7 @@ export function optional( kind: 'schema', type: 'optional', reference: optional, - expects: wrapped.expects, + expects: `(${wrapped.expects} | undefined)`, async: false, wrapped, default: default_, @@ -82,6 +84,23 @@ export function optional( return _getStandardProps(this); }, '~run'(dataset, config) { + // If value is `undefined`, override it with default or return dataset + if (dataset.value === undefined) { + // If default is specified, override value of dataset + if (this.default !== undefined) { + dataset.value = getDefault(this, dataset, config); + } + + // If value is still `undefined`, return dataset + if (dataset.value === undefined) { + // @ts-expect-error + dataset.typed = true; + // @ts-expect-error + return dataset as SuccessDataset; + } + } + + // Otherwise, return dataset of wrapped schema return this.wrapped['~run'](dataset, config); }, }; diff --git a/library/src/schemas/optional/optionalAsync.test.ts b/library/src/schemas/optional/optionalAsync.test.ts index aab9014ee..a75b126b6 100644 --- a/library/src/schemas/optional/optionalAsync.test.ts +++ b/library/src/schemas/optional/optionalAsync.test.ts @@ -1,6 +1,13 @@ import { describe, expect, test } from 'vitest'; -import { expectNoSchemaIssueAsync } from '../../vitest/index.ts'; -import { string, type StringSchema } from '../string/index.ts'; +import { + expectNoSchemaIssueAsync, + expectSchemaIssueAsync, +} from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; import { optionalAsync, type OptionalSchemaAsync } from './optionalAsync.ts'; describe('optionalAsync', () => { @@ -95,6 +102,24 @@ describe('optionalAsync', () => { }); }); + describe('should return dataset with issues', () => { + const schema = optionalAsync(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [123, true, {}]); + }); + + test('for null', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [null]); + }); + }); + describe('should return dataset without default', () => { test('for undefined default', async () => { await expectNoSchemaIssueAsync(optionalAsync(string()), [ diff --git a/library/src/schemas/optional/optionalAsync.ts b/library/src/schemas/optional/optionalAsync.ts index 78c955822..3d0e3ddc0 100644 --- a/library/src/schemas/optional/optionalAsync.ts +++ b/library/src/schemas/optional/optionalAsync.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseIssue, BaseSchema, @@ -5,9 +6,11 @@ import type { DefaultAsync, InferInput, InferIssue, - InferOutput, + SuccessDataset, } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; +import type { optional } from './optional.ts'; +import type { InferOptionalOutput } from './types.ts'; /** * Optional schema async interface. @@ -16,10 +19,10 @@ export interface OptionalSchemaAsync< TWrapped extends | BaseSchema> | BaseSchemaAsync>, - TDefault extends DefaultAsync, + TDefault extends DefaultAsync, > extends BaseSchemaAsync< - InferInput, - InferOutput, + InferInput | undefined, + InferOptionalOutput, InferIssue > { /** @@ -29,11 +32,11 @@ export interface OptionalSchemaAsync< /** * The schema reference. */ - readonly reference: typeof optionalAsync; + readonly reference: typeof optional | typeof optionalAsync; /** * The expected property. */ - readonly expects: TWrapped['expects']; + readonly expects: `(${TWrapped['expects']} | undefined)`; /** * The wrapped schema. */ @@ -69,7 +72,7 @@ export function optionalAsync< const TWrapped extends | BaseSchema> | BaseSchemaAsync>, - const TDefault extends DefaultAsync, + const TDefault extends DefaultAsync, >( wrapped: TWrapped, default_: TDefault @@ -90,7 +93,7 @@ export function optionalAsync( kind: 'schema', type: 'optional', reference: optionalAsync, - expects: wrapped.expects, + expects: `(${wrapped.expects} | undefined)`, async: true, wrapped, default: default_, @@ -98,6 +101,23 @@ export function optionalAsync( return _getStandardProps(this); }, async '~run'(dataset, config) { + // If value is `undefined`, override it with default or return dataset + if (dataset.value === undefined) { + // If default is specified, override value of dataset + if (this.default !== undefined) { + dataset.value = await getDefault(this, dataset, config); + } + + // If value is still `undefined`, return dataset + if (dataset.value === undefined) { + // @ts-expect-error + dataset.typed = true; + // @ts-expect-error + return dataset as SuccessDataset; + } + } + + // Otherwise, return dataset of wrapped schema return this.wrapped['~run'](dataset, config); }, }; diff --git a/library/src/schemas/optional/types.ts b/library/src/schemas/optional/types.ts new file mode 100644 index 000000000..3782f9574 --- /dev/null +++ b/library/src/schemas/optional/types.ts @@ -0,0 +1,20 @@ +import type { + BaseIssue, + BaseSchema, + BaseSchemaAsync, + DefaultAsync, + DefaultValue, + InferOutput, +} from '../../types/index.ts'; + +/** + * Infer optional output type. + */ +export type InferOptionalOutput< + TWrapped extends + | BaseSchema> + | BaseSchemaAsync>, + TDefault extends DefaultAsync, +> = undefined extends TDefault + ? InferOutput | undefined + : InferOutput | Extract, undefined>; diff --git a/library/src/schemas/record/recordAsync.ts b/library/src/schemas/record/recordAsync.ts index 9cbebdd8f..c7f87a84c 100644 --- a/library/src/schemas/record/recordAsync.ts +++ b/library/src/schemas/record/recordAsync.ts @@ -12,6 +12,7 @@ import { _getStandardProps, _isValidObjectKey, } from '../../utils/index.ts'; +import type { record } from './record.ts'; import type { InferRecordInput, InferRecordOutput, @@ -41,7 +42,7 @@ export interface RecordSchemaAsync< /** * The schema reference. */ - readonly reference: typeof recordAsync; + readonly reference: typeof record | typeof recordAsync; /** * The expected property. */ diff --git a/library/src/schemas/set/setAsync.ts b/library/src/schemas/set/setAsync.ts index 493f73ddf..bbfefc387 100644 --- a/library/src/schemas/set/setAsync.ts +++ b/library/src/schemas/set/setAsync.ts @@ -8,6 +8,7 @@ import type { SetPathItem, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { set } from './set.ts'; import type { InferSetInput, InferSetOutput, SetIssue } from './types.ts'; /** @@ -30,7 +31,7 @@ export interface SetSchemaAsync< /** * The schema reference. */ - readonly reference: typeof setAsync; + readonly reference: typeof set | typeof setAsync; /** * The expected property. */ diff --git a/library/src/schemas/strictObject/strictObject.test-d.ts b/library/src/schemas/strictObject/strictObject.test-d.ts index bbb175004..de03052b3 100644 --- a/library/src/schemas/strictObject/strictObject.test-d.ts +++ b/library/src/schemas/strictObject/strictObject.test-d.ts @@ -2,6 +2,8 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { ReadonlyAction, TransformAction } from '../../actions/index.ts'; import type { SchemaWithPipe } from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import type { AnySchema } from '../any/index.ts'; +import type { ExactOptionalSchema } from '../exactOptional/index.ts'; import type { NullishSchema } from '../nullish/index.ts'; import { type NumberIssue, type NumberSchema } from '../number/index.ts'; import type { ObjectIssue, ObjectSchema } from '../object/index.ts'; @@ -12,6 +14,7 @@ import { type StringSchema, } from '../string/index.ts'; import type { UndefinedableSchema } from '../undefinedable/index.ts'; +import type { UnknownSchema } from '../unknown/index.ts'; import { strictObject, type StrictObjectSchema } from './strictObject.ts'; import type { StrictObjectIssue } from './types.ts'; @@ -42,43 +45,104 @@ describe('strictObject', () => { describe('should infer correct types', () => { type Schema = StrictObjectSchema< { - key1: StringSchema; - key2: OptionalSchema, 'foo'>; - key3: NullishSchema, undefined>; - key4: ObjectSchema<{ key: NumberSchema }, undefined>; - key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; - key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipe< + key00: StringSchema; + key01: AnySchema; + key02: UnknownSchema; + key03: ObjectSchema<{ key: NumberSchema }, undefined>; + key04: SchemaWithPipe< + [StringSchema, ReadonlyAction] + >; + key05: UndefinedableSchema, 'bar'>; + key06: SchemaWithPipe< [ OptionalSchema, undefined>, - TransformAction, + TransformAction, ] >; + + // ExactOptionalSchema + key10: ExactOptionalSchema, undefined>; + key11: ExactOptionalSchema, 'foo'>; + key12: ExactOptionalSchema, () => 'foo'>; + + // OptionalSchema + key20: OptionalSchema, undefined>; + key21: OptionalSchema, 'foo'>; + key22: OptionalSchema, () => undefined>; + key23: OptionalSchema, () => 'foo'>; + + // NullishSchema + key30: NullishSchema, undefined>; + key31: NullishSchema, null>; + key32: NullishSchema, 'foo'>; + key33: NullishSchema, () => undefined>; + key34: NullishSchema, () => null>; + key35: NullishSchema, () => 'foo'>; }, undefined >; test('of input', () => { expectTypeOf>().toEqualTypeOf<{ - key1: string; - key2?: string; - key3?: string | null | undefined; - key4: { key: number }; - key5: string; - key6: string | undefined; - key7?: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + key04: string; + key05: string | undefined; + key06?: string | undefined; + + // ExactOptionalSchema + key10?: string; + key11?: string; + key12?: string; + + // OptionalSchema + key20?: string | undefined; + key21?: string | undefined; + key22?: string | undefined; + key23?: string | undefined; + + // NullishSchema + key30?: string | null | undefined; + key31?: string | null | undefined; + key32?: string | null | undefined; + key33?: string | null | undefined; + key34?: string | null | undefined; + key35?: string | null | undefined; }>(); }); test('of output', () => { expectTypeOf>().toEqualTypeOf<{ - key1: string; - key2: string; - key3?: string | null | undefined; - key4: { key: number }; - readonly key5: string; - key6: string; - key7: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + readonly key04: string; + key05: string; + key06?: number; + + // ExactOptionalSchema + key10?: string; + key11: string; + key12: string; + + // OptionalSchema + key20?: string | undefined; + key21: string; + key22: string | undefined; + key23: string; + + // NullishSchema + key30?: string | null | undefined; + key31: string | null; + key32: string; + key33: string | undefined; + key34: string | null; + key35: string; }>(); }); diff --git a/library/src/schemas/strictObject/strictObject.test.ts b/library/src/schemas/strictObject/strictObject.test.ts index 47a6098d2..6adb64e47 100644 --- a/library/src/schemas/strictObject/strictObject.test.ts +++ b/library/src/schemas/strictObject/strictObject.test.ts @@ -1,11 +1,14 @@ import { describe, expect, test } from 'vitest'; import type { FailureDataset, InferIssue } from '../../types/index.ts'; import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { any } from '../any/index.ts'; +import { exactOptional } from '../exactOptional/index.ts'; import { nullish } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { object } from '../object/index.ts'; import { optional } from '../optional/index.ts'; -import { string, type StringIssue } from '../string/index.ts'; +import { string } from '../string/index.ts'; +import { unknown } from '../unknown/index.ts'; import { strictObject, type StrictObjectSchema } from './strictObject.ts'; import type { StrictObjectIssue } from './types.ts'; @@ -131,15 +134,71 @@ describe('strictObject', () => { ]); }); + test('for exact optional entry', () => { + expectNoSchemaIssue(strictObject({ key: exactOptional(string()) }), [ + {}, + { key: 'foo' }, + ]); + }); + + test('for exact optional entry with default', () => { + expect( + strictObject({ key: exactOptional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + strictObject({ key: exactOptional(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + }); + test('for optional entry', () => { expectNoSchemaIssue(strictObject({ key: optional(string()) }), [ {}, - // @ts-expect-error { key: undefined }, { key: 'foo' }, ]); }); + test('for optional entry with default', () => { + expect( + strictObject({ key: optional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + strictObject({ key: optional(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + strictObject({ + key: optional(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); + test('for nullish entry', () => { expectNoSchemaIssue(strictObject({ key: nullish(number()) }), [ {}, @@ -148,12 +207,61 @@ describe('strictObject', () => { { key: 123 }, ]); }); + + test('for nullish entry with default', () => { + expect( + strictObject({ key: nullish(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + strictObject({ key: nullish(string(), null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + strictObject({ key: nullish(string(), () => 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + strictObject({ key: nullish(string(), () => null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + strictObject({ key: nullish(string(), () => undefined) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); }); describe('should return dataset with nested issues', () => { const schema = strictObject({ - key: string(), - nested: object({ key: number() }), + key1: string(), + key2: number(), + nested: strictObject({ key1: string(), key2: number() }), }); const baseInfo = { @@ -165,42 +273,41 @@ describe('strictObject', () => { abortPipeEarly: undefined, }; - const stringIssue: StringIssue = { - ...baseInfo, - kind: 'schema', - type: 'string', - input: undefined, - expected: 'string', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'value', - input: {}, - key: 'key', - value: undefined, - }, - ], - }; - test('for missing entries', () => { - expect(schema['~run']({ value: {} }, {})).toStrictEqual({ + const input = { key2: 123 }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: {}, + value: input, issues: [ - stringIssue, { ...baseInfo, kind: 'schema', - type: 'object', + type: 'strict_object', input: undefined, - expected: 'Object', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', - input: {}, + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"nested"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, key: 'nested', value: undefined, }, @@ -211,7 +318,7 @@ describe('strictObject', () => { }); test('for missing nested entries', () => { - const input = { key: 'value', nested: {} }; + const input = { key1: 'value', nested: {} }; expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, value: input, @@ -219,23 +326,138 @@ describe('strictObject', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'strict_object', input: undefined, - expected: 'number', + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key1"', received: 'undefined', path: [ { type: 'object', origin: 'value', - input: { key: 'value', nested: {} }, + input, key: 'nested', value: {}, }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ { type: 'object', origin: 'value', + input, + key: 'nested', + value: {}, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing entries with abort early', () => { + const input = { key2: 123 }; + expect( + schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing any and unknown entry', () => { + const schema = strictObject({ key1: any(), key2: unknown() }); + expect(schema['~run']({ value: {} }, {})).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', input: {}, - key: 'key', + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key2', value: undefined, }, ], @@ -244,38 +466,213 @@ describe('strictObject', () => { } satisfies FailureDataset>); }); - test('with abort early', () => { - expect(schema['~run']({ value: {} }, { abortEarly: true })).toStrictEqual( - { - typed: false, - value: {}, - issues: [{ ...stringIssue, abortEarly: true }], - } satisfies FailureDataset> - ); + test('for invalid entries', () => { + const input = { key1: false, key2: 123, nested: null }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: null, + expected: 'Object', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: null, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid nested entries', () => { + const input = { + key1: 'value', + key2: 'value', + nested: { + key1: 123, + key2: null, + }, + }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: 'value', + expected: 'number', + received: '"value"', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: input.key2, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: 123, + expected: 'string', + received: '123', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key1', + value: input.nested.key1, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: null, + expected: 'number', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key2', + value: input.nested.key2, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries with abort early', () => { + const input = { key1: false, key2: 123, nested: null }; + expect( + schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid exact optional entry', () => { + const schema = strictObject({ key: exactOptional(string()) }); + const input = { key: undefined }; + expect(schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); }); test('for unknown entries', () => { const input = { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other1: 'foo', other2: 123, }; expect(schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: { key: input.key, nested: input.nested }, + value: { key1: 'foo', key2: 123, nested: { key1: 'foo', key2: 123 } }, issues: [ { ...baseInfo, kind: 'schema', type: 'strict_object', - input: input.other1, + input: 'other1', expected: 'never', - received: `"${input.other1}"`, + received: '"other1"', path: [ { type: 'object', - origin: 'value', + origin: 'key', input, key: 'other1', value: input.other1, diff --git a/library/src/schemas/strictObject/strictObject.ts b/library/src/schemas/strictObject/strictObject.ts index e4f7e708f..160c7f30c 100644 --- a/library/src/schemas/strictObject/strictObject.ts +++ b/library/src/schemas/strictObject/strictObject.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseSchema, ErrorMessage, @@ -98,58 +99,95 @@ export function strictObject( dataset.typed = true; dataset.value = {}; - // Parse schema of each entry - // Hint: We do not distinguish between missing and `undefined` entries. - // The reason for this decision is that it reduces the bundle size, and - // we also expect that most users will expect this behavior. + // Process each object entry of schema for (const key in this.entries) { - // Get and parse value of key - const value = input[key as keyof typeof input]; - const valueDataset = this.entries[key]['~run']({ value }, config); - - // If there are issues, capture them - if (valueDataset.issues) { - // Create object path item - const pathItem: ObjectPathItem = { - type: 'object', - origin: 'value', - input: input as Record, - key, - value, - }; - - // Add modified entry dataset issues to issues - for (const issue of valueDataset.issues) { - if (issue.path) { - issue.path.unshift(pathItem); - } else { + const valueSchema = this.entries[key]; + + // If key is present or its an optional schema with a default value, + // parse input of key or default value + if ( + key in input || + ((valueSchema.type === 'exact_optional' || + valueSchema.type === 'optional' || + valueSchema.type === 'nullish') && + // @ts-expect-error + valueSchema.default !== undefined) + ) { + const value: unknown = + key in input + ? // @ts-expect-error + input[key] + : getDefault(valueSchema); + const valueDataset = valueSchema['~run']({ value }, config); + + // If there are issues, capture them + if (valueDataset.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + type: 'object', + origin: 'value', + input: input as Record, + key, + value, + }; + + // Add modified entry dataset issues to issues + for (const issue of valueDataset.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + // @ts-expect-error + issue.path = [pathItem]; + } // @ts-expect-error - issue.path = [pathItem]; + dataset.issues?.push(issue); + } + if (!dataset.issues) { + // @ts-expect-error + dataset.issues = valueDataset.issues; + } + + // If necessary, abort early + if (config.abortEarly) { + dataset.typed = false; + break; } - // @ts-expect-error - dataset.issues?.push(issue); - } - if (!dataset.issues) { - // @ts-expect-error - dataset.issues = valueDataset.issues; } - // If necessary, abort early - if (config.abortEarly) { + // If not typed, set typed to `false` + if (!valueDataset.typed) { dataset.typed = false; - break; } - } - // If not typed, set typed to `false` - if (!valueDataset.typed) { - dataset.typed = false; - } - - // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { + // Add entry to dataset // @ts-expect-error dataset.value[key] = valueDataset.value; + + // Otherwise, if key is missing and required, add issue + } else if ( + valueSchema.type !== 'exact_optional' && + valueSchema.type !== 'optional' && + valueSchema.type !== 'nullish' + ) { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + // @ts-expect-error + value: input[key], + }, + ], + }); + + // If necessary, abort early + if (config.abortEarly) { + break; + } } } @@ -157,17 +195,17 @@ export function strictObject( if (!dataset.issues || !config.abortEarly) { for (const key in input) { if (!(key in this.entries)) { - const value: unknown = input[key as keyof typeof input]; - _addIssue(this, 'type', dataset, config, { - input: value, + _addIssue(this, 'key', dataset, config, { + input: key, expected: 'never', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: input as Record, key, - value, + // @ts-expect-error + value: input[key], }, ], }); diff --git a/library/src/schemas/strictObject/strictObjectAsync.test-d.ts b/library/src/schemas/strictObject/strictObjectAsync.test-d.ts index ce4f6057d..429dcc425 100644 --- a/library/src/schemas/strictObject/strictObjectAsync.test-d.ts +++ b/library/src/schemas/strictObject/strictObjectAsync.test-d.ts @@ -1,17 +1,26 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { ReadonlyAction, TransformAction } from '../../actions/index.ts'; -import type { SchemaWithPipe } from '../../methods/index.ts'; +import type { + SchemaWithPipe, + SchemaWithPipeAsync, +} from '../../methods/index.ts'; import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; -import type { NullishSchema } from '../nullish/index.ts'; +import type { AnySchema } from '../any/index.ts'; +import type { + ExactOptionalSchema, + ExactOptionalSchemaAsync, +} from '../exactOptional/index.ts'; +import type { NullishSchema, NullishSchemaAsync } from '../nullish/index.ts'; import { type NumberIssue, type NumberSchema } from '../number/index.ts'; -import type { ObjectIssue, ObjectSchema } from '../object/index.ts'; -import type { OptionalSchema } from '../optional/index.ts'; +import type { ObjectIssue, ObjectSchemaAsync } from '../object/index.ts'; +import type { OptionalSchema, OptionalSchemaAsync } from '../optional/index.ts'; import { string, type StringIssue, type StringSchema, } from '../string/index.ts'; import type { UndefinedableSchema } from '../undefinedable/index.ts'; +import type { UnknownSchema } from '../unknown/index.ts'; import { strictObjectAsync, type StrictObjectSchemaAsync, @@ -47,43 +56,210 @@ describe('strictObjectAsync', () => { describe('should infer correct types', () => { type Schema = StrictObjectSchemaAsync< { - key1: StringSchema; - key2: OptionalSchema, 'foo'>; - key3: NullishSchema, undefined>; - key4: ObjectSchema<{ key: NumberSchema }, undefined>; - key5: SchemaWithPipe<[StringSchema, ReadonlyAction]>; - key6: UndefinedableSchema, 'bar'>; - key7: SchemaWithPipe< + key00: StringSchema; + key01: AnySchema; + key02: UnknownSchema; + key03: ObjectSchemaAsync<{ key: NumberSchema }, undefined>; + key04: SchemaWithPipe< + [StringSchema, ReadonlyAction] + >; + key05: UndefinedableSchema, 'bar'>; + key06: SchemaWithPipe< + [ + OptionalSchema, undefined>, + TransformAction, + ] + >; + key07: SchemaWithPipeAsync< [ OptionalSchema, undefined>, - TransformAction, + TransformAction, + ] + >; + key08: SchemaWithPipeAsync< + [ + OptionalSchemaAsync, undefined>, + TransformAction, ] >; + + // ExactOptionalSchema + key10: ExactOptionalSchema, undefined>; + key11: ExactOptionalSchema, 'foo'>; + key12: ExactOptionalSchema, () => 'foo'>; + + // ExactOptionalSchemaAsync + key20: ExactOptionalSchemaAsync, undefined>; + key21: ExactOptionalSchemaAsync, 'foo'>; + key22: ExactOptionalSchemaAsync, () => 'foo'>; + key23: ExactOptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + + // OptionalSchema + key30: OptionalSchema, undefined>; + key31: OptionalSchema, 'foo'>; + key32: OptionalSchema, () => undefined>; + key33: OptionalSchema, () => 'foo'>; + + // OptionalSchemaAsync + key40: OptionalSchemaAsync, undefined>; + key41: OptionalSchemaAsync, 'foo'>; + key42: OptionalSchemaAsync, () => undefined>; + key43: OptionalSchemaAsync, () => 'foo'>; + key44: OptionalSchemaAsync< + StringSchema, + () => Promise + >; + key45: OptionalSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; + + // NullishSchema + key50: NullishSchema, undefined>; + key51: NullishSchema, null>; + key52: NullishSchema, 'foo'>; + key53: NullishSchema, () => undefined>; + key54: NullishSchema, () => null>; + key55: NullishSchema, () => 'foo'>; + + // NullishSchemaAsync + key60: NullishSchemaAsync, undefined>; + key61: NullishSchemaAsync, null>; + key62: NullishSchemaAsync, 'foo'>; + key63: NullishSchemaAsync, () => undefined>; + key64: NullishSchemaAsync, () => null>; + key65: NullishSchemaAsync, () => 'foo'>; + key66: NullishSchemaAsync< + StringSchema, + () => Promise + >; + key67: NullishSchemaAsync, () => Promise>; + key68: NullishSchemaAsync< + StringSchema, + () => Promise<'foo'> + >; }, undefined >; test('of input', () => { expectTypeOf>().toEqualTypeOf<{ - key1: string; - key2?: string; - key3?: string | null | undefined; - key4: { key: number }; - key5: string; - key6: string | undefined; - key7?: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + key04: string; + key05: string | undefined; + key06?: string | undefined; + key07?: string | undefined; + key08?: string | undefined; + + // ExactOptionalSchema + key10?: string; + key11?: string; + key12?: string; + + // ExactOptionalSchemaAsync + key20?: string; + key21?: string; + key22?: string; + key23?: string; + + // OptionalSchema + key30?: string | undefined; + key31?: string | undefined; + key32?: string | undefined; + key33?: string | undefined; + + // OptionalSchemaAsync + key40?: string | undefined; + key41?: string | undefined; + key42?: string | undefined; + key43?: string | undefined; + key44?: string | undefined; + key45?: string | undefined; + + // NullishSchema + key50?: string | null | undefined; + key51?: string | null | undefined; + key52?: string | null | undefined; + key53?: string | null | undefined; + key54?: string | null | undefined; + key55?: string | null | undefined; + + // NullishSchemaAsync + key60?: string | null | undefined; + key61?: string | null | undefined; + key62?: string | null | undefined; + key63?: string | null | undefined; + key64?: string | null | undefined; + key65?: string | null | undefined; + key66?: string | null | undefined; + key67?: string | null | undefined; + key68?: string | null | undefined; }>(); }); test('of output', () => { expectTypeOf>().toEqualTypeOf<{ - key1: string; - key2: string; - key3?: string | null | undefined; - key4: { key: number }; - readonly key5: string; - key6: string; - key7: string; + key00: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + key01: any; + key02: unknown; + key03: { key: number }; + readonly key04: string; + key05: string; + key06?: number; + key07?: number; + key08?: number; + + // ExactOptionalSchema + key10?: string; + key11: string; + key12: string; + + // ExactOptionalSchemaAsync + key20?: string; + key21: string; + key22: string; + key23: string; + + // OptionalSchema + key30?: string | undefined; + key31: string; + key32: string | undefined; + key33: string; + + // OptionalSchemaAsync + key40?: string | undefined; + key41: string; + key42: string | undefined; + key43: string; + key44: string | undefined; + key45: string; + + // NullishSchema + key50?: string | null | undefined; + key51: string | null; + key52: string; + key53: string | undefined; + key54: string | null; + key55: string; + + // NullishSchemaAsync + key60?: string | null | undefined; + key61: string | null; + key62: string; + key63: string | undefined; + key64: string | null; + key65: string; + key66: string | undefined; + key67: string | null; + key68: string; }>(); }); diff --git a/library/src/schemas/strictObject/strictObjectAsync.test.ts b/library/src/schemas/strictObject/strictObjectAsync.test.ts index b1f87c83f..15a65e207 100644 --- a/library/src/schemas/strictObject/strictObjectAsync.test.ts +++ b/library/src/schemas/strictObject/strictObjectAsync.test.ts @@ -4,11 +4,14 @@ import { expectNoSchemaIssueAsync, expectSchemaIssueAsync, } from '../../vitest/index.ts'; -import { nullish } from '../nullish/index.ts'; +import { any } from '../any/index.ts'; +import { exactOptional, exactOptionalAsync } from '../exactOptional/index.ts'; +import { nullish, nullishAsync } from '../nullish/index.ts'; import { number } from '../number/index.ts'; import { objectAsync } from '../object/index.ts'; -import { optional } from '../optional/index.ts'; -import { string, type StringIssue } from '../string/index.ts'; +import { optional, optionalAsync } from '../optional/index.ts'; +import { string } from '../string/index.ts'; +import { unknown } from '../unknown/index.ts'; import { strictObjectAsync, type StrictObjectSchemaAsync, @@ -150,30 +153,269 @@ describe('strictObjectAsync', () => { ); }); + test('for exact optional entry', async () => { + await expectNoSchemaIssueAsync( + strictObjectAsync({ key: exactOptional(string()) }), + [{}, { key: 'foo' }] + ); + await expectNoSchemaIssueAsync( + strictObjectAsync({ key: exactOptionalAsync(string()) }), + [{}, { key: 'foo' }] + ); + }); + + test('for exact optional entry with default', async () => { + // Sync + expect( + await strictObjectAsync({ key: exactOptional(string(), 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ key: exactOptional(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + + // Async + expect( + await strictObjectAsync({ key: exactOptionalAsync(string(), 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ + key: exactOptionalAsync(string(), () => 'foo'), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + }); + test('for optional entry', async () => { await expectNoSchemaIssueAsync( strictObjectAsync({ key: optional(string()) }), - [ - {}, - // @ts-expect-error - { key: undefined }, - { key: 'foo' }, - ] + [{}, { key: undefined }, { key: 'foo' }] + ); + await expectNoSchemaIssueAsync( + strictObjectAsync({ key: optionalAsync(string()) }), + [{}, { key: undefined }, { key: 'foo' }] ); }); + test('for optional entry with default', async () => { + // Sync + expect( + await strictObjectAsync({ key: optional(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ key: optional(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ + key: optional(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + + // Async + expect( + await strictObjectAsync({ key: optionalAsync(string(), 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ key: optionalAsync(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ + key: optionalAsync(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + expect( + await strictObjectAsync({ + key: optionalAsync(string(), async () => 'foo'), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ + key: optionalAsync(string(), async () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + }); + test('for nullish entry', async () => { await expectNoSchemaIssueAsync( strictObjectAsync({ key: nullish(number()) }), [{}, { key: undefined }, { key: null }, { key: 123 }] ); + await expectNoSchemaIssueAsync( + strictObjectAsync({ key: nullishAsync(number()) }), + [{}, { key: undefined }, { key: null }, { key: 123 }] + ); + }); + + test('for nullish entry with default', async () => { + // Sync + expect( + await strictObjectAsync({ key: nullish(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ key: nullish(string(), null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await strictObjectAsync({ key: nullish(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ key: nullish(string(), () => null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await strictObjectAsync({ key: nullish(string(), () => undefined) })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + + // Async + expect( + await strictObjectAsync({ key: nullishAsync(string(), 'foo') })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ key: nullishAsync(string(), null) })['~run']( + { value: {} }, + {} + ) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await strictObjectAsync({ key: nullishAsync(string(), () => 'foo') })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ key: nullishAsync(string(), () => null) })[ + '~run' + ]({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await strictObjectAsync({ + key: nullishAsync(string(), () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); + expect( + await strictObjectAsync({ + key: nullishAsync(string(), async () => 'foo'), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: 'foo' }, + }); + expect( + await strictObjectAsync({ + key: nullishAsync(string(), async () => null), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: null }, + }); + expect( + await strictObjectAsync({ + key: nullishAsync(string(), async () => undefined), + })['~run']({ value: {} }, {}) + ).toStrictEqual({ + typed: true, + value: { key: undefined }, + }); }); }); describe('should return dataset with nested issues', () => { const schema = strictObjectAsync({ - key: string(), - nested: objectAsync({ key: number() }), + key1: string(), + key2: number(), + nested: strictObjectAsync({ key1: string(), key2: number() }), }); const baseInfo = { @@ -185,42 +427,41 @@ describe('strictObjectAsync', () => { abortPipeEarly: undefined, }; - const stringIssue: StringIssue = { - ...baseInfo, - kind: 'schema', - type: 'string', - input: undefined, - expected: 'string', - received: 'undefined', - path: [ - { - type: 'object', - origin: 'value', - input: {}, - key: 'key', - value: undefined, - }, - ], - }; - test('for missing entries', async () => { - expect(await schema['~run']({ value: {} }, {})).toStrictEqual({ + const input = { key2: 123 }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: {}, + value: input, issues: [ - stringIssue, { ...baseInfo, kind: 'schema', - type: 'object', + type: 'strict_object', input: undefined, - expected: 'Object', + expected: '"key1"', received: 'undefined', path: [ { type: 'object', - origin: 'value', - input: {}, + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"nested"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, key: 'nested', value: undefined, }, @@ -231,7 +472,7 @@ describe('strictObjectAsync', () => { }); test('for missing nested entries', async () => { - const input = { key: 'value', nested: {} }; + const input = { key1: 'value', nested: {} }; expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, value: input, @@ -239,23 +480,138 @@ describe('strictObjectAsync', () => { { ...baseInfo, kind: 'schema', - type: 'number', + type: 'strict_object', input: undefined, - expected: 'number', + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key2', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key1"', received: 'undefined', path: [ { type: 'object', origin: 'value', - input: { key: 'value', nested: {} }, + input, key: 'nested', value: {}, }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ { type: 'object', origin: 'value', + input, + key: 'nested', + value: {}, + }, + { + type: 'object', + origin: 'key', + input: input.nested, + key: 'key2', + value: undefined, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing entries with abort early', async () => { + const input = { key2: 123 }; + expect( + await schema['~run']({ value: input }, { abortEarly: true }) + ).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input, + key: 'key1', + value: undefined, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for missing any and unknown entry', async () => { + const schema = strictObjectAsync({ key1: any(), key2: unknown() }); + expect(await schema['~run']({ value: {} }, {})).toStrictEqual({ + typed: false, + value: {}, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key1"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', + input: {}, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: undefined, + expected: '"key2"', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'key', input: {}, - key: 'key', + key: 'key2', value: undefined, }, ], @@ -264,38 +620,233 @@ describe('strictObjectAsync', () => { } satisfies FailureDataset>); }); - test('with abort early', async () => { + test('for invalid entries', async () => { + const input = { key1: false, key2: 123, nested: null }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'strict_object', + input: null, + expected: 'Object', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: null, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid nested entries', async () => { + const input = { + key1: 'value', + key2: 'value', + nested: { + key1: 123, + key2: null, + }, + }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: 'value', + expected: 'number', + received: '"value"', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: input.key2, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: 123, + expected: 'string', + received: '123', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key1', + value: input.nested.key1, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'number', + input: null, + expected: 'number', + received: 'null', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'nested', + value: input.nested, + }, + { + type: 'object', + origin: 'value', + input: input.nested, + key: 'key2', + value: input.nested.key2, + }, + ], + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid entries with abort early', async () => { + const input = { key1: false, key2: 123, nested: null }; expect( - await schema['~run']({ value: {} }, { abortEarly: true }) + await schema['~run']({ value: input }, { abortEarly: true }) ).toStrictEqual({ typed: false, value: {}, - issues: [{ ...stringIssue, abortEarly: true }], + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: false, + expected: 'string', + received: 'false', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: false, + }, + ], + abortEarly: true, + }, + ], + } satisfies FailureDataset>); + }); + + test('for invalid exact optional entry', async () => { + const schema = strictObjectAsync({ + key1: exactOptional(string()), + key2: exactOptionalAsync(string()), + }); + const input = { key1: undefined, key2: undefined }; + expect(await schema['~run']({ value: input }, {})).toStrictEqual({ + typed: false, + value: input, + issues: [ + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key1', + value: undefined, + }, + ], + }, + { + ...baseInfo, + kind: 'schema', + type: 'string', + input: undefined, + expected: 'string', + received: 'undefined', + path: [ + { + type: 'object', + origin: 'value', + input, + key: 'key2', + value: undefined, + }, + ], + }, + ], } satisfies FailureDataset>); }); test('for unknown entries', async () => { const input = { - key: 'foo', - nested: { key: 123 }, + key1: 'foo', + key2: 123, + nested: { key1: 'foo', key2: 123 }, other1: 'foo', other2: 123, }; expect(await schema['~run']({ value: input }, {})).toStrictEqual({ typed: false, - value: { key: input.key, nested: input.nested }, + value: { key1: 'foo', key2: 123, nested: { key1: 'foo', key2: 123 } }, issues: [ { ...baseInfo, kind: 'schema', type: 'strict_object', - input: input.other1, + input: 'other1', expected: 'never', - received: `"${input.other1}"`, + received: '"other1"', path: [ { type: 'object', - origin: 'value', + origin: 'key', input, key: 'other1', value: input.other1, diff --git a/library/src/schemas/strictObject/strictObjectAsync.ts b/library/src/schemas/strictObject/strictObjectAsync.ts index 6938ce9ef..1893b0783 100644 --- a/library/src/schemas/strictObject/strictObjectAsync.ts +++ b/library/src/schemas/strictObject/strictObjectAsync.ts @@ -1,3 +1,4 @@ +import { getDefault } from '../../methods/index.ts'; import type { BaseSchemaAsync, ErrorMessage, @@ -9,6 +10,7 @@ import type { OutputDataset, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { strictObject } from './strictObject.ts'; import type { StrictObjectIssue } from './types.ts'; /** @@ -29,7 +31,7 @@ export interface StrictObjectSchemaAsync< /** * The schema reference. */ - readonly reference: typeof strictObjectAsync; + readonly reference: typeof strictObject | typeof strictObjectAsync; /** * The expected property. */ @@ -101,66 +103,112 @@ export function strictObjectAsync( dataset.typed = true; dataset.value = {}; - // Parse schema of each entry - // Hint: We do not distinguish between missing and `undefined` entries. - // The reason for this decision is that it reduces the bundle size, and - // we also expect that most users will expect this behavior. + // If key is present or its an optional schema with a default value, + // parse input of key or default value asynchronously const valueDatasets = await Promise.all( - Object.entries(this.entries).map(async ([key, schema]) => { - const value = input[key as keyof typeof input]; + Object.entries(this.entries).map(async ([key, valueSchema]) => { + if ( + key in input || + ((valueSchema.type === 'exact_optional' || + valueSchema.type === 'optional' || + valueSchema.type === 'nullish') && + // @ts-expect-error + valueSchema.default !== undefined) + ) { + const value: unknown = + key in input + ? // @ts-expect-error + input[key] + : await getDefault(valueSchema); + return [ + key, + value, + valueSchema, + await valueSchema['~run']({ value }, config), + ] as const; + } return [ key, - value, - await schema['~run']({ value }, config), + // @ts-expect-error + input[key] as unknown, + valueSchema, + null, ] as const; }) ); - // Process each value dataset - for (const [key, value, valueDataset] of valueDatasets) { - // If there are issues, capture them - if (valueDataset.issues) { - // Create object path item - const pathItem: ObjectPathItem = { - type: 'object', - origin: 'value', - input: input as Record, - key, - value, - }; - - // Add modified entry dataset issues to issues - for (const issue of valueDataset.issues) { - if (issue.path) { - issue.path.unshift(pathItem); - } else { + // Process each object entry of schema + for (const [key, value, valueSchema, valueDataset] of valueDatasets) { + // If key is present or its an optional schema with a default value, + // process its value dataset + if (valueDataset) { + // If there are issues, capture them + if (valueDataset.issues) { + // Create object path item + const pathItem: ObjectPathItem = { + type: 'object', + origin: 'value', + input: input as Record, + key, + value, + }; + + // Add modified entry dataset issues to issues + for (const issue of valueDataset.issues) { + if (issue.path) { + issue.path.unshift(pathItem); + } else { + // @ts-expect-error + issue.path = [pathItem]; + } // @ts-expect-error - issue.path = [pathItem]; + dataset.issues?.push(issue); + } + if (!dataset.issues) { + // @ts-expect-error + dataset.issues = valueDataset.issues; + } + + // If necessary, abort early + if (config.abortEarly) { + dataset.typed = false; + break; } - // @ts-expect-error - dataset.issues?.push(issue); - } - if (!dataset.issues) { - // @ts-expect-error - dataset.issues = valueDataset.issues; } - // If necessary, abort early - if (config.abortEarly) { + // If not typed, set typed to `false` + if (!valueDataset.typed) { dataset.typed = false; - break; } - } - - // If not typed, set typed to `false` - if (!valueDataset.typed) { - dataset.typed = false; - } - // Add entry to dataset if necessary - if (valueDataset.value !== undefined || key in input) { + // Add entry to dataset // @ts-expect-error dataset.value[key] = valueDataset.value; + + // Otherwise, if key is missing and required, add issue + } else if ( + valueSchema.type !== 'exact_optional' && + valueSchema.type !== 'optional' && + valueSchema.type !== 'nullish' + ) { + _addIssue(this, 'key', dataset, config, { + input: undefined, + expected: `"${key}"`, + path: [ + { + type: 'object', + origin: 'key', + input: input as Record, + key, + value, + }, + ], + }); + + // If necessary, abort early + if (config.abortEarly) { + break; + } } } @@ -168,17 +216,17 @@ export function strictObjectAsync( if (!dataset.issues || !config.abortEarly) { for (const key in input) { if (!(key in this.entries)) { - const value: unknown = input[key as keyof typeof input]; - _addIssue(this, 'type', dataset, config, { - input: value, + _addIssue(this, 'key', dataset, config, { + input: key, expected: 'never', path: [ { type: 'object', - origin: 'value', + origin: 'key', input: input as Record, key, - value, + // @ts-expect-error + value: input[key], }, ], }); diff --git a/library/src/schemas/strictObject/types.ts b/library/src/schemas/strictObject/types.ts index 5122707c3..db8930eeb 100644 --- a/library/src/schemas/strictObject/types.ts +++ b/library/src/schemas/strictObject/types.ts @@ -15,5 +15,5 @@ export interface StrictObjectIssue extends BaseIssue { /** * The expected property. */ - readonly expected: 'Object' | 'never'; + readonly expected: 'Object' | `"${string}"` | 'never'; } diff --git a/library/src/schemas/strictTuple/strictTuple.ts b/library/src/schemas/strictTuple/strictTuple.ts index 75d27d186..43536aaad 100644 --- a/library/src/schemas/strictTuple/strictTuple.ts +++ b/library/src/schemas/strictTuple/strictTuple.ts @@ -150,9 +150,8 @@ export function strictTuple( !(dataset.issues && config.abortEarly) && this.items.length < input.length ) { - const value = input[items.length]; _addIssue(this, 'type', dataset, config, { - input: value, + input: input[this.items.length], expected: 'never', path: [ { @@ -160,7 +159,7 @@ export function strictTuple( origin: 'value', input, key: this.items.length, - value, + value: input[this.items.length], }, ], }); diff --git a/library/src/schemas/strictTuple/strictTupleAsync.ts b/library/src/schemas/strictTuple/strictTupleAsync.ts index 84e9e4bed..b2222511c 100644 --- a/library/src/schemas/strictTuple/strictTupleAsync.ts +++ b/library/src/schemas/strictTuple/strictTupleAsync.ts @@ -10,6 +10,7 @@ import type { TupleItemsAsync, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { strictTuple } from './strictTuple.ts'; import type { StrictTupleIssue } from './types.ts'; /** @@ -30,7 +31,7 @@ export interface StrictTupleSchemaAsync< /** * The schema reference. */ - readonly reference: typeof strictTupleAsync; + readonly reference: typeof strictTuple | typeof strictTupleAsync; /** * The expected property. */ @@ -158,9 +159,8 @@ export function strictTupleAsync( !(dataset.issues && config.abortEarly) && this.items.length < input.length ) { - const value = input[items.length]; _addIssue(this, 'type', dataset, config, { - input: value, + input: input[this.items.length], expected: 'never', path: [ { @@ -168,7 +168,7 @@ export function strictTupleAsync( origin: 'value', input, key: this.items.length, - value, + value: input[this.items.length], }, ], }); diff --git a/library/src/schemas/tuple/tupleAsync.ts b/library/src/schemas/tuple/tupleAsync.ts index c94458814..843d0a7cf 100644 --- a/library/src/schemas/tuple/tupleAsync.ts +++ b/library/src/schemas/tuple/tupleAsync.ts @@ -10,6 +10,7 @@ import type { TupleItemsAsync, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { tuple } from './tuple.ts'; import type { TupleIssue } from './types.ts'; /** @@ -30,7 +31,7 @@ export interface TupleSchemaAsync< /** * The schema reference. */ - readonly reference: typeof tupleAsync; + readonly reference: typeof tuple | typeof tupleAsync; /** * The expected property. */ diff --git a/library/src/schemas/tupleWithRest/tupleWithRestAsync.ts b/library/src/schemas/tupleWithRest/tupleWithRestAsync.ts index 81bbd2b38..29c58fa47 100644 --- a/library/src/schemas/tupleWithRest/tupleWithRestAsync.ts +++ b/library/src/schemas/tupleWithRest/tupleWithRestAsync.ts @@ -14,6 +14,7 @@ import type { TupleItemsAsync, } from '../../types/index.ts'; import { _addIssue, _getStandardProps } from '../../utils/index.ts'; +import type { tupleWithRest } from './tupleWithRest.ts'; import type { TupleWithRestIssue } from './types.ts'; /** @@ -37,7 +38,7 @@ export interface TupleWithRestSchemaAsync< /** * The schema reference. */ - readonly reference: typeof tupleWithRestAsync; + readonly reference: typeof tupleWithRest | typeof tupleWithRestAsync; /** * The expected property. */ diff --git a/library/src/schemas/undefinedable/undefinedable.test.ts b/library/src/schemas/undefinedable/undefinedable.test.ts index 26147eea0..fd3eceab5 100644 --- a/library/src/schemas/undefinedable/undefinedable.test.ts +++ b/library/src/schemas/undefinedable/undefinedable.test.ts @@ -1,6 +1,10 @@ import { describe, expect, test } from 'vitest'; -import { expectNoSchemaIssue } from '../../vitest/index.ts'; -import { string, type StringSchema } from '../string/index.ts'; +import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; import { undefinedable, type UndefinedableSchema } from './undefinedable.ts'; describe('undefinedable', () => { @@ -79,6 +83,24 @@ describe('undefinedable', () => { }); }); + describe('should return dataset with issues', () => { + const schema = undefinedable(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', () => { + expectSchemaIssue(schema, baseIssue, [123, true, {}]); + }); + + test('for null', () => { + expectSchemaIssue(schema, baseIssue, [null]); + }); + }); + describe('should return dataset without default', () => { test('for undefined default', () => { expectNoSchemaIssue(undefinedable(string()), [undefined, 'foo']); diff --git a/library/src/schemas/undefinedable/undefinedable.ts b/library/src/schemas/undefinedable/undefinedable.ts index 6e53429c9..4edfcc9cc 100644 --- a/library/src/schemas/undefinedable/undefinedable.ts +++ b/library/src/schemas/undefinedable/undefinedable.ts @@ -44,23 +44,23 @@ export interface UndefinedableSchema< } /** - * Creates a undefinedable schema. + * Creates an undefinedable schema. * * @param wrapped The wrapped schema. * - * @returns A undefinedable schema. + * @returns An undefinedable schema. */ export function undefinedable< const TWrapped extends BaseSchema>, >(wrapped: TWrapped): UndefinedableSchema; /** - * Creates a undefinedable schema. + * Creates an undefinedable schema. * * @param wrapped The wrapped schema. * @param default_ The default value. * - * @returns A undefinedable schema. + * @returns An undefinedable schema. */ export function undefinedable< const TWrapped extends BaseSchema>, diff --git a/library/src/schemas/undefinedable/undefinedableAsync.test.ts b/library/src/schemas/undefinedable/undefinedableAsync.test.ts index 10306f1d3..77ed20aa3 100644 --- a/library/src/schemas/undefinedable/undefinedableAsync.test.ts +++ b/library/src/schemas/undefinedable/undefinedableAsync.test.ts @@ -1,6 +1,13 @@ import { describe, expect, test } from 'vitest'; -import { expectNoSchemaIssueAsync } from '../../vitest/index.ts'; -import { string, type StringSchema } from '../string/index.ts'; +import { + expectNoSchemaIssueAsync, + expectSchemaIssueAsync, +} from '../../vitest/index.ts'; +import { + string, + type StringIssue, + type StringSchema, +} from '../string/index.ts'; import { undefinedableAsync, type UndefinedableSchemaAsync, @@ -110,6 +117,24 @@ describe('undefinedableAsync', () => { }); }); + describe('should return dataset with issues', () => { + const schema = undefinedableAsync(string('message')); + const baseIssue: Omit = { + kind: 'schema', + type: 'string', + expected: 'string', + message: 'message', + }; + + test('for invalid wrapper type', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [123, true, {}]); + }); + + test('for null', async () => { + await expectSchemaIssueAsync(schema, baseIssue, [null]); + }); + }); + describe('should return dataset without default', () => { test('for undefined default', async () => { await expectNoSchemaIssueAsync(undefinedableAsync(string()), [ diff --git a/library/src/schemas/undefinedable/undefinedableAsync.ts b/library/src/schemas/undefinedable/undefinedableAsync.ts index 5b2116e05..910a37bf7 100644 --- a/library/src/schemas/undefinedable/undefinedableAsync.ts +++ b/library/src/schemas/undefinedable/undefinedableAsync.ts @@ -10,6 +10,7 @@ import type { } from '../../types/index.ts'; import { _getStandardProps } from '../../utils/index.ts'; import type { InferUndefinedableOutput } from './types.ts'; +import type { undefinedable } from './undefinedable.ts'; /** * Undefinedable schema async interface. @@ -31,7 +32,7 @@ export interface UndefinedableSchemaAsync< /** * The schema reference. */ - readonly reference: typeof undefinedableAsync; + readonly reference: typeof undefinedable | typeof undefinedableAsync; /** * The expected property. */ diff --git a/library/src/schemas/union/unionAsync.ts b/library/src/schemas/union/unionAsync.ts index ca50a3178..6f23fa179 100644 --- a/library/src/schemas/union/unionAsync.ts +++ b/library/src/schemas/union/unionAsync.ts @@ -18,6 +18,7 @@ import { _joinExpects, } from '../../utils/index.ts'; import type { UnionIssue } from './types.ts'; +import type { union } from './union.ts'; import { _subIssues } from './utils/index.ts'; /** @@ -50,7 +51,7 @@ export interface UnionSchemaAsync< /** * The schema reference. */ - readonly reference: typeof unionAsync; + readonly reference: typeof union | typeof unionAsync; /** * The union options. */ diff --git a/library/src/schemas/variant/types.ts b/library/src/schemas/variant/types.ts index e223cac86..89074f9cc 100644 --- a/library/src/schemas/variant/types.ts +++ b/library/src/schemas/variant/types.ts @@ -65,7 +65,7 @@ export interface VariantOptionSchema export interface VariantOptionSchemaAsync extends BaseSchemaAsync> { readonly type: 'variant'; - readonly reference: typeof variantAsync; + readonly reference: typeof variant | typeof variantAsync; readonly key: string; readonly options: VariantOptionsAsync; readonly message: ErrorMessage | undefined; diff --git a/library/src/schemas/variant/variantAsync.ts b/library/src/schemas/variant/variantAsync.ts index ee736c612..1546db4ac 100644 --- a/library/src/schemas/variant/variantAsync.ts +++ b/library/src/schemas/variant/variantAsync.ts @@ -18,6 +18,7 @@ import type { VariantOptionSchema, VariantOptionSchemaAsync, } from './types.ts'; +import type { variant } from './variant.ts'; /** * Variant schema async interface. @@ -38,7 +39,7 @@ export interface VariantSchemaAsync< /** * The schema reference. */ - readonly reference: typeof variantAsync; + readonly reference: typeof variant | typeof variantAsync; /** * The expected property. */ diff --git a/library/src/types/issue.ts b/library/src/types/issue.ts index 134e4ee4f..348c35fef 100644 --- a/library/src/types/issue.ts +++ b/library/src/types/issue.ts @@ -3,6 +3,8 @@ import type { ArrayIssue, ArraySchema, ArraySchemaAsync, + ExactOptionalSchema, + ExactOptionalSchemaAsync, IntersectIssue, IntersectSchema, IntersectSchemaAsync, @@ -497,6 +499,17 @@ export type IssueDotPath< | DotPath : // Wrapped (sync) TSchema extends + | ExactOptionalSchema< + infer TWrapped, + Default< + BaseSchema< + unknown, + unknown, + BaseIssue + >, + never + > + > | LazySchema | NonNullableSchema< infer TWrapped, @@ -560,6 +573,22 @@ export type IssueDotPath< ? IssueDotPath : // Wrapped (async) TSchema extends + | ExactOptionalSchemaAsync< + infer TWrapped, + DefaultAsync< + | BaseSchema< + unknown, + unknown, + BaseIssue + > + | BaseSchemaAsync< + unknown, + unknown, + BaseIssue + >, + never + > + > | LazySchemaAsync | NonNullableSchemaAsync< infer TWrapped, diff --git a/library/src/types/object.ts b/library/src/types/object.ts index 1964c9d91..a1f82c229 100644 --- a/library/src/types/object.ts +++ b/library/src/types/object.ts @@ -1,9 +1,13 @@ import type { ReadonlyAction } from '../actions/index.ts'; import type { SchemaWithPipe, SchemaWithPipeAsync } from '../methods/index.ts'; import type { + ExactOptionalSchema, + ExactOptionalSchemaAsync, LooseObjectIssue, LooseObjectSchema, LooseObjectSchemaAsync, + NullishSchema, + NullishSchemaAsync, ObjectIssue, ObjectSchema, ObjectSchemaAsync, @@ -22,13 +26,44 @@ import type { ErrorMessage } from './other.ts'; import type { BaseSchema, BaseSchemaAsync } from './schema.ts'; import type { MarkOptional, MaybeReadonly, Prettify } from './utils.ts'; +/** + * Optional entry schema type. + */ +type OptionalEntrySchema = + | ExactOptionalSchema< + BaseSchema>, + unknown + > + | NullishSchema>, unknown> + | OptionalSchema>, unknown>; + +/** + * Optional entry schema async type. + */ +type OptionalEntrySchemaAsync = + | ExactOptionalSchemaAsync< + | BaseSchema> + | BaseSchemaAsync>, + unknown + > + | NullishSchemaAsync< + | BaseSchema> + | BaseSchemaAsync>, + unknown + > + | OptionalSchemaAsync< + | BaseSchema> + | BaseSchemaAsync>, + unknown + >; + /** * Object entries interface. */ export interface ObjectEntries { [key: string]: | BaseSchema> - | OptionalSchema>, unknown>; + | OptionalEntrySchema; } /** @@ -38,12 +73,8 @@ export interface ObjectEntriesAsync { [key: string]: | BaseSchema> | BaseSchemaAsync> - | OptionalSchema>, unknown> - | OptionalSchemaAsync< - | BaseSchema> - | BaseSchemaAsync>, - unknown - >; + | OptionalEntrySchema + | OptionalEntrySchemaAsync; } /** @@ -85,17 +116,6 @@ export type ObjectKeys< >, > = MaybeReadonly<[keyof TSchema['entries'], ...(keyof TSchema['entries'])[]]>; -/** - * Question mark schema type. - */ -type QuestionMarkSchema = - | OptionalSchema>, unknown> - | OptionalSchemaAsync< - | BaseSchema> - | BaseSchemaAsync>, - unknown - >; - /** * Infer entries input type. */ @@ -114,7 +134,9 @@ type InferEntriesOutput = { * Optional input keys type. */ type OptionalInputKeys = { - [TKey in keyof TEntries]: TEntries[TKey] extends QuestionMarkSchema + [TKey in keyof TEntries]: TEntries[TKey] extends + | OptionalEntrySchema + | OptionalEntrySchemaAsync ? TKey : never; }[keyof TEntries]; @@ -123,7 +145,9 @@ type OptionalInputKeys = { * Optional output keys type. */ type OptionalOutputKeys = { - [TKey in keyof TEntries]: TEntries[TKey] extends QuestionMarkSchema + [TKey in keyof TEntries]: TEntries[TKey] extends + | OptionalEntrySchema + | OptionalEntrySchemaAsync ? undefined extends TEntries[TKey]['default'] ? TKey : never From e8c4c2f290cc0377c079e8cc0b6218378ce607d1 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 21 Jan 2025 17:22:51 -0500 Subject: [PATCH 7/9] Add additional unit test to config method of library --- library/src/methods/config/config.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/library/src/methods/config/config.test.ts b/library/src/methods/config/config.test.ts index b965045b1..17244179b 100644 --- a/library/src/methods/config/config.test.ts +++ b/library/src/methods/config/config.test.ts @@ -4,6 +4,23 @@ import type { BaseIssue, Config } from '../../types/index.ts'; import { config } from './config.ts'; describe('config', () => { + test('should return copy of passed schema', () => { + expect(config(string(), {})).toStrictEqual({ + kind: 'schema', + type: 'string', + reference: string, + expects: 'string', + async: false, + message: undefined, + '~standard': { + version: 1, + vendor: 'valibot', + validate: expect.any(Function), + }, + '~run': expect.any(Function), + }); + }); + test('should override config of schema', () => { const schema = string(); // @ts-expect-error From 9e4da699238eb7ae2c9744bc0b58e23e165937b1 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 21 Jan 2025 18:54:10 -0500 Subject: [PATCH 8/9] Update docs to reflect changes of PR #1013 --- .../src/routes/api/(actions)/brand/index.mdx | 1 + .../src/routes/api/(actions)/check/index.mdx | 1 + .../api/(actions)/description/index.mdx | 1 + .../routes/api/(actions)/metadata/index.mdx | 1 + .../routes/api/(actions)/rawCheck/index.mdx | 1 + .../api/(actions)/rawTransform/index.mdx | 1 + .../routes/api/(actions)/readonly/index.mdx | 1 + .../routes/api/(actions)/returns/index.mdx | 1 + .../src/routes/api/(actions)/title/index.mdx | 1 + .../routes/api/(actions)/transform/index.mdx | 1 + .../routes/api/(async)/arrayAsync/index.mdx | 2 + .../routes/api/(async)/awaitAsync/index.mdx | 2 + .../routes/api/(async)/checkAsync/index.mdx | 2 + .../routes/api/(async)/customAsync/index.mdx | 2 + .../api/(async)/exactOptionalAsync/index.mdx | 223 ++++++++++++++++++ .../(async)/exactOptionalAsync/properties.ts | 86 +++++++ .../api/(async)/fallbackAsync/index.mdx | 2 + .../api/(async)/getDefaultsAsync/index.mdx | 2 + .../api/(async)/getFallbacksAsync/index.mdx | 2 + .../api/(async)/intersectAsync/index.mdx | 2 + .../routes/api/(async)/lazyAsync/index.mdx | 2 + .../api/(async)/looseObjectAsync/index.mdx | 4 +- .../api/(async)/looseTupleAsync/index.mdx | 2 + .../src/routes/api/(async)/mapAsync/index.mdx | 2 + .../api/(async)/nonNullableAsync/index.mdx | 2 + .../api/(async)/nonNullishAsync/index.mdx | 2 + .../api/(async)/nonOptionalAsync/index.mdx | 2 + .../api/(async)/nullableAsync/index.mdx | 6 +- .../routes/api/(async)/nullishAsync/index.mdx | 6 +- .../routes/api/(async)/objectAsync/index.mdx | 4 +- .../api/(async)/objectWithRestAsync/index.mdx | 4 +- .../api/(async)/optionalAsync/index.mdx | 6 +- .../routes/api/(async)/parseAsync/index.mdx | 2 + .../routes/api/(async)/parserAsync/index.mdx | 2 + .../routes/api/(async)/partialAsync/index.mdx | 2 + .../routes/api/(async)/pipeAsync/index.mdx | 2 + .../api/(async)/rawCheckAsync/index.mdx | 2 + .../api/(async)/rawTransformAsync/index.mdx | 2 + .../routes/api/(async)/recordAsync/index.mdx | 2 + .../api/(async)/requiredAsync/index.mdx | 2 + .../routes/api/(async)/returnsAsync/index.mdx | 2 + .../api/(async)/safeParseAsync/index.mdx | 2 + .../api/(async)/safeParserAsync/index.mdx | 2 + .../src/routes/api/(async)/setAsync/index.mdx | 2 + .../api/(async)/strictObjectAsync/index.mdx | 4 +- .../api/(async)/strictTupleAsync/index.mdx | 2 + .../api/(async)/transformAsync/index.mdx | 2 + .../routes/api/(async)/tupleAsync/index.mdx | 2 + .../api/(async)/tupleWithRestAsync/index.mdx | 2 + .../api/(async)/undefinedableAsync/index.mdx | 6 +- .../routes/api/(async)/unionAsync/index.mdx | 2 + .../src/routes/api/(methods)/assert/index.mdx | 1 + .../src/routes/api/(methods)/config/index.mdx | 2 + .../routes/api/(methods)/fallback/index.mdx | 1 + .../routes/api/(methods)/getDefault/index.mdx | 2 + .../api/(methods)/getDefaults/index.mdx | 1 + .../api/(methods)/getFallback/index.mdx | 2 + .../api/(methods)/getFallbacks/index.mdx | 1 + website/src/routes/api/(methods)/is/index.mdx | 1 + .../src/routes/api/(methods)/keyof/index.mdx | 2 + .../src/routes/api/(methods)/omit/index.mdx | 45 ++++ .../src/routes/api/(methods)/parse/index.mdx | 1 + .../src/routes/api/(methods)/parser/index.mdx | 1 + .../routes/api/(methods)/partial/index.mdx | 1 + .../src/routes/api/(methods)/pick/index.mdx | 45 ++++ .../src/routes/api/(methods)/pipe/index.mdx | 1 + .../routes/api/(methods)/required/index.mdx | 1 + .../routes/api/(methods)/safeParse/index.mdx | 1 + .../routes/api/(methods)/safeParser/index.mdx | 1 + .../src/routes/api/(methods)/unwrap/index.mdx | 16 ++ .../routes/api/(methods)/unwrap/properties.ts | 66 ++++++ .../src/routes/api/(schemas)/any/index.mdx | 1 + .../src/routes/api/(schemas)/array/index.mdx | 1 + .../src/routes/api/(schemas)/bigint/index.mdx | 1 + .../src/routes/api/(schemas)/blob/index.mdx | 1 + .../routes/api/(schemas)/boolean/index.mdx | 1 + .../src/routes/api/(schemas)/custom/index.mdx | 1 + .../src/routes/api/(schemas)/date/index.mdx | 1 + .../src/routes/api/(schemas)/enum/index.mdx | 1 + .../api/(schemas)/exactOptional/index.mdx | 164 +++++++++++++ .../api/(schemas)/exactOptional/properties.ts | 66 ++++++ .../src/routes/api/(schemas)/file/index.mdx | 1 + .../routes/api/(schemas)/function/index.mdx | 1 + .../routes/api/(schemas)/instance/index.mdx | 1 + .../routes/api/(schemas)/intersect/index.mdx | 1 + .../src/routes/api/(schemas)/lazy/index.mdx | 1 + .../routes/api/(schemas)/literal/index.mdx | 1 + .../api/(schemas)/looseObject/index.mdx | 3 +- .../routes/api/(schemas)/looseTuple/index.mdx | 1 + .../src/routes/api/(schemas)/map/index.mdx | 1 + .../src/routes/api/(schemas)/nan/index.mdx | 1 + .../src/routes/api/(schemas)/never/index.mdx | 1 + .../api/(schemas)/nonNullable/index.mdx | 1 + .../routes/api/(schemas)/nonNullish/index.mdx | 1 + .../api/(schemas)/nonOptional/index.mdx | 1 + .../src/routes/api/(schemas)/null/index.mdx | 1 + .../routes/api/(schemas)/nullable/index.mdx | 1 + .../api/(schemas)/nullable/properties.ts | 17 +- .../routes/api/(schemas)/nullish/index.mdx | 1 + .../api/(schemas)/nullish/properties.ts | 20 +- .../src/routes/api/(schemas)/number/index.mdx | 1 + .../src/routes/api/(schemas)/object/index.mdx | 3 +- .../api/(schemas)/objectWithRest/index.mdx | 3 +- .../routes/api/(schemas)/optional/index.mdx | 1 + .../api/(schemas)/optional/properties.ts | 17 +- .../routes/api/(schemas)/picklist/index.mdx | 1 + .../routes/api/(schemas)/promise/index.mdx | 1 + .../src/routes/api/(schemas)/record/index.mdx | 1 + .../src/routes/api/(schemas)/set/index.mdx | 1 + .../api/(schemas)/strictObject/index.mdx | 3 +- .../api/(schemas)/strictTuple/index.mdx | 1 + .../src/routes/api/(schemas)/string/index.mdx | 1 + .../src/routes/api/(schemas)/symbol/index.mdx | 1 + .../src/routes/api/(schemas)/tuple/index.mdx | 1 + .../api/(schemas)/tupleWithRest/index.mdx | 1 + .../routes/api/(schemas)/undefined/index.mdx | 1 + .../api/(schemas)/undefinedable/index.mdx | 1 + .../api/(schemas)/undefinedable/properties.ts | 17 +- .../src/routes/api/(schemas)/union/index.mdx | 1 + .../routes/api/(schemas)/unknown/index.mdx | 1 + .../src/routes/api/(schemas)/void/index.mdx | 1 + .../(types)/ArraySchemaAsync/properties.ts | 19 +- .../(types)/CustomSchemaAsync/properties.ts | 19 +- .../api/(types)/ExactOptionalSchema/index.mdx | 27 +++ .../(types)/ExactOptionalSchema/properties.ts | 118 +++++++++ .../ExactOptionalSchemaAsync/index.mdx | 27 +++ .../ExactOptionalSchemaAsync/properties.ts | 161 +++++++++++++ .../routes/api/(types)/Fallback/properties.ts | 26 +- .../api/(types)/FallbackAsync/properties.ts | 26 +- .../IntersectSchemaAsync/properties.ts | 19 +- .../api/(types)/LazySchemaAsync/properties.ts | 19 +- .../(types)/LooseObjectIssue/properties.ts | 23 +- .../LooseObjectSchemaAsync/properties.ts | 19 +- .../LooseTupleSchemaAsync/properties.ts | 19 +- .../api/(types)/MapSchemaAsync/properties.ts | 19 +- .../NonNullableSchemaAsync/properties.ts | 19 +- .../NonNullishSchemaAsync/properties.ts | 19 +- .../NonOptionalSchemaAsync/properties.ts | 19 +- .../(types)/NullableSchemaAsync/properties.ts | 19 +- .../api/(types)/ObjectIssue/properties.ts | 23 +- .../(types)/ObjectSchemaAsync/properties.ts | 19 +- .../(types)/ObjectWithRestIssue/properties.ts | 23 +- .../(types)/OptionalSchemaAsync/properties.ts | 19 +- .../(types)/RecordSchemaAsync/properties.ts | 19 +- .../api/(types)/SetSchemaAsync/properties.ts | 19 +- .../(types)/StrictObjectIssue/properties.ts | 27 ++- .../StrictObjectSchemaAsync/properties.ts | 19 +- .../StrictTupleSchemaAsync/properties.ts | 19 +- .../TransformActionAsync/properties.ts | 19 +- .../(types)/TupleSchemaAsync/properties.ts | 19 +- .../TupleWithRestSchemaAsync/properties.ts | 19 +- .../UndefinedableSchemaAsync/properties.ts | 19 +- .../(types)/UnionSchemaAsync/properties.ts | 19 +- .../(types)/VariantSchemaAsync/properties.ts | 19 +- .../api/(utils)/entriesFromList/index.mdx | 1 + website/src/routes/api/menu.md | 4 + .../guides/(main-concepts)/schemas/index.mdx | 2 + 157 files changed, 1711 insertions(+), 180 deletions(-) create mode 100644 website/src/routes/api/(async)/exactOptionalAsync/index.mdx create mode 100644 website/src/routes/api/(async)/exactOptionalAsync/properties.ts create mode 100644 website/src/routes/api/(schemas)/exactOptional/index.mdx create mode 100644 website/src/routes/api/(schemas)/exactOptional/properties.ts create mode 100644 website/src/routes/api/(types)/ExactOptionalSchema/index.mdx create mode 100644 website/src/routes/api/(types)/ExactOptionalSchema/properties.ts create mode 100644 website/src/routes/api/(types)/ExactOptionalSchemaAsync/index.mdx create mode 100644 website/src/routes/api/(types)/ExactOptionalSchemaAsync/properties.ts diff --git a/website/src/routes/api/(actions)/brand/index.mdx b/website/src/routes/api/(actions)/brand/index.mdx index e6619bd7d..f615b53a5 100644 --- a/website/src/routes/api/(actions)/brand/index.mdx +++ b/website/src/routes/api/(actions)/brand/index.mdx @@ -70,6 +70,7 @@ The following APIs can be combined with `brand`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/check/index.mdx b/website/src/routes/api/(actions)/check/index.mdx index 22e910ba2..7c6fa832a 100644 --- a/website/src/routes/api/(actions)/check/index.mdx +++ b/website/src/routes/api/(actions)/check/index.mdx @@ -72,6 +72,7 @@ The following APIs can be combined with `check`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/description/index.mdx b/website/src/routes/api/(actions)/description/index.mdx index 546faa802..14de31e03 100644 --- a/website/src/routes/api/(actions)/description/index.mdx +++ b/website/src/routes/api/(actions)/description/index.mdx @@ -69,6 +69,7 @@ The following APIs can be combined with `description`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/metadata/index.mdx b/website/src/routes/api/(actions)/metadata/index.mdx index d21a87513..f682be1b5 100644 --- a/website/src/routes/api/(actions)/metadata/index.mdx +++ b/website/src/routes/api/(actions)/metadata/index.mdx @@ -74,6 +74,7 @@ The following APIs can be combined with `metadata`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/rawCheck/index.mdx b/website/src/routes/api/(actions)/rawCheck/index.mdx index c05691af5..99c9afc0e 100644 --- a/website/src/routes/api/(actions)/rawCheck/index.mdx +++ b/website/src/routes/api/(actions)/rawCheck/index.mdx @@ -95,6 +95,7 @@ The following APIs can be combined with `rawCheck`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/rawTransform/index.mdx b/website/src/routes/api/(actions)/rawTransform/index.mdx index 8716748ba..533b61e98 100644 --- a/website/src/routes/api/(actions)/rawTransform/index.mdx +++ b/website/src/routes/api/(actions)/rawTransform/index.mdx @@ -113,6 +113,7 @@ The following APIs can be combined with `rawTransform`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/readonly/index.mdx b/website/src/routes/api/(actions)/readonly/index.mdx index e3d88b970..71562bef2 100644 --- a/website/src/routes/api/(actions)/readonly/index.mdx +++ b/website/src/routes/api/(actions)/readonly/index.mdx @@ -65,6 +65,7 @@ The following APIs can be combined with `readonly`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/returns/index.mdx b/website/src/routes/api/(actions)/returns/index.mdx index a68c39f68..a11d852a9 100644 --- a/website/src/routes/api/(actions)/returns/index.mdx +++ b/website/src/routes/api/(actions)/returns/index.mdx @@ -66,6 +66,7 @@ The following APIs can be combined with `returns`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/title/index.mdx b/website/src/routes/api/(actions)/title/index.mdx index 4c2ee2e12..a0f8c89f6 100644 --- a/website/src/routes/api/(actions)/title/index.mdx +++ b/website/src/routes/api/(actions)/title/index.mdx @@ -69,6 +69,7 @@ The following APIs can be combined with `title`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(actions)/transform/index.mdx b/website/src/routes/api/(actions)/transform/index.mdx index 76f7870d0..e850e2e68 100644 --- a/website/src/routes/api/(actions)/transform/index.mdx +++ b/website/src/routes/api/(actions)/transform/index.mdx @@ -81,6 +81,7 @@ The following APIs can be combined with `transform`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(async)/arrayAsync/index.mdx b/website/src/routes/api/(async)/arrayAsync/index.mdx index 5a79c656f..139c0f2d7 100644 --- a/website/src/routes/api/(async)/arrayAsync/index.mdx +++ b/website/src/routes/api/(async)/arrayAsync/index.mdx @@ -74,6 +74,7 @@ The following APIs can be combined with `arrayAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -161,6 +162,7 @@ The following APIs can be combined with `arrayAsync`. items={[ 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/awaitAsync/index.mdx b/website/src/routes/api/(async)/awaitAsync/index.mdx index c379c595f..ba75105cb 100644 --- a/website/src/routes/api/(async)/awaitAsync/index.mdx +++ b/website/src/routes/api/(async)/awaitAsync/index.mdx @@ -62,6 +62,7 @@ The following APIs can be combined with `awaitAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -112,6 +113,7 @@ The following APIs can be combined with `awaitAsync`. items={[ 'arrayAsync', 'customAsync', + 'exactOptionalAsync', 'intersectAsync', 'lazyAsync', 'looseObjectAsync', diff --git a/website/src/routes/api/(async)/checkAsync/index.mdx b/website/src/routes/api/(async)/checkAsync/index.mdx index 69dccfcd5..4fcba1536 100644 --- a/website/src/routes/api/(async)/checkAsync/index.mdx +++ b/website/src/routes/api/(async)/checkAsync/index.mdx @@ -75,6 +75,7 @@ The following APIs can be combined with `checkAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -126,6 +127,7 @@ The following APIs can be combined with `checkAsync`. 'arrayAsync', 'awaitAsync', 'customAsync', + 'exactOptionalAsync', 'intersectAsync', 'lazyAsync', 'looseObjectAsync', diff --git a/website/src/routes/api/(async)/customAsync/index.mdx b/website/src/routes/api/(async)/customAsync/index.mdx index 4387360f3..fd2ac8f5d 100644 --- a/website/src/routes/api/(async)/customAsync/index.mdx +++ b/website/src/routes/api/(async)/customAsync/index.mdx @@ -75,6 +75,7 @@ The following APIs can be combined with `customAsync`. (wrapped, default_); +``` + +## Generics + +- `TWrapped` +- `TDefault` + +## Parameters + +- `wrapped` +- `default_` {/* prettier-ignore */} + +### Explanation + +With `exactOptionalAsync` the validation of your schema will pass missing object entries, and if you specify a `default_` input value, the schema will use it if the object entry is missing. For this reason, the output type may differ from the input type of the schema. + +> The difference to `optionalAsync` is that this schema function follows the implementation of TypeScript's [`exactOptionalPropertyTypes` configuration](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes) and only allows missing but not undefined object entries. + +## Returns + +- `Schema` + +## Examples + +The following examples show how `exactOptionalAsync` can be used. + +### New user schema + +Schema to validate new user details. + +```ts +import { isEmailUnique, isUsernameUnique } from '~/api'; + +const NewUserSchema = v.objectAsync({ + email: v.pipeAsync( + v.string(), + v.email(), + v.checkAsync(isEmailUnique, 'The email is not unique.') + ), + username: v.exactOptionalAsync( + v.pipeAsync( + v.string(), + v.nonEmpty(), + v.checkAsync(isUsernameUnique, 'The username is not unique.') + ) + ), + password: v.pipe(v.string(), v.minLength(8)), +}); + +/* + The input and output types of the schema: + { + email: string; + password: string; + username?: string; + } +*/ +``` + +### Unwrap exact optional schema + +Use `unwrap` to undo the effect of `exactOptionalAsync`. + +```ts +import { isUsernameUnique } from '~/api'; + +const UsernameSchema = v.unwrap( + // Assume this schema is from a different file and is reused here + v.exactOptionalAsync( + v.pipeAsync( + v.string(), + v.nonEmpty(), + v.checkAsync(isUsernameUnique, 'The username is not unique.') + ) + ) +); +``` + +## Related + +The following APIs can be combined with `exactOptionalAsync`. + +### Schemas + + + +### Methods + + + +### Actions + + + +### Utils + + + +### Async + + diff --git a/website/src/routes/api/(async)/exactOptionalAsync/properties.ts b/website/src/routes/api/(async)/exactOptionalAsync/properties.ts new file mode 100644 index 000000000..d6ca7286e --- /dev/null +++ b/website/src/routes/api/(async)/exactOptionalAsync/properties.ts @@ -0,0 +1,86 @@ +import type { PropertyProps } from '~/components'; + +export const properties: Record = { + TWrapped: { + modifier: 'extends', + type: { + type: 'union', + options: [ + { + type: 'custom', + name: 'BaseSchema', + href: '../BaseSchema/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + { + type: 'custom', + name: 'BaseSchemaAsync', + href: '../BaseSchemaAsync/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + ], + }, + }, + TDefault: { + modifier: 'extends', + type: { + type: 'custom', + name: 'DefaultAsync', + href: '../DefaultAsync/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + 'never', + ], + }, + }, + wrapped: { + type: { + type: 'custom', + name: 'TWrapped', + }, + }, + default_: { + type: { + type: 'custom', + name: 'TDefault', + }, + }, + Schema: { + type: { + type: 'custom', + name: 'ExactOptionalSchemaAsync', + href: '../ExactOptionalSchemaAsync/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + { + type: 'custom', + name: 'TDefault', + }, + ], + }, + }, +}; diff --git a/website/src/routes/api/(async)/fallbackAsync/index.mdx b/website/src/routes/api/(async)/fallbackAsync/index.mdx index 5ca1bcb02..1bb850ea5 100644 --- a/website/src/routes/api/(async)/fallbackAsync/index.mdx +++ b/website/src/routes/api/(async)/fallbackAsync/index.mdx @@ -76,6 +76,7 @@ The following APIs can be combined with `fallbackAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -242,6 +243,7 @@ The following APIs can be combined with `fallbackAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'getDefaultsAsync', 'getFallbacksAsync', 'intersectAsync', diff --git a/website/src/routes/api/(async)/getDefaultsAsync/index.mdx b/website/src/routes/api/(async)/getDefaultsAsync/index.mdx index 72e717146..3b0878280 100644 --- a/website/src/routes/api/(async)/getDefaultsAsync/index.mdx +++ b/website/src/routes/api/(async)/getDefaultsAsync/index.mdx @@ -79,6 +79,7 @@ The following APIs can be combined with `getDefaultsAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -142,6 +143,7 @@ The following APIs can be combined with `getDefaultsAsync`. items={[ 'arrayAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'intersectAsync', 'lazyAsync', diff --git a/website/src/routes/api/(async)/getFallbacksAsync/index.mdx b/website/src/routes/api/(async)/getFallbacksAsync/index.mdx index 2ae6877eb..8c0136b4f 100644 --- a/website/src/routes/api/(async)/getFallbacksAsync/index.mdx +++ b/website/src/routes/api/(async)/getFallbacksAsync/index.mdx @@ -77,6 +77,7 @@ The following APIs can be combined with `getFallbacksAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -140,6 +141,7 @@ The following APIs can be combined with `getFallbacksAsync`. items={[ 'arrayAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'intersectAsync', 'lazyAsync', diff --git a/website/src/routes/api/(async)/intersectAsync/index.mdx b/website/src/routes/api/(async)/intersectAsync/index.mdx index ae85462e2..57d262a63 100644 --- a/website/src/routes/api/(async)/intersectAsync/index.mdx +++ b/website/src/routes/api/(async)/intersectAsync/index.mdx @@ -89,6 +89,7 @@ The following APIs can be combined with `intersectAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -245,6 +246,7 @@ The following APIs can be combined with `intersectAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/lazyAsync/index.mdx b/website/src/routes/api/(async)/lazyAsync/index.mdx index ca8bb3627..e980b1dd9 100644 --- a/website/src/routes/api/(async)/lazyAsync/index.mdx +++ b/website/src/routes/api/(async)/lazyAsync/index.mdx @@ -121,6 +121,7 @@ The following APIs can be combined with `lazyAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -277,6 +278,7 @@ The following APIs can be combined with `lazyAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/looseObjectAsync/index.mdx b/website/src/routes/api/(async)/looseObjectAsync/index.mdx index 9609a63a8..40bf79671 100644 --- a/website/src/routes/api/(async)/looseObjectAsync/index.mdx +++ b/website/src/routes/api/(async)/looseObjectAsync/index.mdx @@ -35,8 +35,6 @@ With `looseObjectAsync` you can validate the data type of the input and whether > The difference to `objectAsync` is that this schema includes any unknown entries in the output. In addition, this schema filters certain entries from the unknown entries for security reasons. -> This schema does not distinguish between missing and `undefined` entries. The reason for this decision is that it reduces the bundle size, and we also expect that most users will expect this behavior. - ## Returns - `Schema` @@ -81,6 +79,7 @@ The following APIs can be combined with `looseObjectAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -153,6 +152,7 @@ The following APIs can be combined with `looseObjectAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'forwardAsync', 'getDefaultsAsync', diff --git a/website/src/routes/api/(async)/looseTupleAsync/index.mdx b/website/src/routes/api/(async)/looseTupleAsync/index.mdx index 5f657f243..2f3e33d37 100644 --- a/website/src/routes/api/(async)/looseTupleAsync/index.mdx +++ b/website/src/routes/api/(async)/looseTupleAsync/index.mdx @@ -76,6 +76,7 @@ The following APIs can be combined with `looseTupleAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -163,6 +164,7 @@ The following APIs can be combined with `looseTupleAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/mapAsync/index.mdx b/website/src/routes/api/(async)/mapAsync/index.mdx index ed35018a1..b01ff3b25 100644 --- a/website/src/routes/api/(async)/mapAsync/index.mdx +++ b/website/src/routes/api/(async)/mapAsync/index.mdx @@ -74,6 +74,7 @@ The following APIs can be combined with `mapAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -148,6 +149,7 @@ The following APIs can be combined with `mapAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/nonNullableAsync/index.mdx b/website/src/routes/api/(async)/nonNullableAsync/index.mdx index d86c1eb28..ba3263f07 100644 --- a/website/src/routes/api/(async)/nonNullableAsync/index.mdx +++ b/website/src/routes/api/(async)/nonNullableAsync/index.mdx @@ -78,6 +78,7 @@ The following APIs can be combined with `nonNullableAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -151,6 +152,7 @@ The following APIs can be combined with `nonNullableAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/nonNullishAsync/index.mdx b/website/src/routes/api/(async)/nonNullishAsync/index.mdx index a4bfc85e0..9f2f3f6ec 100644 --- a/website/src/routes/api/(async)/nonNullishAsync/index.mdx +++ b/website/src/routes/api/(async)/nonNullishAsync/index.mdx @@ -74,6 +74,7 @@ The following APIs can be combined with `nonNullishAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -147,6 +148,7 @@ The following APIs can be combined with `nonNullishAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/nonOptionalAsync/index.mdx b/website/src/routes/api/(async)/nonOptionalAsync/index.mdx index b2a395b81..76320f8ff 100644 --- a/website/src/routes/api/(async)/nonOptionalAsync/index.mdx +++ b/website/src/routes/api/(async)/nonOptionalAsync/index.mdx @@ -84,6 +84,7 @@ The following APIs can be combined with `nonOptionalAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -157,6 +158,7 @@ The following APIs can be combined with `nonOptionalAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/nullableAsync/index.mdx b/website/src/routes/api/(async)/nullableAsync/index.mdx index 64381db91..600e83ed8 100644 --- a/website/src/routes/api/(async)/nullableAsync/index.mdx +++ b/website/src/routes/api/(async)/nullableAsync/index.mdx @@ -63,9 +63,9 @@ const NullableUsernameSchema = v.nullableAsync( ); ``` -### Username schema +### Unwrap nullable schema -Schema that accepts a unique username. +Use `unwrap` to undo the effect of `nullableAsync`. ```ts import { isUsernameUnique } from '~/api'; @@ -98,6 +98,7 @@ The following APIs can be combined with `nullableAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -171,6 +172,7 @@ The following APIs can be combined with `nullableAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/nullishAsync/index.mdx b/website/src/routes/api/(async)/nullishAsync/index.mdx index 2c6bf08c4..3a55d7a5c 100644 --- a/website/src/routes/api/(async)/nullishAsync/index.mdx +++ b/website/src/routes/api/(async)/nullishAsync/index.mdx @@ -96,9 +96,9 @@ const NewUserSchema = v.objectAsync({ */ ``` -### Username schema +### Unwrap nullish schema -Schema that accepts a unique username. +Use `unwrap` to undo the effect of `nullishAsync`. ```ts import { isUsernameUnique } from '~/api'; @@ -131,6 +131,7 @@ The following APIs can be combined with `nullishAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -204,6 +205,7 @@ The following APIs can be combined with `nullishAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/objectAsync/index.mdx b/website/src/routes/api/(async)/objectAsync/index.mdx index dcb758750..04934f03c 100644 --- a/website/src/routes/api/(async)/objectAsync/index.mdx +++ b/website/src/routes/api/(async)/objectAsync/index.mdx @@ -35,8 +35,6 @@ With `objectAsync` you can validate the data type of the input and whether the c > This schema removes unknown entries. The output will only include the entries you specify. To include unknown entries, use `looseObjectAsync`. To return an issue for unknown entries, use `strictObjectAsync`. To include and validate unknown entries, use `objectWithRestAsync`. -> This schema does not distinguish between missing and `undefined` entries. The reason for this decision is that it reduces the bundle size, and we also expect that most users will expect this behavior. - ## Returns - `Schema` @@ -81,6 +79,7 @@ The following APIs can be combined with `objectAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -155,6 +154,7 @@ The following APIs can be combined with `objectAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'forwardAsync', 'getDefaultsAsync', diff --git a/website/src/routes/api/(async)/objectWithRestAsync/index.mdx b/website/src/routes/api/(async)/objectWithRestAsync/index.mdx index e2955c9c3..0bd95546f 100644 --- a/website/src/routes/api/(async)/objectWithRestAsync/index.mdx +++ b/website/src/routes/api/(async)/objectWithRestAsync/index.mdx @@ -41,8 +41,6 @@ With `objectWithRestAsync` you can validate the data type of the input and wheth > The difference to `objectAsync` is that this schema includes unknown entries in the output. In addition, this schema filters certain entries from the unknown entries for security reasons. -> This schema does not distinguish between missing and `undefined` entries. The reason for this decision is that it reduces the bundle size, and we also expect that most users will expect this behavior. - ## Returns - `Schema` @@ -95,6 +93,7 @@ The following APIs can be combined with `objectWithRestAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -169,6 +168,7 @@ The following APIs can be combined with `objectWithRestAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'forwardAsync', 'getDefaultsAsync', diff --git a/website/src/routes/api/(async)/optionalAsync/index.mdx b/website/src/routes/api/(async)/optionalAsync/index.mdx index aa15c2e52..8a0c6f813 100644 --- a/website/src/routes/api/(async)/optionalAsync/index.mdx +++ b/website/src/routes/api/(async)/optionalAsync/index.mdx @@ -96,9 +96,9 @@ const NewUserSchema = v.objectAsync({ */ ``` -### Username schema +### Unwrap optional schema -Schema that accepts a unique username. +Use `unwrap` to undo the effect of `optionalAsync`. ```ts import { isUsernameUnique } from '~/api'; @@ -131,6 +131,7 @@ The following APIs can be combined with `optionalAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -204,6 +205,7 @@ The following APIs can be combined with `optionalAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/parseAsync/index.mdx b/website/src/routes/api/(async)/parseAsync/index.mdx index c0dee515c..47f049d41 100644 --- a/website/src/routes/api/(async)/parseAsync/index.mdx +++ b/website/src/routes/api/(async)/parseAsync/index.mdx @@ -76,6 +76,7 @@ The following APIs can be combined with `parseAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -144,6 +145,7 @@ The following APIs can be combined with `parseAsync`. items={[ 'arrayAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'intersectAsync', 'lazyAsync', diff --git a/website/src/routes/api/(async)/parserAsync/index.mdx b/website/src/routes/api/(async)/parserAsync/index.mdx index 075f82e0e..5ad7c8d9e 100644 --- a/website/src/routes/api/(async)/parserAsync/index.mdx +++ b/website/src/routes/api/(async)/parserAsync/index.mdx @@ -70,6 +70,7 @@ The following APIs can be combined with `parserAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -138,6 +139,7 @@ The following APIs can be combined with `parserAsync`. items={[ 'arrayAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'intersectAsync', 'lazyAsync', diff --git a/website/src/routes/api/(async)/partialAsync/index.mdx b/website/src/routes/api/(async)/partialAsync/index.mdx index 671c2d878..43ef103fe 100644 --- a/website/src/routes/api/(async)/partialAsync/index.mdx +++ b/website/src/routes/api/(async)/partialAsync/index.mdx @@ -86,6 +86,7 @@ The following APIs can be combined with `partialAsync`. The difference to `objectAsync` is that this schema returns an issue for unknown entries. It intentionally returns only one issue. Otherwise, attackers could send large objects to exhaust device resources. If you want an issue for every unknown key, use the `objectWithRestAsync` schema with `never` for the `rest` argument. -> This schema does not distinguish between missing and `undefined` entries. The reason for this decision is that it reduces the bundle size, and we also expect that most users will expect this behavior. - ## Returns - `Schema` @@ -81,6 +79,7 @@ The following APIs can be combined with `strictObjectAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -155,6 +154,7 @@ The following APIs can be combined with `strictObjectAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'forwardAsync', 'getDefaultsAsync', diff --git a/website/src/routes/api/(async)/strictTupleAsync/index.mdx b/website/src/routes/api/(async)/strictTupleAsync/index.mdx index 0246db8fd..44a7052bc 100644 --- a/website/src/routes/api/(async)/strictTupleAsync/index.mdx +++ b/website/src/routes/api/(async)/strictTupleAsync/index.mdx @@ -76,6 +76,7 @@ The following APIs can be combined with `strictTupleAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -163,6 +164,7 @@ The following APIs can be combined with `strictTupleAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/transformAsync/index.mdx b/website/src/routes/api/(async)/transformAsync/index.mdx index 098114af2..2348bbf7c 100644 --- a/website/src/routes/api/(async)/transformAsync/index.mdx +++ b/website/src/routes/api/(async)/transformAsync/index.mdx @@ -68,6 +68,7 @@ The following APIs can be combined with `transformAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -119,6 +120,7 @@ The following APIs can be combined with `transformAsync`. 'arrayAsync', 'awaitAsync', 'customAsync', + 'exactOptionalAsync', 'intersectAsync', 'lazyAsync', 'looseObjectAsync', diff --git a/website/src/routes/api/(async)/tupleAsync/index.mdx b/website/src/routes/api/(async)/tupleAsync/index.mdx index 5afa14944..5eb015370 100644 --- a/website/src/routes/api/(async)/tupleAsync/index.mdx +++ b/website/src/routes/api/(async)/tupleAsync/index.mdx @@ -76,6 +76,7 @@ The following APIs can be combined with `tupleAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -163,6 +164,7 @@ The following APIs can be combined with `tupleAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/tupleWithRestAsync/index.mdx b/website/src/routes/api/(async)/tupleWithRestAsync/index.mdx index 9c98f8472..684c1e531 100644 --- a/website/src/routes/api/(async)/tupleWithRestAsync/index.mdx +++ b/website/src/routes/api/(async)/tupleWithRestAsync/index.mdx @@ -83,6 +83,7 @@ The following APIs can be combined with `tupleWithRestAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -170,6 +171,7 @@ The following APIs can be combined with `tupleWithRestAsync`. 'arrayAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/undefinedableAsync/index.mdx b/website/src/routes/api/(async)/undefinedableAsync/index.mdx index 5562046f3..05b53dbbc 100644 --- a/website/src/routes/api/(async)/undefinedableAsync/index.mdx +++ b/website/src/routes/api/(async)/undefinedableAsync/index.mdx @@ -98,9 +98,9 @@ const NewUserSchema = v.objectAsync({ */ ``` -### Username schema +### Unwrap undefinedable schema -Schema that accepts a unique username. +Use `unwrap` to undo the effect of `undefinedableAsync`. ```ts import { isUsernameUnique } from '~/api'; @@ -133,6 +133,7 @@ The following APIs can be combined with `undefinedableAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -206,6 +207,7 @@ The following APIs can be combined with `undefinedableAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(async)/unionAsync/index.mdx b/website/src/routes/api/(async)/unionAsync/index.mdx index 766b54247..3ceacb5de 100644 --- a/website/src/routes/api/(async)/unionAsync/index.mdx +++ b/website/src/routes/api/(async)/unionAsync/index.mdx @@ -82,6 +82,7 @@ The following APIs can be combined with `unionAsync`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -237,6 +238,7 @@ The following APIs can be combined with `unionAsync`. 'awaitAsync', 'checkAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'getDefaultsAsync', 'getFallbacksAsync', diff --git a/website/src/routes/api/(methods)/assert/index.mdx b/website/src/routes/api/(methods)/assert/index.mdx index 1d44d3a55..427f6e162 100644 --- a/website/src/routes/api/(methods)/assert/index.mdx +++ b/website/src/routes/api/(methods)/assert/index.mdx @@ -56,6 +56,7 @@ The following APIs can be combined with `assert`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(methods)/config/index.mdx b/website/src/routes/api/(methods)/config/index.mdx index a6cacf6d5..c19a1cf0f 100644 --- a/website/src/routes/api/(methods)/config/index.mdx +++ b/website/src/routes/api/(methods)/config/index.mdx @@ -82,6 +82,7 @@ The following APIs can be combined with `config`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -257,6 +258,7 @@ The following APIs can be combined with `config`. items={[ 'arrayAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'intersectAsync', 'lazyAsync', diff --git a/website/src/routes/api/(methods)/fallback/index.mdx b/website/src/routes/api/(methods)/fallback/index.mdx index 9e1a5962e..65340e36b 100644 --- a/website/src/routes/api/(methods)/fallback/index.mdx +++ b/website/src/routes/api/(methods)/fallback/index.mdx @@ -81,6 +81,7 @@ The following APIs can be combined with `fallback`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(methods)/getDefault/index.mdx b/website/src/routes/api/(methods)/getDefault/index.mdx index acde35d52..074f153a9 100644 --- a/website/src/routes/api/(methods)/getDefault/index.mdx +++ b/website/src/routes/api/(methods)/getDefault/index.mdx @@ -60,6 +60,7 @@ The following APIs can be combined with `getDefault`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -123,6 +124,7 @@ The following APIs can be combined with `getDefault`. items={[ 'arrayAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'intersectAsync', 'lazyAsync', diff --git a/website/src/routes/api/(methods)/getDefaults/index.mdx b/website/src/routes/api/(methods)/getDefaults/index.mdx index 533b3c2f9..ad660d63a 100644 --- a/website/src/routes/api/(methods)/getDefaults/index.mdx +++ b/website/src/routes/api/(methods)/getDefaults/index.mdx @@ -74,6 +74,7 @@ The following APIs can be combined with `getDefaults`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(methods)/getFallback/index.mdx b/website/src/routes/api/(methods)/getFallback/index.mdx index 386db924a..df4708e75 100644 --- a/website/src/routes/api/(methods)/getFallback/index.mdx +++ b/website/src/routes/api/(methods)/getFallback/index.mdx @@ -60,6 +60,7 @@ The following APIs can be combined with `getFallback`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', @@ -123,6 +124,7 @@ The following APIs can be combined with `getFallback`. items={[ 'arrayAsync', 'customAsync', + 'exactOptionalAsync', 'fallbackAsync', 'intersectAsync', 'lazyAsync', diff --git a/website/src/routes/api/(methods)/getFallbacks/index.mdx b/website/src/routes/api/(methods)/getFallbacks/index.mdx index 83753575f..4c306d852 100644 --- a/website/src/routes/api/(methods)/getFallbacks/index.mdx +++ b/website/src/routes/api/(methods)/getFallbacks/index.mdx @@ -74,6 +74,7 @@ The following APIs can be combined with `getFallbacks`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(methods)/is/index.mdx b/website/src/routes/api/(methods)/is/index.mdx index a655df477..1e65f1067 100644 --- a/website/src/routes/api/(methods)/is/index.mdx +++ b/website/src/routes/api/(methods)/is/index.mdx @@ -61,6 +61,7 @@ The following APIs can be combined with `is`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(methods)/keyof/index.mdx b/website/src/routes/api/(methods)/keyof/index.mdx index 6e0354664..62d77e9f2 100644 --- a/website/src/routes/api/(methods)/keyof/index.mdx +++ b/website/src/routes/api/(methods)/keyof/index.mdx @@ -53,6 +53,7 @@ The following APIs can be combined with `keyof`. + +### Async + + diff --git a/website/src/routes/api/(methods)/parse/index.mdx b/website/src/routes/api/(methods)/parse/index.mdx index 214f19369..5aba8cc25 100644 --- a/website/src/routes/api/(methods)/parse/index.mdx +++ b/website/src/routes/api/(methods)/parse/index.mdx @@ -67,6 +67,7 @@ The following APIs can be combined with `parse`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(methods)/parser/index.mdx b/website/src/routes/api/(methods)/parser/index.mdx index 2335bc673..8356d378b 100644 --- a/website/src/routes/api/(methods)/parser/index.mdx +++ b/website/src/routes/api/(methods)/parser/index.mdx @@ -63,6 +63,7 @@ The following APIs can be combined with `parser`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(methods)/partial/index.mdx b/website/src/routes/api/(methods)/partial/index.mdx index c2c023492..483ca4535 100644 --- a/website/src/routes/api/(methods)/partial/index.mdx +++ b/website/src/routes/api/(methods)/partial/index.mdx @@ -81,6 +81,7 @@ The following APIs can be combined with `partial`. + +### Async + + diff --git a/website/src/routes/api/(methods)/pipe/index.mdx b/website/src/routes/api/(methods)/pipe/index.mdx index b9577b9aa..48e675999 100644 --- a/website/src/routes/api/(methods)/pipe/index.mdx +++ b/website/src/routes/api/(methods)/pipe/index.mdx @@ -84,6 +84,7 @@ The following APIs can be combined with `pipe`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(methods)/required/index.mdx b/website/src/routes/api/(methods)/required/index.mdx index 877e60921..02ea181b0 100644 --- a/website/src/routes/api/(methods)/required/index.mdx +++ b/website/src/routes/api/(methods)/required/index.mdx @@ -90,6 +90,7 @@ The following APIs can be combined with `required`. + +### Async + + diff --git a/website/src/routes/api/(methods)/unwrap/properties.ts b/website/src/routes/api/(methods)/unwrap/properties.ts index 0459ca2d8..8033311d8 100644 --- a/website/src/routes/api/(methods)/unwrap/properties.ts +++ b/website/src/routes/api/(methods)/unwrap/properties.ts @@ -6,6 +6,72 @@ export const properties: Record = { type: { type: 'union', options: [ + { + type: 'custom', + name: 'ExactOptionalSchema', + href: '../ExactOptionalSchema/', + generics: [ + { + type: 'custom', + name: 'BaseSchema', + href: '../BaseSchema/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + 'unknown', + ], + }, + { + type: 'custom', + name: 'ExactOptionalSchemaAsync', + href: '../ExactOptionalSchemaAsync/', + generics: [ + { + type: 'union', + options: [ + { + type: 'custom', + name: 'BaseSchema', + href: '../BaseSchema/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + { + type: 'custom', + name: 'BaseSchemaAsync', + href: '../BaseSchemaAsync/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + ], + }, + 'unknown', + ], + }, { type: 'custom', name: 'NonNullableSchema', diff --git a/website/src/routes/api/(schemas)/any/index.mdx b/website/src/routes/api/(schemas)/any/index.mdx index baa628c2d..780c16955 100644 --- a/website/src/routes/api/(schemas)/any/index.mdx +++ b/website/src/routes/api/(schemas)/any/index.mdx @@ -35,6 +35,7 @@ The following APIs can be combined with `any`. (wrapped, default_); +``` + +## Generics + +- `TWrapped` +- `TDefault` + +## Parameters + +- `wrapped` +- `default_` {/* prettier-ignore */} + +### Explanation + +With `exactOptional` the validation of your schema will pass missing object entries, and if you specify a `default_` input value, the schema will use it if the object entry is missing. For this reason, the output type may differ from the input type of the schema. + +> The difference to `optional` is that this schema function follows the implementation of TypeScript's [`exactOptionalPropertyTypes` configuration](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes) and only allows missing but not undefined object entries. + +## Returns + +- `Schema` + +## Examples + +The following examples show how `exactOptional` can be used. + +### Exact optional object entries + +Object schema with exact optional entries. + +> By using a function as the `default_` parameter, the schema will return a new [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) instance each time the input is `undefined`. + +```ts +const OptionalEntrySchema = v.object({ + key1: v.exactOptional(v.string()), + key2: v.exactOptional(v.string(), "I'm the default!"), + key3: v.exactOptional(v.date(), () => new Date()), +}); +``` + +### Unwrap exact optional schema + +Use `unwrap` to undo the effect of `exactOptional`. + +```ts +const OptionalNumberSchema = v.exactOptional(v.number()); +const NumberSchema = v.unwrap(OptionalNumberSchema); +``` + +## Related + +The following APIs can be combined with `exactOptional`. + +### Schemas + + + +### Methods + + + +### Actions + + + +### Utils + + diff --git a/website/src/routes/api/(schemas)/exactOptional/properties.ts b/website/src/routes/api/(schemas)/exactOptional/properties.ts new file mode 100644 index 000000000..51fae0651 --- /dev/null +++ b/website/src/routes/api/(schemas)/exactOptional/properties.ts @@ -0,0 +1,66 @@ +import type { PropertyProps } from '~/components'; + +export const properties: Record = { + TWrapped: { + modifier: 'extends', + type: { + type: 'custom', + name: 'BaseSchema', + href: '../BaseSchema/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + }, + TDefault: { + modifier: 'extends', + type: { + type: 'custom', + name: 'Default', + href: '../Default/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + 'never', + ], + }, + }, + wrapped: { + type: { + type: 'custom', + name: 'TWrapped', + }, + }, + default_: { + type: { + type: 'custom', + name: 'TDefault', + }, + }, + Schema: { + type: { + type: 'custom', + name: 'ExactOptionalSchema', + href: '../ExactOptionalSchema/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + { + type: 'custom', + name: 'TDefault', + }, + ], + }, + }, +}; diff --git a/website/src/routes/api/(schemas)/file/index.mdx b/website/src/routes/api/(schemas)/file/index.mdx index 49603a0bb..f5ccb4b43 100644 --- a/website/src/routes/api/(schemas)/file/index.mdx +++ b/website/src/routes/api/(schemas)/file/index.mdx @@ -60,6 +60,7 @@ The following APIs can be combined with `file`. The difference to `object` is that this schema includes any unknown entries in the output. In addition, this schema filters certain entries from the unknown entries for security reasons. -> This schema does not distinguish between missing and `undefined` entries. The reason for this decision is that it reduces the bundle size, and we also expect that most users will expect this behavior. - ## Returns - `Schema` @@ -111,6 +109,7 @@ The following APIs can be combined with `looseObject`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/looseTuple/index.mdx b/website/src/routes/api/(schemas)/looseTuple/index.mdx index 661cc1d31..170656784 100644 --- a/website/src/routes/api/(schemas)/looseTuple/index.mdx +++ b/website/src/routes/api/(schemas)/looseTuple/index.mdx @@ -66,6 +66,7 @@ The following APIs can be combined with `looseTuple`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/map/index.mdx b/website/src/routes/api/(schemas)/map/index.mdx index e97d71218..ca585e3c5 100644 --- a/website/src/routes/api/(schemas)/map/index.mdx +++ b/website/src/routes/api/(schemas)/map/index.mdx @@ -73,6 +73,7 @@ The following APIs can be combined with `map`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/nan/index.mdx b/website/src/routes/api/(schemas)/nan/index.mdx index 9a6c2e035..7ca925e7e 100644 --- a/website/src/routes/api/(schemas)/nan/index.mdx +++ b/website/src/routes/api/(schemas)/nan/index.mdx @@ -42,6 +42,7 @@ The following APIs can be combined with `nan`. = { TDefault: { modifier: 'extends', type: { - type: 'union', - options: [ + type: 'custom', + name: 'Default', + href: '../Default/', + generics: [ { type: 'custom', - name: 'Default', - href: '../Default/', - generics: [ - { - type: 'custom', - name: 'TWrapped', - }, - ], + name: 'TWrapped', }, - 'never', + 'null', ], }, }, diff --git a/website/src/routes/api/(schemas)/nullish/index.mdx b/website/src/routes/api/(schemas)/nullish/index.mdx index e42116616..d740184d9 100644 --- a/website/src/routes/api/(schemas)/nullish/index.mdx +++ b/website/src/routes/api/(schemas)/nullish/index.mdx @@ -96,6 +96,7 @@ The following APIs can be combined with `nullish`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/nullish/properties.ts b/website/src/routes/api/(schemas)/nullish/properties.ts index cf074821e..bb8fbbec1 100644 --- a/website/src/routes/api/(schemas)/nullish/properties.ts +++ b/website/src/routes/api/(schemas)/nullish/properties.ts @@ -22,20 +22,18 @@ export const properties: Record = { TDefault: { modifier: 'extends', type: { - type: 'union', - options: [ + type: 'custom', + name: 'Default', + href: '../Default/', + generics: [ { type: 'custom', - name: 'Default', - href: '../Default/', - generics: [ - { - type: 'custom', - name: 'TWrapped', - }, - ], + name: 'TWrapped', + }, + { + type: 'union', + options: ['null', 'undefined'], }, - 'never', ], }, }, diff --git a/website/src/routes/api/(schemas)/number/index.mdx b/website/src/routes/api/(schemas)/number/index.mdx index 817a1d8fc..d12a97629 100644 --- a/website/src/routes/api/(schemas)/number/index.mdx +++ b/website/src/routes/api/(schemas)/number/index.mdx @@ -70,6 +70,7 @@ The following APIs can be combined with `number`. This schema removes unknown entries. The output will only include the entries you specify. To include unknown entries, use `looseObject`. To return an issue for unknown entries, use `strictObject`. To include and validate unknown entries, use `objectWithRest`. -> This schema does not distinguish between missing and `undefined` entries. The reason for this decision is that it reduces the bundle size, and we also expect that most users will expect this behavior. - ## Returns - `Schema` @@ -111,6 +109,7 @@ The following APIs can be combined with `object`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/objectWithRest/index.mdx b/website/src/routes/api/(schemas)/objectWithRest/index.mdx index ab6852a73..b3af0b458 100644 --- a/website/src/routes/api/(schemas)/objectWithRest/index.mdx +++ b/website/src/routes/api/(schemas)/objectWithRest/index.mdx @@ -41,8 +41,6 @@ With `objectWithRest` you can validate the data type of the input and whether th > The difference to `object` is that this schema includes unknown entries in the output. In addition, this schema filters certain entries from the unknown entries for security reasons. -> This schema does not distinguish between missing and `undefined` entries. The reason for this decision is that it reduces the bundle size, and we also expect that most users will expect this behavior. - ## Returns - `Schema` @@ -129,6 +127,7 @@ The following APIs can be combined with `objectWithRest`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/optional/index.mdx b/website/src/routes/api/(schemas)/optional/index.mdx index a25cb980c..d8cdd93c0 100644 --- a/website/src/routes/api/(schemas)/optional/index.mdx +++ b/website/src/routes/api/(schemas)/optional/index.mdx @@ -96,6 +96,7 @@ The following APIs can be combined with `optional`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/optional/properties.ts b/website/src/routes/api/(schemas)/optional/properties.ts index 1367a55e8..6e7ac7e4c 100644 --- a/website/src/routes/api/(schemas)/optional/properties.ts +++ b/website/src/routes/api/(schemas)/optional/properties.ts @@ -22,20 +22,15 @@ export const properties: Record = { TDefault: { modifier: 'extends', type: { - type: 'union', - options: [ + type: 'custom', + name: 'Default', + href: '../Default/', + generics: [ { type: 'custom', - name: 'Default', - href: '../Default/', - generics: [ - { - type: 'custom', - name: 'TWrapped', - }, - ], + name: 'TWrapped', }, - 'never', + 'undefined', ], }, }, diff --git a/website/src/routes/api/(schemas)/picklist/index.mdx b/website/src/routes/api/(schemas)/picklist/index.mdx index c429e45fb..7e4dfce5b 100644 --- a/website/src/routes/api/(schemas)/picklist/index.mdx +++ b/website/src/routes/api/(schemas)/picklist/index.mdx @@ -78,6 +78,7 @@ The following APIs can be combined with `picklist`. The difference to `object` is that this schema returns an issue for unknown entries. It intentionally returns only one issue. Otherwise, attackers could send large objects to exhaust device resources. If you want an issue for every unknown key, use the `objectWithRest` schema with `never` for the `rest` argument. -> This schema does not distinguish between missing and `undefined` entries. The reason for this decision is that it reduces the bundle size, and we also expect that most users will expect this behavior. - ## Returns - `Schema` @@ -111,6 +109,7 @@ The following APIs can be combined with `strictObject`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/strictTuple/index.mdx b/website/src/routes/api/(schemas)/strictTuple/index.mdx index 6995f85ba..c1eb10281 100644 --- a/website/src/routes/api/(schemas)/strictTuple/index.mdx +++ b/website/src/routes/api/(schemas)/strictTuple/index.mdx @@ -66,6 +66,7 @@ The following APIs can be combined with `strictTuple`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/string/index.mdx b/website/src/routes/api/(schemas)/string/index.mdx index 76a23cbb6..49de9bfa3 100644 --- a/website/src/routes/api/(schemas)/string/index.mdx +++ b/website/src/routes/api/(schemas)/string/index.mdx @@ -88,6 +88,7 @@ The following APIs can be combined with `string`. = { TDefault: { modifier: 'extends', type: { - type: 'union', - options: [ + type: 'custom', + name: 'Default', + href: '../Default/', + generics: [ { type: 'custom', - name: 'Default', - href: '../Default/', - generics: [ - { - type: 'custom', - name: 'TWrapped', - }, - ], + name: 'TWrapped', }, - 'never', + 'undefined', ], }, }, diff --git a/website/src/routes/api/(schemas)/union/index.mdx b/website/src/routes/api/(schemas)/union/index.mdx index 75a6a744e..8b8a0d97b 100644 --- a/website/src/routes/api/(schemas)/union/index.mdx +++ b/website/src/routes/api/(schemas)/union/index.mdx @@ -86,6 +86,7 @@ The following APIs can be combined with `union`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/(schemas)/unknown/index.mdx b/website/src/routes/api/(schemas)/unknown/index.mdx index 42e247188..7ae9bc982 100644 --- a/website/src/routes/api/(schemas)/unknown/index.mdx +++ b/website/src/routes/api/(schemas)/unknown/index.mdx @@ -35,6 +35,7 @@ The following APIs can be combined with `unknown`. = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'arrayAsync', - href: '../arrayAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'array', + href: '../array/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'arrayAsync', + href: '../arrayAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/CustomSchemaAsync/properties.ts b/website/src/routes/api/(types)/CustomSchemaAsync/properties.ts index 944744321..edc615e7c 100644 --- a/website/src/routes/api/(types)/CustomSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/CustomSchemaAsync/properties.ts @@ -57,10 +57,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'customAsync', - href: '../customAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'custom', + href: '../custom/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'customAsync', + href: '../customAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/ExactOptionalSchema/index.mdx b/website/src/routes/api/(types)/ExactOptionalSchema/index.mdx new file mode 100644 index 000000000..3175a03df --- /dev/null +++ b/website/src/routes/api/(types)/ExactOptionalSchema/index.mdx @@ -0,0 +1,27 @@ +--- +title: ExactOptionalSchema +description: Exact optional schema interface. +contributors: + - fabian-hiller +--- + +import { Property } from '~/components'; +import { properties } from './properties'; + +# ExactOptionalSchema + +Exact optional schema interface. + +## Generics + +- `TWrapped` +- `TDefault` + +## Definition + +- `ExactOptionalSchema` + - `type` + - `reference` + - `expects` + - `wrapped` + - `default` diff --git a/website/src/routes/api/(types)/ExactOptionalSchema/properties.ts b/website/src/routes/api/(types)/ExactOptionalSchema/properties.ts new file mode 100644 index 000000000..bd7107f74 --- /dev/null +++ b/website/src/routes/api/(types)/ExactOptionalSchema/properties.ts @@ -0,0 +1,118 @@ +import type { PropertyProps } from '~/components'; + +export const properties: Record = { + TWrapped: { + modifier: 'extends', + type: { + type: 'custom', + name: 'BaseSchema', + href: '../BaseSchema/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + }, + TDefault: { + modifier: 'extends', + type: { + type: 'custom', + name: 'Default', + href: '../Default/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + 'undefined', + ], + }, + }, + BaseSchema: { + modifier: 'extends', + type: { + type: 'custom', + name: 'BaseSchema', + href: '../BaseSchema/', + generics: [ + { + type: 'custom', + name: 'InferInput', + href: '../InferInput/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + ], + }, + { + type: 'custom', + name: 'InferOutput', + href: '../InferOutput/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + ], + }, + { + type: 'custom', + name: 'InferIssue', + href: '../InferIssue/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + ], + }, + ], + }, + }, + type: { + type: { + type: 'string', + value: 'exact_optional', + }, + }, + reference: { + type: { + type: 'custom', + modifier: 'typeof', + name: 'exactOptional', + href: '../exactOptional/', + }, + }, + expects: { + type: { + type: 'custom', + name: 'TWrapped', + indexes: [ + { + type: 'string', + value: 'expects', + }, + ], + }, + }, + wrapped: { + type: { + type: 'custom', + name: 'TWrapped', + }, + }, + default: { + type: { + type: 'custom', + name: 'TDefault', + }, + }, +}; diff --git a/website/src/routes/api/(types)/ExactOptionalSchemaAsync/index.mdx b/website/src/routes/api/(types)/ExactOptionalSchemaAsync/index.mdx new file mode 100644 index 000000000..a535167ee --- /dev/null +++ b/website/src/routes/api/(types)/ExactOptionalSchemaAsync/index.mdx @@ -0,0 +1,27 @@ +--- +title: ExactOptionalSchemaAsync +description: Exact optional schema async interface. +contributors: + - fabian-hiller +--- + +import { Property } from '~/components'; +import { properties } from './properties'; + +# ExactOptionalSchemaAsync + +Exact optional schema async interface. + +## Generics + +- `TWrapped` +- `TDefault` + +## Definition + +- `ExactOptionalSchemaAsync` + - `type` + - `reference` + - `expects` + - `wrapped` + - `default` diff --git a/website/src/routes/api/(types)/ExactOptionalSchemaAsync/properties.ts b/website/src/routes/api/(types)/ExactOptionalSchemaAsync/properties.ts new file mode 100644 index 000000000..303e47fda --- /dev/null +++ b/website/src/routes/api/(types)/ExactOptionalSchemaAsync/properties.ts @@ -0,0 +1,161 @@ +import type { PropertyProps } from '~/components'; + +export const properties: Record = { + TWrapped: { + modifier: 'extends', + type: { + type: 'union', + options: [ + { + type: 'custom', + name: 'BaseSchema', + href: '../BaseSchema/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + { + type: 'custom', + name: 'BaseSchemaAsync', + href: '../BaseSchemaAsync/', + generics: [ + 'unknown', + 'unknown', + { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: ['unknown'], + }, + ], + }, + ], + }, + }, + TDefault: { + modifier: 'extends', + type: { + type: 'custom', + name: 'DefaultAsync', + href: '../DefaultAsync/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + 'undefined', + ], + }, + }, + BaseSchemaAsync: { + type: { + type: 'custom', + name: 'BaseSchemaAsync', + href: '../BaseSchemaAsync/', + generics: [ + { + type: 'custom', + name: 'InferInput', + href: '../InferInput/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + ], + }, + { + type: 'custom', + name: 'InferOutput', + href: '../InferOutput/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + ], + }, + { + type: 'custom', + name: 'InferIssue', + href: '../InferIssue/', + generics: [ + { + type: 'custom', + name: 'TWrapped', + }, + ], + }, + ], + }, + }, + type: { + type: { + type: 'string', + value: 'exact_optional', + }, + }, + reference: { + type: { + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'exactOptional', + href: '../exactOptional/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'exactOptionalAsync', + href: '../exactOptionalAsync/', + }, + ], + }, + }, + expects: { + type: { + type: 'template', + parts: [ + { + type: 'string', + value: '(', + }, + { + type: 'custom', + name: 'TWrapped', + indexes: [ + { + type: 'string', + value: 'expects', + }, + ], + }, + { + type: 'string', + value: ' | undefined)', + }, + ], + }, + }, + wrapped: { + type: { + type: 'custom', + name: 'TWrapped', + }, + }, + default: { + type: { + type: 'custom', + name: 'TDefault', + }, + }, +}; diff --git a/website/src/routes/api/(types)/Fallback/properties.ts b/website/src/routes/api/(types)/Fallback/properties.ts index ec3d5c25e..2ecc96045 100644 --- a/website/src/routes/api/(types)/Fallback/properties.ts +++ b/website/src/routes/api/(types)/Fallback/properties.ts @@ -26,12 +26,19 @@ export const properties: Record = { options: [ { type: 'custom', - name: 'InferOutput', - href: '../InferOutput/', + name: 'MaybeReadonly', + href: '../MaybeReadonly/', generics: [ { type: 'custom', - name: 'TSchema', + name: 'InferOutput', + href: '../InferOutput/', + generics: [ + { + type: 'custom', + name: 'TSchema', + }, + ], }, ], }, @@ -96,12 +103,19 @@ export const properties: Record = { ], return: { type: 'custom', - name: 'InferOutput', - href: '../InferOutput/', + name: 'MaybeReadonly', + href: '../MaybeReadonly/', generics: [ { type: 'custom', - name: 'TSchema', + name: 'InferOutput', + href: '../InferOutput/', + generics: [ + { + type: 'custom', + name: 'TSchema', + }, + ], }, ], }, diff --git a/website/src/routes/api/(types)/FallbackAsync/properties.ts b/website/src/routes/api/(types)/FallbackAsync/properties.ts index a8ad0b7f4..c59d0222c 100644 --- a/website/src/routes/api/(types)/FallbackAsync/properties.ts +++ b/website/src/routes/api/(types)/FallbackAsync/properties.ts @@ -46,12 +46,19 @@ export const properties: Record = { options: [ { type: 'custom', - name: 'InferOutput', - href: '../InferOutput/', + name: 'MaybeReadonly', + href: '../MaybeReadonly/', generics: [ { type: 'custom', - name: 'TSchema', + name: 'InferOutput', + href: '../InferOutput/', + generics: [ + { + type: 'custom', + name: 'TSchema', + }, + ], }, ], }, @@ -121,12 +128,19 @@ export const properties: Record = { generics: [ { type: 'custom', - name: 'InferOutput', - href: '../InferOutput/', + name: 'MaybeReadonly', + href: '../MaybeReadonly/', generics: [ { type: 'custom', - name: 'TSchema', + name: 'InferOutput', + href: '../InferOutput/', + generics: [ + { + type: 'custom', + name: 'TSchema', + }, + ], }, ], }, diff --git a/website/src/routes/api/(types)/IntersectSchemaAsync/properties.ts b/website/src/routes/api/(types)/IntersectSchemaAsync/properties.ts index a9a5bbfe3..5febbe443 100644 --- a/website/src/routes/api/(types)/IntersectSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/IntersectSchemaAsync/properties.ts @@ -92,10 +92,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'intersectAsync', - href: '../intersectAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'intersect', + href: '../intersect/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'intersectAsync', + href: '../intersectAsync/', + }, + ], }, }, options: { diff --git a/website/src/routes/api/(types)/LazySchemaAsync/properties.ts b/website/src/routes/api/(types)/LazySchemaAsync/properties.ts index 01587983a..ab5467100 100644 --- a/website/src/routes/api/(types)/LazySchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/LazySchemaAsync/properties.ts @@ -90,10 +90,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'lazyAsync', - href: '../lazyAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'lazy', + href: '../lazy/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'lazyAsync', + href: '../lazyAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/LooseObjectIssue/properties.ts b/website/src/routes/api/(types)/LooseObjectIssue/properties.ts index 3f7c49b8b..d9d0284a9 100644 --- a/website/src/routes/api/(types)/LooseObjectIssue/properties.ts +++ b/website/src/routes/api/(types)/LooseObjectIssue/properties.ts @@ -24,8 +24,27 @@ export const properties: Record = { }, expected: { type: { - type: 'string', - value: 'Object', + type: 'union', + options: [ + { + type: 'string', + value: 'Object', + }, + { + type: 'template', + parts: [ + { + type: 'string', + value: '"', + }, + 'string', + { + type: 'string', + value: '"', + }, + ], + }, + ], }, }, }; diff --git a/website/src/routes/api/(types)/LooseObjectSchemaAsync/properties.ts b/website/src/routes/api/(types)/LooseObjectSchemaAsync/properties.ts index c6cce0046..2dba2d2b9 100644 --- a/website/src/routes/api/(types)/LooseObjectSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/LooseObjectSchemaAsync/properties.ts @@ -125,10 +125,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'looseObjectAsync', - href: '../looseObjectAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'looseObject', + href: '../looseObject/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'looseObjectAsync', + href: '../looseObjectAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/LooseTupleSchemaAsync/properties.ts b/website/src/routes/api/(types)/LooseTupleSchemaAsync/properties.ts index 728ba6cd7..0a09c8398 100644 --- a/website/src/routes/api/(types)/LooseTupleSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/LooseTupleSchemaAsync/properties.ts @@ -113,10 +113,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'looseTupleAsync', - href: '../looseTupleAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'looseTuple', + href: '../looseTuple/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'looseTupleAsync', + href: '../looseTupleAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/MapSchemaAsync/properties.ts b/website/src/routes/api/(types)/MapSchemaAsync/properties.ts index 7b82addfc..31df7fdca 100644 --- a/website/src/routes/api/(types)/MapSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/MapSchemaAsync/properties.ts @@ -178,10 +178,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'mapAsync', - href: '../mapAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'map', + href: '../map/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'mapAsync', + href: '../mapAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/NonNullableSchemaAsync/properties.ts b/website/src/routes/api/(types)/NonNullableSchemaAsync/properties.ts index a6fdfb467..b7435e081 100644 --- a/website/src/routes/api/(types)/NonNullableSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/NonNullableSchemaAsync/properties.ts @@ -120,10 +120,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'nonNullableAsync', - href: '../nonNullableAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'nonNullable', + href: '../nonNullable/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'nonNullableAsync', + href: '../nonNullableAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/NonNullishSchemaAsync/properties.ts b/website/src/routes/api/(types)/NonNullishSchemaAsync/properties.ts index 4f6e33b8c..7a7c6686a 100644 --- a/website/src/routes/api/(types)/NonNullishSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/NonNullishSchemaAsync/properties.ts @@ -120,10 +120,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'nonNullishAsync', - href: '../nonNullishAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'nonNullish', + href: '../nonNullish/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'nonNullishAsync', + href: '../nonNullishAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/NonOptionalSchemaAsync/properties.ts b/website/src/routes/api/(types)/NonOptionalSchemaAsync/properties.ts index 7e98555b7..9009068b9 100644 --- a/website/src/routes/api/(types)/NonOptionalSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/NonOptionalSchemaAsync/properties.ts @@ -120,10 +120,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'nonOptionalAsync', - href: '../nonOptionalAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'nonOptional', + href: '../nonOptional/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'nonOptionalAsync', + href: '../nonOptionalAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/NullableSchemaAsync/properties.ts b/website/src/routes/api/(types)/NullableSchemaAsync/properties.ts index 1912a90d8..f6623e4ec 100644 --- a/website/src/routes/api/(types)/NullableSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/NullableSchemaAsync/properties.ts @@ -114,10 +114,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'nullableAsync', - href: '../nullableAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'nullable', + href: '../nullable/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'nullableAsync', + href: '../nullableAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/ObjectIssue/properties.ts b/website/src/routes/api/(types)/ObjectIssue/properties.ts index dabb2880e..7b81956b2 100644 --- a/website/src/routes/api/(types)/ObjectIssue/properties.ts +++ b/website/src/routes/api/(types)/ObjectIssue/properties.ts @@ -24,8 +24,27 @@ export const properties: Record = { }, expected: { type: { - type: 'string', - value: 'Object', + type: 'union', + options: [ + { + type: 'string', + value: 'Object', + }, + { + type: 'template', + parts: [ + { + type: 'string', + value: '"', + }, + 'string', + { + type: 'string', + value: '"', + }, + ], + }, + ], }, }, }; diff --git a/website/src/routes/api/(types)/ObjectSchemaAsync/properties.ts b/website/src/routes/api/(types)/ObjectSchemaAsync/properties.ts index 09d017315..1501bfd32 100644 --- a/website/src/routes/api/(types)/ObjectSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/ObjectSchemaAsync/properties.ts @@ -91,10 +91,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'objectAsync', - href: '../objectAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'object', + href: '../object/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'objectAsync', + href: '../objectAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/ObjectWithRestIssue/properties.ts b/website/src/routes/api/(types)/ObjectWithRestIssue/properties.ts index 9beedd39f..5ce5d9618 100644 --- a/website/src/routes/api/(types)/ObjectWithRestIssue/properties.ts +++ b/website/src/routes/api/(types)/ObjectWithRestIssue/properties.ts @@ -24,8 +24,27 @@ export const properties: Record = { }, expected: { type: { - type: 'string', - value: 'Object', + type: 'union', + options: [ + { + type: 'string', + value: 'Object', + }, + { + type: 'template', + parts: [ + { + type: 'string', + value: '"', + }, + 'string', + { + type: 'string', + value: '"', + }, + ], + }, + ], }, }, }; diff --git a/website/src/routes/api/(types)/OptionalSchemaAsync/properties.ts b/website/src/routes/api/(types)/OptionalSchemaAsync/properties.ts index e41ce94b7..6e1736e2a 100644 --- a/website/src/routes/api/(types)/OptionalSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/OptionalSchemaAsync/properties.ts @@ -114,10 +114,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'optionalAsync', - href: '../optionalAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'optional', + href: '../optional/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'optionalAsync', + href: '../optionalAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/RecordSchemaAsync/properties.ts b/website/src/routes/api/(types)/RecordSchemaAsync/properties.ts index 381022b7d..e876f81e5 100644 --- a/website/src/routes/api/(types)/RecordSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/RecordSchemaAsync/properties.ts @@ -184,10 +184,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'recordAsync', - href: '../recordAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'record', + href: '../record/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'recordAsync', + href: '../recordAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/SetSchemaAsync/properties.ts b/website/src/routes/api/(types)/SetSchemaAsync/properties.ts index ef53b6aba..212297f01 100644 --- a/website/src/routes/api/(types)/SetSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/SetSchemaAsync/properties.ts @@ -120,10 +120,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'setAsync', - href: '../setAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'set', + href: '../set/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'setAsync', + href: '../setAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/StrictObjectIssue/properties.ts b/website/src/routes/api/(types)/StrictObjectIssue/properties.ts index 9ba81de7d..9502060d8 100644 --- a/website/src/routes/api/(types)/StrictObjectIssue/properties.ts +++ b/website/src/routes/api/(types)/StrictObjectIssue/properties.ts @@ -24,8 +24,31 @@ export const properties: Record = { }, expected: { type: { - type: 'string', - value: 'Object', + type: 'union', + options: [ + { + type: 'string', + value: 'Object', + }, + { + type: 'template', + parts: [ + { + type: 'string', + value: '"', + }, + 'string', + { + type: 'string', + value: '"', + }, + ], + }, + { + type: 'string', + value: 'never', + }, + ], }, }, }; diff --git a/website/src/routes/api/(types)/StrictObjectSchemaAsync/properties.ts b/website/src/routes/api/(types)/StrictObjectSchemaAsync/properties.ts index 6d595f2e1..23ab1fc0b 100644 --- a/website/src/routes/api/(types)/StrictObjectSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/StrictObjectSchemaAsync/properties.ts @@ -91,10 +91,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'strictObjectAsync', - href: '../strictObjectAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'strictObject', + href: '../strictObject/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'strictObjectAsync', + href: '../strictObjectAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/StrictTupleSchemaAsync/properties.ts b/website/src/routes/api/(types)/StrictTupleSchemaAsync/properties.ts index 7a0d08942..02b3f7448 100644 --- a/website/src/routes/api/(types)/StrictTupleSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/StrictTupleSchemaAsync/properties.ts @@ -91,10 +91,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'strictTupleAsync', - href: '../strictTupleAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'strictTuple', + href: '../strictTuple/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'strictTupleAsync', + href: '../strictTupleAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/TransformActionAsync/properties.ts b/website/src/routes/api/(types)/TransformActionAsync/properties.ts index 050de07cc..3ccb9fc68 100644 --- a/website/src/routes/api/(types)/TransformActionAsync/properties.ts +++ b/website/src/routes/api/(types)/TransformActionAsync/properties.ts @@ -36,10 +36,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'transformAsync', - href: '../transformAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'transform', + href: '../transform/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'transformAsync', + href: '../transformAsync/', + }, + ], }, }, operation: { diff --git a/website/src/routes/api/(types)/TupleSchemaAsync/properties.ts b/website/src/routes/api/(types)/TupleSchemaAsync/properties.ts index 5856fdd4a..301302671 100644 --- a/website/src/routes/api/(types)/TupleSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/TupleSchemaAsync/properties.ts @@ -91,10 +91,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'tupleAsync', - href: '../tupleAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'tuple', + href: '../tuple/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'tupleAsync', + href: '../tupleAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/TupleWithRestSchemaAsync/properties.ts b/website/src/routes/api/(types)/TupleWithRestSchemaAsync/properties.ts index b19562df0..63c237dba 100644 --- a/website/src/routes/api/(types)/TupleWithRestSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/TupleWithRestSchemaAsync/properties.ts @@ -182,10 +182,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'tupleWithRestAsync', - href: '../tupleWithRestAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'tupleWithRest', + href: '../tupleWithRest/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'tupleWithRestAsync', + href: '../tupleWithRestAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/UndefinedableSchemaAsync/properties.ts b/website/src/routes/api/(types)/UndefinedableSchemaAsync/properties.ts index aab595c9e..5ec542fd1 100644 --- a/website/src/routes/api/(types)/UndefinedableSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/UndefinedableSchemaAsync/properties.ts @@ -114,10 +114,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'undefinedableAsync', - href: '../undefinedableAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'undefinedable', + href: '../undefinedable/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'undefinedableAsync', + href: '../undefinedableAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(types)/UnionSchemaAsync/properties.ts b/website/src/routes/api/(types)/UnionSchemaAsync/properties.ts index f88a52ab8..dc8e77415 100644 --- a/website/src/routes/api/(types)/UnionSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/UnionSchemaAsync/properties.ts @@ -121,10 +121,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'unionAsync', - href: '../unionAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'union', + href: '../union/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'unionAsync', + href: '../unionAsync/', + }, + ], }, }, options: { diff --git a/website/src/routes/api/(types)/VariantSchemaAsync/properties.ts b/website/src/routes/api/(types)/VariantSchemaAsync/properties.ts index cb9827f5a..08d339385 100644 --- a/website/src/routes/api/(types)/VariantSchemaAsync/properties.ts +++ b/website/src/routes/api/(types)/VariantSchemaAsync/properties.ts @@ -103,10 +103,21 @@ export const properties: Record = { }, reference: { type: { - type: 'custom', - modifier: 'typeof', - name: 'variantAsync', - href: '../variantAsync/', + type: 'union', + options: [ + { + type: 'custom', + modifier: 'typeof', + name: 'variant', + href: '../variant/', + }, + { + type: 'custom', + modifier: 'typeof', + name: 'variantAsync', + href: '../variantAsync/', + }, + ], }, }, expects: { diff --git a/website/src/routes/api/(utils)/entriesFromList/index.mdx b/website/src/routes/api/(utils)/entriesFromList/index.mdx index 8af47e0f8..df7e108ea 100644 --- a/website/src/routes/api/(utils)/entriesFromList/index.mdx +++ b/website/src/routes/api/(utils)/entriesFromList/index.mdx @@ -57,6 +57,7 @@ The following APIs can be combined with `entriesFromList`. 'custom', 'date', 'enum', + 'exactOptional', 'file', 'function', 'instance', diff --git a/website/src/routes/api/menu.md b/website/src/routes/api/menu.md index 1e904c392..2250f006b 100644 --- a/website/src/routes/api/menu.md +++ b/website/src/routes/api/menu.md @@ -10,6 +10,7 @@ - [custom](/api/custom/) - [date](/api/date/) - [enum](/api/enum/) +- [exactOptional](/api/exactOptional/) - [file](/api/file/) - [function](/api/function/) - [instance](/api/instance/) @@ -202,6 +203,7 @@ - [checkAsync](/api/checkAsync/) - [checkItemsAsync](/api/checkItemsAsync/) - [customAsync](/api/customAsync/) +- [exactOptionalAsync](/api/exactOptionalAsync/) - [fallbackAsync](/api/fallbackAsync/) - [forwardAsync](/api/forwardAsync/) - [getDefaultsAsync](/api/getDefaultsAsync/) @@ -319,6 +321,8 @@ - [ErrorMessage](/api/ErrorMessage/) - [EveryItemAction](/api/EveryItemAction/) - [EveryItemIssue](/api/EveryItemIssue/) +- [ExactOptionalSchema](/api/ExactOptionalSchema/) +- [ExactOptionalSchemaAsync](/api/ExactOptionalSchemaAsync/) - [ExcludesAction](/api/ExcludesAction/) - [ExcludesIssue](/api/ExcludesIssue/) - [FailureDataset](/api/FailureDataset/) diff --git a/website/src/routes/guides/(main-concepts)/schemas/index.mdx b/website/src/routes/guides/(main-concepts)/schemas/index.mdx index 62ddfa955..0a25fc54f 100644 --- a/website/src/routes/guides/(main-concepts)/schemas/index.mdx +++ b/website/src/routes/guides/(main-concepts)/schemas/index.mdx @@ -109,6 +109,7 @@ Beyond primitive and complex values, there are also schema functions for more sp 'any', 'custom', 'enum', + 'exactOptional', 'instance', 'intersect', 'lazy', @@ -136,6 +137,7 @@ import * as v from 'valibot'; const AnySchema = v.any(); // any const CustomSchema = v.custom<`${number}px`>(isPixelString); // `${number}px` const EnumSchema = v.enum(Direction); // Direction +const ExactOptionalSchema = v.exactOptional(v.string()); // string const InstanceSchema = v.instance(Error); // Error const LazySchema = v.lazy(() => v.string()); // string const IntersectSchema = v.intersect([v.string(), v.literal('a')]); // string & 'a' From d42d8907265bf6417a421b67aee3957a1b53f7b8 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Tue, 21 Jan 2025 19:01:26 -0500 Subject: [PATCH 9/9] Update changelog of library to reflect changes of PR #1013 --- library/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/CHANGELOG.md b/library/CHANGELOG.md index f27022a19..6c849a5e4 100644 --- a/library/CHANGELOG.md +++ b/library/CHANGELOG.md @@ -12,11 +12,13 @@ All notable changes to the library will be documented in this file. - Add `rfcEmail` action to validate RFC 5322 email addresses (pull request #912) - Add new overload signature to `pipe` and `pipeAync` method to support unlimited pipe items of same input and output type (issue #852) - Add `@__NO_SIDE_EFFECTS__` notation to improve tree shaking (pull request #995) +- Add `exactOptional` and `exactOptionalAsync` schema (PR #1013) - Change types and implementation to support Standard Schema - Change behaviour of `minValue` and `maxValue` for `NaN` (pull request #843) - Change type and behaviour of `nullable`, `nullableAsync`, `nullish`, `nullishAsync`, `optional`, `optionalAsync`, `undefinedable` and `undefinedableAsync` for undefined default value (issue #878) - Change type signature of `partialCheck` and `partialCheckAsync` action to add `.pathList` property in a type-safe way - Change type signature of `findItem` action to support type predicates (issue #867) +- Change validation of missing object entries in `looseObject`, `looseObjectAsync`, `object`, `objectAsync`, `objectWithRest`, `objectWithRestAsync`, `strictObject` and `strictObject` (PR #1013) - Refactor `bytes`, `maxBytes`, `minBytes` and `notBytes` action - Fix implementation of `nonOptional`, `nonOptionalAsync`, `nonNullable`, `nonNullableAsync`, `nonNullish` and `nonNullishAsync` schema in edge cases (issue #909) - Fix instantiation error for `any` in `PathKeys` type (issue #929)