From 3e8087694afc241b1aa829537ecbe1d894f6ab06 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 19 Jan 2025 02:40:40 +0800 Subject: [PATCH 01/10] feat: create `entriesFromObjects` --- .../entriesFromObjects.test-d.ts | 42 +++++++++++++++++++ .../entriesFromObjects.test.ts | 35 ++++++++++++++++ .../entriesFromObjects/entriesFromObjects.ts | 34 +++++++++++++++ library/src/utils/entriesFromObjects/index.ts | 1 + 4 files changed, 112 insertions(+) create mode 100644 library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts create mode 100644 library/src/utils/entriesFromObjects/entriesFromObjects.test.ts create mode 100644 library/src/utils/entriesFromObjects/entriesFromObjects.ts create mode 100644 library/src/utils/entriesFromObjects/index.ts 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..14da6e057 --- /dev/null +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts @@ -0,0 +1,42 @@ +import { describe, expectTypeOf, test } from 'vitest'; +import { number, object, optional, string } from '../../schemas/index.ts'; +import { entriesFromObjects } from './entriesFromObjects.ts'; + +describe('entriesFromObjects', () => { + describe('should return objects entries', () => { + const fooSchema = object({ foo: string() }); + const barSchema = object({ bar: string() }); + const overrideSchema = object({ foo: optional(number()) }); + + test('for single schema', () => { + expectTypeOf(object(entriesFromObjects(fooSchema))).toEqualTypeOf< + typeof fooSchema + >() + }); + + test('for multi schema', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const foobar = object({ + ...fooSchema.entries, + ...barSchema.entries + }) + + expectTypeOf(object(entriesFromObjects(fooSchema, barSchema))).toEqualTypeOf< + typeof foobar + >(); + }); + + test('for override schema', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const foobarOverride = object({ + ...fooSchema.entries, + ...barSchema.entries, + ...overrideSchema.entries + }) + + expectTypeOf(object(entriesFromObjects(fooSchema, barSchema, overrideSchema))).toEqualTypeOf< + typeof foobarOverride + >(); + }); + }); +}); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts new file mode 100644 index 000000000..da5c60428 --- /dev/null +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from 'vitest'; +import { number, object, optional, string } from '../../schemas/index.ts'; +import { entriesFromObjects } from './entriesFromObjects.ts'; + +describe('entriesFromObjects', () => { + describe('should return objects entries', () => { + const fooSchema = object({ foo: string() }); + const barSchema = object({ bar: string() }); + const overrideSchema = object({ foo: optional(number()) }); + + test('for single schema', () => { + expect(entriesFromObjects(fooSchema)).toStrictEqual(fooSchema.entries) + + }); + + test('for multi schema', () => { + expect(entriesFromObjects(fooSchema, barSchema)).toStrictEqual({ + ...fooSchema.entries, + ...barSchema.entries + }) + }); + + test('for override schema', () => { + expect(entriesFromObjects( + fooSchema, + barSchema, + overrideSchema + )).toStrictEqual({ + ...fooSchema.entries, + ...barSchema.entries, + ...overrideSchema.entries + }) + }); + }); +}); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.ts new file mode 100644 index 000000000..4bb010d55 --- /dev/null +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -0,0 +1,34 @@ +import type { ObjectSchema } from '../../schemas/index.ts'; +import type { ObjectEntries } from '../../types/index.ts'; + +type Merge = Omit & B + +type Flatten = { [K in keyof T]: T[K] }; + +type MergedEntries[]> = Flatten< + TSchemas extends [infer First, ...infer Rest] + ? First extends ObjectSchema + ? Rest extends ObjectSchema[] + ? Merge> + : FirstEntries + : unknown + : object +>; + +/** + * Creates a new object entries definition from existing object schemas. + * + * @param schemas The schemas to merge. + * + * @returns The object entries. + */ +// @__NO_SIDE_EFFECTS__ +export function entriesFromObjects< + TSchemas extends ObjectSchema[] +>( + ...schemas: TSchemas +): MergedEntries { + return schemas.reduce((acc, schema) => { + return { ...acc, ...schema.entries }; + }, {} as MergedEntries); +} 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'; From 8c9528c1cee47abdc7a7638dfa8e3ca89ab26ba7 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 19 Jan 2025 03:08:38 +0800 Subject: [PATCH 02/10] refactor: use `Object.assign` for `entriesFromObjects` --- .../src/utils/entriesFromObjects/entriesFromObjects.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.ts index 4bb010d55..4a28159be 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -28,7 +28,9 @@ export function entriesFromObjects< >( ...schemas: TSchemas ): MergedEntries { - return schemas.reduce((acc, schema) => { - return { ...acc, ...schema.entries }; - }, {} as MergedEntries); + const entries = {} as MergedEntries; + for (const schema of schemas) { + Object.assign(entries, schema.entries); + } + return entries; } From d4d89f7e8f9f93491da17685087aa01c218cd078 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 19 Jan 2025 03:10:13 +0800 Subject: [PATCH 03/10] feat: support more object schemas --- .../entriesFromObjects/entriesFromObjects.ts | 75 ++++++++++++++++--- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.ts index 4a28159be..6c077362c 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -1,18 +1,71 @@ -import type { ObjectSchema } from '../../schemas/index.ts'; -import type { ObjectEntries } from '../../types/index.ts'; +import type { + ObjectSchema , + LooseObjectIssue, + LooseObjectSchema, + LooseObjectSchemaAsync, + ObjectIssue, + ObjectSchemaAsync, + ObjectWithRestIssue, + ObjectWithRestSchema, + ObjectWithRestSchemaAsync, + StrictObjectIssue, + StrictObjectSchema, + StrictObjectSchemaAsync, +} from '../../schemas/index.ts'; +import type { + ObjectEntries , + BaseIssue, + BaseSchema, + BaseSchemaAsync, + ErrorMessage, + ObjectEntriesAsync, +} from '../../types/index.ts'; -type Merge = Omit & B +/** + * 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 + >; + +// Type Utils +type MergeObject = Omit & B type Flatten = { [K in keyof T]: T[K] }; -type MergedEntries[]> = Flatten< - TSchemas extends [infer First, ...infer Rest] - ? First extends ObjectSchema - ? Rest extends ObjectSchema[] - ? Merge> - : FirstEntries - : unknown +type MergedEntries = Flatten< + TSchemas extends [infer TFirstSchema, ...infer TRestSchemas] + ? TFirstSchema extends Schema + ? TRestSchemas extends Schema[] + ? MergeObject> + : TFirstSchema['entries'] : object + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + : {} >; /** @@ -24,7 +77,7 @@ type MergedEntries[]> = */ // @__NO_SIDE_EFFECTS__ export function entriesFromObjects< - TSchemas extends ObjectSchema[] + const TSchemas extends Schema[] >( ...schemas: TSchemas ): MergedEntries { From f55b1b93a86d5f28fbe71d81bfdf3501f6489047 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 19 Jan 2025 03:11:48 +0800 Subject: [PATCH 04/10] test: update test units --- .../entriesFromObjects.test-d.ts | 38 +++++++++++++------ .../entriesFromObjects.test.ts | 6 ++- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts index 14da6e057..df0c2b006 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts @@ -1,5 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { describe, expectTypeOf, test } from 'vitest'; -import { number, object, optional, string } from '../../schemas/index.ts'; +import { number, object, objectAsync, optional, string } from '../../schemas/index.ts'; import { entriesFromObjects } from './entriesFromObjects.ts'; describe('entriesFromObjects', () => { @@ -8,35 +9,48 @@ describe('entriesFromObjects', () => { const barSchema = object({ bar: string() }); const overrideSchema = object({ foo: optional(number()) }); + test('for empty schema', () => { + const r1 = {} + expectTypeOf(entriesFromObjects()).toEqualTypeOf() + + const r2 = object(entriesFromObjects(object({}))) + expectTypeOf(object({})).toEqualTypeOf() + }) + test('for single schema', () => { - expectTypeOf(object(entriesFromObjects(fooSchema))).toEqualTypeOf< - typeof fooSchema - >() + const o = object(entriesFromObjects(fooSchema)) + expectTypeOf(o).toEqualTypeOf() }); test('for multi schema', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const foobar = object({ ...fooSchema.entries, ...barSchema.entries }) - expectTypeOf(object(entriesFromObjects(fooSchema, barSchema))).toEqualTypeOf< - typeof foobar - >(); + const o = object(entriesFromObjects(fooSchema, barSchema)) + expectTypeOf(o).toEqualTypeOf(); }); test('for override schema', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const foobarOverride = object({ ...fooSchema.entries, ...barSchema.entries, ...overrideSchema.entries }) - expectTypeOf(object(entriesFromObjects(fooSchema, barSchema, overrideSchema))).toEqualTypeOf< - typeof foobarOverride - >(); + const o = object(entriesFromObjects(fooSchema, barSchema, overrideSchema)) + expectTypeOf(o).toEqualTypeOf(); }); + + test('for async object schema', () => { + const asyncFooSchema = objectAsync(entriesFromObjects(fooSchema)) + + const o = objectAsync(entriesFromObjects(asyncFooSchema)) + expectTypeOf(o).toEqualTypeOf(); + + const o2 = object(entriesFromObjects(asyncFooSchema)) + expectTypeOf(o2).toEqualTypeOf(); + }) }); }); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts index da5c60428..327aeb93d 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts @@ -8,9 +8,13 @@ describe('entriesFromObjects', () => { const barSchema = object({ bar: string() }); const overrideSchema = object({ foo: optional(number()) }); + test('for empty schema', () => { + expect(entriesFromObjects()).toStrictEqual({}) + expect(entriesFromObjects(object({}))).toStrictEqual({}) + }) + test('for single schema', () => { expect(entriesFromObjects(fooSchema)).toStrictEqual(fooSchema.entries) - }); test('for multi schema', () => { From 7a6072bfbf6ca1937ba6866c2b0a8c7ec04677dc Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 19 Jan 2025 03:12:47 +0800 Subject: [PATCH 05/10] feat: export `entriesFromObjects` in utils --- library/src/utils/index.ts | 1 + 1 file changed, 1 insertion(+) 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'; From e69e0314206a3925340cd2f994dd82ef9f04b027 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 19 Jan 2025 03:30:29 +0800 Subject: [PATCH 06/10] chore: lint and format --- .../entriesFromObjects.test-d.ts | 44 +++++++++++-------- .../entriesFromObjects.test.ts | 24 +++++----- .../entriesFromObjects/entriesFromObjects.ts | 24 +++++----- 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts index df0c2b006..3a261cd64 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts @@ -1,6 +1,12 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { describe, expectTypeOf, test } from 'vitest'; -import { number, object, objectAsync, optional, string } from '../../schemas/index.ts'; +import { + number, + object, + objectAsync, + optional, + string, +} from '../../schemas/index.ts'; import { entriesFromObjects } from './entriesFromObjects.ts'; describe('entriesFromObjects', () => { @@ -10,25 +16,25 @@ describe('entriesFromObjects', () => { const overrideSchema = object({ foo: optional(number()) }); test('for empty schema', () => { - const r1 = {} - expectTypeOf(entriesFromObjects()).toEqualTypeOf() + const r1 = {}; + expectTypeOf(entriesFromObjects()).toEqualTypeOf(); - const r2 = object(entriesFromObjects(object({}))) - expectTypeOf(object({})).toEqualTypeOf() - }) + const r2 = object(entriesFromObjects(object({}))); + expectTypeOf(object({})).toEqualTypeOf(); + }); test('for single schema', () => { - const o = object(entriesFromObjects(fooSchema)) - expectTypeOf(o).toEqualTypeOf() + const o = object(entriesFromObjects(fooSchema)); + expectTypeOf(o).toEqualTypeOf(); }); test('for multi schema', () => { const foobar = object({ ...fooSchema.entries, - ...barSchema.entries - }) + ...barSchema.entries, + }); - const o = object(entriesFromObjects(fooSchema, barSchema)) + const o = object(entriesFromObjects(fooSchema, barSchema)); expectTypeOf(o).toEqualTypeOf(); }); @@ -36,21 +42,23 @@ describe('entriesFromObjects', () => { const foobarOverride = object({ ...fooSchema.entries, ...barSchema.entries, - ...overrideSchema.entries - }) + ...overrideSchema.entries, + }); - const o = object(entriesFromObjects(fooSchema, barSchema, overrideSchema)) + const o = object( + entriesFromObjects(fooSchema, barSchema, overrideSchema) + ); expectTypeOf(o).toEqualTypeOf(); }); test('for async object schema', () => { - const asyncFooSchema = objectAsync(entriesFromObjects(fooSchema)) + const asyncFooSchema = objectAsync(entriesFromObjects(fooSchema)); - const o = objectAsync(entriesFromObjects(asyncFooSchema)) + const o = objectAsync(entriesFromObjects(asyncFooSchema)); expectTypeOf(o).toEqualTypeOf(); - const o2 = object(entriesFromObjects(asyncFooSchema)) + const o2 = object(entriesFromObjects(asyncFooSchema)); expectTypeOf(o2).toEqualTypeOf(); - }) + }); }); }); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts index 327aeb93d..ba51519f0 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts @@ -9,31 +9,29 @@ describe('entriesFromObjects', () => { const overrideSchema = object({ foo: optional(number()) }); test('for empty schema', () => { - expect(entriesFromObjects()).toStrictEqual({}) - expect(entriesFromObjects(object({}))).toStrictEqual({}) - }) + expect(entriesFromObjects()).toStrictEqual({}); + expect(entriesFromObjects(object({}))).toStrictEqual({}); + }); test('for single schema', () => { - expect(entriesFromObjects(fooSchema)).toStrictEqual(fooSchema.entries) + expect(entriesFromObjects(fooSchema)).toStrictEqual(fooSchema.entries); }); test('for multi schema', () => { expect(entriesFromObjects(fooSchema, barSchema)).toStrictEqual({ ...fooSchema.entries, - ...barSchema.entries - }) + ...barSchema.entries, + }); }); test('for override schema', () => { - expect(entriesFromObjects( - fooSchema, - barSchema, - overrideSchema - )).toStrictEqual({ + expect( + entriesFromObjects(fooSchema, barSchema, overrideSchema) + ).toStrictEqual({ ...fooSchema.entries, ...barSchema.entries, - ...overrideSchema.entries - }) + ...overrideSchema.entries, + }); }); }); }); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.ts index 6c077362c..76e23424d 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -1,9 +1,9 @@ import type { - ObjectSchema , LooseObjectIssue, LooseObjectSchema, LooseObjectSchemaAsync, ObjectIssue, + ObjectSchema, ObjectSchemaAsync, ObjectWithRestIssue, ObjectWithRestSchema, @@ -13,11 +13,11 @@ import type { StrictObjectSchemaAsync, } from '../../schemas/index.ts'; import type { - ObjectEntries , BaseIssue, BaseSchema, BaseSchemaAsync, ErrorMessage, + ObjectEntries, ObjectEntriesAsync, } from '../../types/index.ts'; @@ -53,19 +53,19 @@ type Schema = >; // Type Utils -type MergeObject = Omit & B +type MergeObject = Omit & B; type Flatten = { [K in keyof T]: T[K] }; type MergedEntries = Flatten< TSchemas extends [infer TFirstSchema, ...infer TRestSchemas] - ? TFirstSchema extends Schema - ? TRestSchemas extends Schema[] - ? MergeObject> - : TFirstSchema['entries'] - : object - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - : {} + ? TFirstSchema extends Schema + ? TRestSchemas extends Schema[] + ? MergeObject> + : TFirstSchema['entries'] + : object + : // eslint-disable-next-line @typescript-eslint/no-empty-object-type + {} >; /** @@ -76,9 +76,7 @@ type MergedEntries = Flatten< * @returns The object entries. */ // @__NO_SIDE_EFFECTS__ -export function entriesFromObjects< - const TSchemas extends Schema[] ->( +export function entriesFromObjects( ...schemas: TSchemas ): MergedEntries { const entries = {} as MergedEntries; From 21a0a7e73f742a9aa1be1d84f3d8c496c89c1d20 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 19 Jan 2025 21:07:22 +0800 Subject: [PATCH 07/10] feat: limit at least one parameter passed to `entriesFromObjects` --- .../entriesFromObjects.test-d.ts | 3 --- .../entriesFromObjects.test.ts | 1 - .../entriesFromObjects/entriesFromObjects.ts | 22 ++++++++++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts index 3a261cd64..184aab35d 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts @@ -16,9 +16,6 @@ describe('entriesFromObjects', () => { const overrideSchema = object({ foo: optional(number()) }); test('for empty schema', () => { - const r1 = {}; - expectTypeOf(entriesFromObjects()).toEqualTypeOf(); - const r2 = object(entriesFromObjects(object({}))); expectTypeOf(object({})).toEqualTypeOf(); }); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts index ba51519f0..372b2b791 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts @@ -9,7 +9,6 @@ describe('entriesFromObjects', () => { const overrideSchema = object({ foo: optional(number()) }); test('for empty schema', () => { - expect(entriesFromObjects()).toStrictEqual({}); expect(entriesFromObjects(object({}))).toStrictEqual({}); }); diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.ts index 76e23424d..65679268b 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -57,29 +57,35 @@ type MergeObject = Omit & B; type Flatten = { [K in keyof T]: T[K] }; +/* eslint-disable @typescript-eslint/no-empty-object-type */ type MergedEntries = Flatten< TSchemas extends [infer TFirstSchema, ...infer TRestSchemas] ? TFirstSchema extends Schema ? TRestSchemas extends Schema[] ? MergeObject> : TFirstSchema['entries'] - : object - : // eslint-disable-next-line @typescript-eslint/no-empty-object-type - {} + : {} + : {} >; +/* eslint-enable @typescript-eslint/no-empty-object-type */ /** * Creates a new object entries definition from existing object schemas. * - * @param schemas The schemas to merge. + * @param schema The first schema to extract the entries from. + * @param schemas The rest schemas to merge the entries from. * - * @returns The object entries. + * @returns The object entries from the schemas. */ // @__NO_SIDE_EFFECTS__ -export function entriesFromObjects( +export function entriesFromObjects< + const TSchema extends Schema, + const TSchemas extends Schema[], +>( + schema: TSchema, ...schemas: TSchemas -): MergedEntries { - const entries = {} as MergedEntries; +): MergedEntries<[TSchema, ...TSchemas]> { + const entries = schema.entries as MergedEntries<[TSchema, ...TSchemas]>; for (const schema of schemas) { Object.assign(entries, schema.entries); } From 2fbd038062e4f01327ee1e5daf58aa58cca305e2 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 19 Jan 2025 21:29:37 +0800 Subject: [PATCH 08/10] fix: prevent assign to input schema entries --- library/src/utils/entriesFromObjects/entriesFromObjects.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.ts index 65679268b..3f9395a98 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -85,7 +85,8 @@ export function entriesFromObjects< schema: TSchema, ...schemas: TSchemas ): MergedEntries<[TSchema, ...TSchemas]> { - const entries = schema.entries as MergedEntries<[TSchema, ...TSchemas]>; + const entries = {} as MergedEntries<[TSchema, ...TSchemas]>; + Object.assign(entries, schema.entries); for (const schema of schemas) { Object.assign(entries, schema.entries); } From d9d096757df8827080c96ab3d075970f7490a101 Mon Sep 17 00:00:00 2001 From: SerKo Date: Sun, 2 Feb 2025 03:57:47 +0800 Subject: [PATCH 09/10] fix: flatten `MergedEntries` after merged entries --- .../entriesFromObjects/entriesFromObjects.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/library/src/utils/entriesFromObjects/entriesFromObjects.ts b/library/src/utils/entriesFromObjects/entriesFromObjects.ts index 3f9395a98..05b4fb168 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -55,20 +55,24 @@ type Schema = // Type Utils type MergeObject = Omit & B; -type Flatten = { [K in keyof T]: T[K] }; - /* eslint-disable @typescript-eslint/no-empty-object-type */ -type MergedEntries = Flatten< - TSchemas extends [infer TFirstSchema, ...infer TRestSchemas] - ? TFirstSchema extends Schema - ? TRestSchemas extends Schema[] - ? MergeObject> - : TFirstSchema['entries'] - : {} +type _MergedEntries = TSchemas extends [ + infer TFirstSchema, + ...infer TRestSchemas, +] + ? TFirstSchema extends Schema + ? TRestSchemas extends Schema[] + ? MergeObject> + : TFirstSchema['entries'] : {} ->; + : {}; /* eslint-enable @typescript-eslint/no-empty-object-type */ +type Flatten = { [K in keyof T]: T[K] } & {}; +type MergedEntries = Flatten< + _MergedEntries +>; + /** * Creates a new object entries definition from existing object schemas. * From 508eb97696c8d35229add0f601f9de71acb35e36 Mon Sep 17 00:00:00 2001 From: Fabian Hiller Date: Sun, 16 Feb 2025 17:38:23 -0500 Subject: [PATCH 10/10] Change entriesFromObjects util, improve tests and add docs --- library/CHANGELOG.md | 1 + .../entriesFromObjects.test-d.ts | 78 ++++++++++--------- .../entriesFromObjects.test.ts | 50 +++++++----- .../entriesFromObjects/entriesFromObjects.ts | 73 ++++++++++------- .../api/(utils)/entriesFromObjects/index.mdx | 54 +++++++++++++ .../(utils)/entriesFromObjects/properties.ts | 42 ++++++++++ website/src/routes/api/menu.md | 1 + 7 files changed, 216 insertions(+), 83 deletions(-) create mode 100644 website/src/routes/api/(utils)/entriesFromObjects/index.mdx create mode 100644 website/src/routes/api/(utils)/entriesFromObjects/properties.ts 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 index 184aab35d..0dcfa5db5 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts @@ -1,61 +1,63 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { describe, expectTypeOf, test } from 'vitest'; import { + boolean, + type BooleanSchema, number, + type NumberSchema, object, objectAsync, - optional, string, + type StringSchema, } from '../../schemas/index.ts'; import { entriesFromObjects } from './entriesFromObjects.ts'; describe('entriesFromObjects', () => { describe('should return objects entries', () => { - const fooSchema = object({ foo: string() }); - const barSchema = object({ bar: string() }); - const overrideSchema = object({ foo: optional(number()) }); - - test('for empty schema', () => { - const r2 = object(entriesFromObjects(object({}))); - expectTypeOf(object({})).toEqualTypeOf(); + 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', () => { - const o = object(entriesFromObjects(fooSchema)); - expectTypeOf(o).toEqualTypeOf(); + expectTypeOf(entriesFromObjects([schema1])).toEqualTypeOf<{ + readonly foo: StringSchema; + readonly bar: StringSchema; + }>(); }); - test('for multi schema', () => { - const foobar = object({ - ...fooSchema.entries, - ...barSchema.entries, - }); - - const o = object(entriesFromObjects(fooSchema, barSchema)); - expectTypeOf(o).toEqualTypeOf(); + test('for multiple schemes', () => { + expectTypeOf(entriesFromObjects([schema1, schema2])).toEqualTypeOf<{ + readonly foo: StringSchema; + readonly bar: StringSchema; + readonly baz: NumberSchema; + readonly qux: NumberSchema; + }>(); }); - test('for override schema', () => { - const foobarOverride = object({ - ...fooSchema.entries, - ...barSchema.entries, - ...overrideSchema.entries, - }); - - const o = object( - entriesFromObjects(fooSchema, barSchema, overrideSchema) - ); - expectTypeOf(o).toEqualTypeOf(); + test('with overwrites', () => { + expectTypeOf( + entriesFromObjects([schema1, schema2, schema3]) + ).toEqualTypeOf<{ + readonly bar: StringSchema; + readonly qux: NumberSchema; + readonly foo: BooleanSchema; + readonly baz: BooleanSchema; + }>(); }); - test('for async object schema', () => { - const asyncFooSchema = objectAsync(entriesFromObjects(fooSchema)); - - const o = objectAsync(entriesFromObjects(asyncFooSchema)); - expectTypeOf(o).toEqualTypeOf(); - - const o2 = object(entriesFromObjects(asyncFooSchema)); - expectTypeOf(o2).toEqualTypeOf(); + 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 index 372b2b791..e7e670e58 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.test.ts @@ -1,36 +1,50 @@ import { describe, expect, test } from 'vitest'; -import { number, object, optional, string } from '../../schemas/index.ts'; +import { + boolean, + number, + object, + objectAsync, + string, +} from '../../schemas/index.ts'; import { entriesFromObjects } from './entriesFromObjects.ts'; describe('entriesFromObjects', () => { describe('should return objects entries', () => { - const fooSchema = object({ foo: string() }); - const barSchema = object({ bar: string() }); - const overrideSchema = object({ foo: optional(number()) }); + const schema1 = object({ foo: string(), bar: string() }); + const schema2 = objectAsync({ baz: number(), qux: number() }); + const schema3 = object({ foo: boolean(), baz: boolean() }); - test('for empty schema', () => { - expect(entriesFromObjects(object({}))).toStrictEqual({}); + test('for missing schema', () => { + expect( + // @ts-expect-error + entriesFromObjects([]) + ).toStrictEqual({}); }); test('for single schema', () => { - expect(entriesFromObjects(fooSchema)).toStrictEqual(fooSchema.entries); + expect(entriesFromObjects([schema1])).toStrictEqual(schema1.entries); }); - test('for multi schema', () => { - expect(entriesFromObjects(fooSchema, barSchema)).toStrictEqual({ - ...fooSchema.entries, - ...barSchema.entries, + test('for multiple schemes', () => { + expect(entriesFromObjects([schema1, schema2])).toStrictEqual({ + ...schema1.entries, + ...schema2.entries, }); }); - test('for override schema', () => { - expect( - entriesFromObjects(fooSchema, barSchema, overrideSchema) - ).toStrictEqual({ - ...fooSchema.entries, - ...barSchema.entries, - ...overrideSchema.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 index 05b4fb168..5f930dc69 100644 --- a/library/src/utils/entriesFromObjects/entriesFromObjects.ts +++ b/library/src/utils/entriesFromObjects/entriesFromObjects.ts @@ -19,6 +19,7 @@ import type { ErrorMessage, ObjectEntries, ObjectEntriesAsync, + Prettify, } from '../../types/index.ts'; /** @@ -52,47 +53,65 @@ type Schema = ErrorMessage | undefined >; -// Type Utils -type MergeObject = Omit & B; +/** + * 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 +>; -/* eslint-disable @typescript-eslint/no-empty-object-type */ -type _MergedEntries = TSchemas extends [ - infer TFirstSchema, - ...infer TRestSchemas, +/** + * Recursive merge type. + */ +type RecursiveMerge = TSchemas extends [ + infer TFirstSchema extends Schema, ] - ? TFirstSchema extends Schema - ? TRestSchemas extends Schema[] - ? MergeObject> - : TFirstSchema['entries'] - : {} - : {}; -/* eslint-enable @typescript-eslint/no-empty-object-type */ + ? TFirstSchema['entries'] + : TSchemas extends [ + infer TFirstSchema extends Schema, + ...infer TRestSchemas extends [Schema, ...Schema[]], + ] + ? MergeEntries> + : never; -type Flatten = { [K in keyof T]: T[K] } & {}; -type MergedEntries = Flatten< - _MergedEntries +/** + * Merged entries types. + */ +type MergedEntries = Prettify< + RecursiveMerge >; /** * Creates a new object entries definition from existing object schemas. * - * @param schema The first schema to extract the entries from. - * @param schemas The rest schemas to merge the entries from. + * @param schemas The schemas to merge the entries from. * * @returns The object entries from the schemas. + * + * @beta */ -// @__NO_SIDE_EFFECTS__ export function entriesFromObjects< - const TSchema extends Schema, - const TSchemas extends Schema[], ->( - schema: TSchema, - ...schemas: TSchemas -): MergedEntries<[TSchema, ...TSchemas]> { - const entries = {} as MergedEntries<[TSchema, ...TSchemas]>; - Object.assign(entries, schema.entries); + 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/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/)