Skip to content

Commit

Permalink
Merge pull request #1023 from serkodev/feat/entries-from-objects
Browse files Browse the repository at this point in the history
feat: create `entriesFromObjects` utils
  • Loading branch information
fabian-hiller authored Feb 16, 2025
2 parents 82ced4d + 508eb97 commit eb020ae
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 0 deletions.
1 change: 1 addition & 0 deletions library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
63 changes: 63 additions & 0 deletions library/src/utils/entriesFromObjects/entriesFromObjects.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<never>();
});

test('for single schema', () => {
expectTypeOf(entriesFromObjects([schema1])).toEqualTypeOf<{
readonly foo: StringSchema<undefined>;
readonly bar: StringSchema<undefined>;
}>();
});

test('for multiple schemes', () => {
expectTypeOf(entriesFromObjects([schema1, schema2])).toEqualTypeOf<{
readonly foo: StringSchema<undefined>;
readonly bar: StringSchema<undefined>;
readonly baz: NumberSchema<undefined>;
readonly qux: NumberSchema<undefined>;
}>();
});

test('with overwrites', () => {
expectTypeOf(
entriesFromObjects([schema1, schema2, schema3])
).toEqualTypeOf<{
readonly bar: StringSchema<undefined>;
readonly qux: NumberSchema<undefined>;
readonly foo: BooleanSchema<undefined>;
readonly baz: BooleanSchema<undefined>;
}>();
});

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<{}>();
});
});
});
50 changes: 50 additions & 0 deletions library/src/utils/entriesFromObjects/entriesFromObjects.test.ts
Original file line number Diff line number Diff line change
@@ -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(
{}
);
});
});
});
117 changes: 117 additions & 0 deletions library/src/utils/entriesFromObjects/entriesFromObjects.ts
Original file line number Diff line number Diff line change
@@ -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<ObjectEntries, ErrorMessage<LooseObjectIssue> | undefined>
| LooseObjectSchemaAsync<
ObjectEntriesAsync,
ErrorMessage<LooseObjectIssue> | undefined
>
| ObjectSchema<ObjectEntries, ErrorMessage<ObjectIssue> | undefined>
| ObjectSchemaAsync<ObjectEntriesAsync, ErrorMessage<ObjectIssue> | undefined>
| ObjectWithRestSchema<
ObjectEntries,
BaseSchema<unknown, unknown, BaseIssue<unknown>>,
ErrorMessage<ObjectWithRestIssue> | undefined
>
| ObjectWithRestSchemaAsync<
ObjectEntriesAsync,
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>,
ErrorMessage<ObjectWithRestIssue> | undefined
>
| StrictObjectSchema<
ObjectEntries,
ErrorMessage<StrictObjectIssue> | undefined
>
| StrictObjectSchemaAsync<
ObjectEntriesAsync,
ErrorMessage<StrictObjectIssue> | 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 [Schema, ...Schema[]]> = TSchemas extends [
infer TFirstSchema extends Schema,
]
? TFirstSchema['entries']
: TSchemas extends [
infer TFirstSchema extends Schema,
...infer TRestSchemas extends [Schema, ...Schema[]],
]
? MergeEntries<TFirstSchema['entries'], RecursiveMerge<TRestSchemas>>
: never;

/**
* Merged entries types.
*/
type MergedEntries<TSchemas extends [Schema, ...Schema[]]> = Prettify<
RecursiveMerge<TSchemas>
>;

/**
* 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<TSchemas>;

// @__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;
}
1 change: 1 addition & 0 deletions library/src/utils/entriesFromObjects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './entriesFromObjects.ts';
1 change: 1 addition & 0 deletions library/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
54 changes: 54 additions & 0 deletions website/src/routes/api/(utils)/entriesFromObjects/index.mdx
Original file line number Diff line number Diff line change
@@ -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<TSchemas>(schemas);
```

## Generics

- `TSchemas` <Property {...properties.TSchemas} />

## Parameters

- `schemas` <Property {...properties.schemas} />

## Returns

- `entries` <Property {...properties.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

<ApiList items={['looseObject', 'object', 'objectWithRest', 'strictObject']} />
42 changes: 42 additions & 0 deletions website/src/routes/api/(utils)/entriesFromObjects/properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { PropertyProps } from '~/components';

export const properties: Record<string, PropertyProps> = {
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',
},
],
},
},
};
1 change: 1 addition & 0 deletions website/src/routes/api/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
## Utils

- [entriesFromList](/api/entriesFromList/)
- [entriesFromObjects](/api/entriesFromObjects/)
- [getDotPath](/api/getDotPath/)
- [isOfKind](/api/isOfKind/)
- [isOfType](/api/isOfType/)
Expand Down

0 comments on commit eb020ae

Please sign in to comment.