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