Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create entriesFromObjects utils #1023

Merged
merged 11 commits into from
Feb 16, 2025
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