diff --git a/library/CHANGELOG.md b/library/CHANGELOG.md index 0ade50f47..2bb7e275c 100644 --- a/library/CHANGELOG.md +++ b/library/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the library will be documented in this file. - Add `gtValue` and `ltValue` action for greater than and less than validation (pull request #978, #985) - Add `values` and `notValues` action to validate values (pull request #919) - Add `slug` action to validate URL slugs (pull request #910) +- Add `entriesFromObjects` util to improve tree shaking (issue #1023) - Add new overload signature to `pipe` and `pipeAync` method to support unlimited pipe items of same input and output type (issue #852) - Add `@__NO_SIDE_EFFECTS__` notation to improve tree shaking (pull request #995) - Add `exactOptional` and `exactOptionalAsync` schema (PR #1013) diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts new file mode 100644 index 000000000..0dcfa5db5 --- /dev/null +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts @@ -0,0 +1,63 @@ +import { describe, expectTypeOf, test } from 'vitest'; +import { + boolean, + type BooleanSchema, + number, + type NumberSchema, + object, + objectAsync, + string, + type StringSchema, +} from '../../schemas/index.ts'; +import { entriesFromObjects } from './entriesFromObjects.ts'; + +describe('entriesFromObjects', () => { + describe('should return objects entries', () => { + const schema1 = object({ foo: string(), bar: string() }); + const schema2 = objectAsync({ baz: number(), qux: number() }); + const schema3 = object({ foo: boolean(), baz: boolean() }); + + test('for missing schema', () => { + expectTypeOf( + // @ts-expect-error + entriesFromObjects([]) + ).toEqualTypeOf(); + }); + + test('for single schema', () => { + expectTypeOf(entriesFromObjects([schema1])).toEqualTypeOf<{ + readonly foo: StringSchema; + readonly bar: StringSchema; + }>(); + }); + + test('for multiple schemes', () => { + expectTypeOf(entriesFromObjects([schema1, schema2])).toEqualTypeOf<{ + readonly foo: StringSchema; + readonly bar: StringSchema; + readonly baz: NumberSchema; + readonly qux: NumberSchema; + }>(); + }); + + test('with overwrites', () => { + expectTypeOf( + entriesFromObjects([schema1, schema2, schema3]) + ).toEqualTypeOf<{ + readonly bar: StringSchema; + readonly qux: NumberSchema; + readonly foo: BooleanSchema; + readonly baz: BooleanSchema; + }>(); + }); + + test('for empty entries', () => { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + expectTypeOf(entriesFromObjects([object({})])).toEqualTypeOf<{}>(); + expectTypeOf( + entriesFromObjects([object({}), objectAsync({})]) + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + ).toEqualTypeOf<{}>(); + }); + }); +}); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts new file mode 100644 index 000000000..e7e670e58 --- /dev/null +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from 'vitest'; +import { + boolean, + number, + object, + objectAsync, + string, +} from '../../schemas/index.ts'; +import { entriesFromObjects } from './entriesFromObjects.ts'; + +describe('entriesFromObjects', () => { + describe('should return objects entries', () => { + const schema1 = object({ foo: string(), bar: string() }); + const schema2 = objectAsync({ baz: number(), qux: number() }); + const schema3 = object({ foo: boolean(), baz: boolean() }); + + test('for missing schema', () => { + expect( + // @ts-expect-error + entriesFromObjects([]) + ).toStrictEqual({}); + }); + + test('for single schema', () => { + expect(entriesFromObjects([schema1])).toStrictEqual(schema1.entries); + }); + + test('for multiple schemes', () => { + expect(entriesFromObjects([schema1, schema2])).toStrictEqual({ + ...schema1.entries, + ...schema2.entries, + }); + }); + + test('with overwrites', () => { + expect(entriesFromObjects([schema1, schema2, schema3])).toStrictEqual({ + ...schema1.entries, + ...schema2.entries, + ...schema3.entries, + }); + }); + + test('for empty entries', () => { + expect(entriesFromObjects([object({})])).toStrictEqual({}); + expect(entriesFromObjects([object({}), objectAsync({})])).toStrictEqual( + {} + ); + }); + }); +}); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.ts new file mode 100644 index 000000000..5f930dc69 --- /dev/null +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -0,0 +1,117 @@ +import type { + LooseObjectIssue, + LooseObjectSchema, + LooseObjectSchemaAsync, + ObjectIssue, + ObjectSchema, + ObjectSchemaAsync, + ObjectWithRestIssue, + ObjectWithRestSchema, + ObjectWithRestSchemaAsync, + StrictObjectIssue, + StrictObjectSchema, + StrictObjectSchemaAsync, +} from '../../schemas/index.ts'; +import type { + BaseIssue, + BaseSchema, + BaseSchemaAsync, + ErrorMessage, + ObjectEntries, + ObjectEntriesAsync, + Prettify, +} from '../../types/index.ts'; + +/** + * Schema type. + */ +type Schema = + | LooseObjectSchema | undefined> + | LooseObjectSchemaAsync< + ObjectEntriesAsync, + ErrorMessage | undefined + > + | ObjectSchema | undefined> + | ObjectSchemaAsync | undefined> + | ObjectWithRestSchema< + ObjectEntries, + BaseSchema>, + ErrorMessage | undefined + > + | ObjectWithRestSchemaAsync< + ObjectEntriesAsync, + | BaseSchema> + | BaseSchemaAsync>, + ErrorMessage | undefined + > + | StrictObjectSchema< + ObjectEntries, + ErrorMessage | undefined + > + | StrictObjectSchemaAsync< + ObjectEntriesAsync, + ErrorMessage | undefined + >; + +/** + * Merge entries type. + */ +type MergeEntries< + TFirstEntries extends ObjectEntries | ObjectEntriesAsync, + TRestEntries extends ObjectEntries | ObjectEntriesAsync, +> = Prettify< + { + [TKey in keyof TFirstEntries as TKey extends Exclude< + keyof TFirstEntries, + keyof TRestEntries + > + ? TKey + : never]: TFirstEntries[TKey]; + } & TRestEntries +>; + +/** + * Recursive merge type. + */ +type RecursiveMerge = TSchemas extends [ + infer TFirstSchema extends Schema, +] + ? TFirstSchema['entries'] + : TSchemas extends [ + infer TFirstSchema extends Schema, + ...infer TRestSchemas extends [Schema, ...Schema[]], + ] + ? MergeEntries> + : never; + +/** + * Merged entries types. + */ +type MergedEntries = Prettify< + RecursiveMerge +>; + +/** + * Creates a new object entries definition from existing object schemas. + * + * @param schemas The schemas to merge the entries from. + * + * @returns The object entries from the schemas. + * + * @beta + */ +export function entriesFromObjects< + const TSchemas extends [Schema, ...Schema[]], +>(schemas: TSchemas): MergedEntries; + +// @__NO_SIDE_EFFECTS__ +export function entriesFromObjects( + schemas: [Schema, ...Schema[]] +): MergedEntries<[Schema, ...Schema[]]> { + const entries = {}; + for (const schema of schemas) { + Object.assign(entries, schema.entries); + } + // @ts-expect-error + return entries; +} diff --git a/library/src/utils/entriesFromObjects/index.ts b/library/src/utils/entriesFromObjects/index.ts new file mode 100644 index 000000000..a409918f7 --- /dev/null +++ b/library/src/utils/entriesFromObjects/index.ts @@ -0,0 +1 @@ +export * from './entriesFromObjects.ts'; diff --git a/library/src/utils/index.ts b/library/src/utils/index.ts index 976733785..23ec9d0ed 100644 --- a/library/src/utils/index.ts +++ b/library/src/utils/index.ts @@ -8,6 +8,7 @@ export * from './_isValidObjectKey/index.ts'; export * from './_joinExpects/index.ts'; export * from './_stringify/index.ts'; export * from './entriesFromList/index.ts'; +export * from './entriesFromObjects/index.ts'; export * from './getDotPath/index.ts'; export * from './isOfKind/index.ts'; export * from './isOfType/index.ts'; diff --git a/website/src/routes/api/(utils)/entriesFromObjects/index.mdx b/website/src/routes/api/(utils)/entriesFromObjects/index.mdx new file mode 100644 index 000000000..382a75bd9 --- /dev/null +++ b/website/src/routes/api/(utils)/entriesFromObjects/index.mdx @@ -0,0 +1,54 @@ +--- +title: entriesFromObjects +description: Creates a new object entries definition from existing object schemas. +source: /utils/entriesFromObjects/entriesFromObjects.ts +contributors: + - fabian-hiller +--- + +import { ApiList, Property } from '~/components'; +import { properties } from './properties'; + +# entriesFromObjects + +Creates a new object entries definition from existing object schemas. + +```ts +const entries = v.entriesFromObjects(schemas); +``` + +## Generics + +- `TSchemas` + +## Parameters + +- `schemas` + +## Returns + +- `entries` + +## Examples + +The following example show how `entriesFromObjects` can be used. + +> Hint: The third schema of the list overwrites the `foo` and `baz` properties of the previous schemas. + +```ts +const ObjectSchema = v.object( + v.entriesFromObjects([ + v.object({ foo: v.string(), bar: v.string() }); + v.object({ baz: v.number(), qux: v.number() }); + v.object({ foo: v.boolean(), baz: v.boolean() }); + ]) +); +``` + +## Related + +The following APIs can be combined with `entriesFromObjects`. + +### Schemas + + diff --git a/website/src/routes/api/(utils)/entriesFromObjects/properties.ts b/website/src/routes/api/(utils)/entriesFromObjects/properties.ts new file mode 100644 index 000000000..8612741f2 --- /dev/null +++ b/website/src/routes/api/(utils)/entriesFromObjects/properties.ts @@ -0,0 +1,42 @@ +import type { PropertyProps } from '~/components'; + +export const properties: Record = { + TSchemas: { + modifier: 'extends', + type: { + type: 'tuple', + items: [ + { + type: 'custom', + name: 'Schema', + }, + { + type: 'array', + spread: true, + item: { + type: 'custom', + name: 'Schema', + }, + }, + ], + }, + }, + schemas: { + type: { + type: 'custom', + name: 'TSchemas', + }, + }, + entries: { + type: { + type: 'custom', + name: 'MergedEntries', + generics: [ + { + type: 'custom', + name: 'TSchemas', + }, + ], + }, + }, +}; diff --git a/website/src/routes/api/menu.md b/website/src/routes/api/menu.md index 76ac7cf51..a64ca2602 100644 --- a/website/src/routes/api/menu.md +++ b/website/src/routes/api/menu.md @@ -194,6 +194,7 @@ ## Utils - [entriesFromList](/api/entriesFromList/) +- [entriesFromObjects](/api/entriesFromObjects/) - [getDotPath](/api/getDotPath/) - [isOfKind](/api/isOfKind/) - [isOfType](/api/isOfType/)