diff --git a/src/utils/__tests__/defaults.spec-d.ts b/src/utils/__tests__/defaults.spec-d.ts new file mode 100644 index 00000000..4ad191b4 --- /dev/null +++ b/src/utils/__tests__/defaults.spec-d.ts @@ -0,0 +1,20 @@ +/** + * @file Type Tests - defaults + * @module tutils/utils/tests/unit-d/defaults + */ + +import type Vehicle from '#fixtures/types/vehicle' +import type { Defaults, Partial } from '#src/types' +import type testSubject from '../defaults' + +describe('unit-d:utils/defaults', () => { + it('should return Defaults', () => { + // Arrange + type T = Partial + type U = [{ readonly vrm: number }, { readonly vin: string }] + type Expect = Defaults + + // Expect + expectTypeOf>().returns.toEqualTypeOf() + }) +}) diff --git a/src/utils/__tests__/defaults.spec.ts b/src/utils/__tests__/defaults.spec.ts new file mode 100644 index 00000000..047db033 --- /dev/null +++ b/src/utils/__tests__/defaults.spec.ts @@ -0,0 +1,27 @@ +/** + * @file Unit Tests - defaults + * @module tutils/utils/tests/unit/defaults + */ + +import type Vehicle from '#fixtures/types/vehicle' +import type { Partial } from '#src/types' +import testSubject from '../defaults' + +describe('unit:utils/defaults', () => { + let base: Partial & { vrm: string } + + beforeAll(() => { + base = { vrm: faker.vehicle.vrm() } + }) + + it('should return merge result', () => { + // Arrange + const s1: { vrm: number } = { vrm: faker.number.int() } + const s2: { vin: string } = { vin: faker.vehicle.vin() } + + // Act + Expect + expect(testSubject(base, s1, s2)) + .to.eql({ ...base, ...s2 }) + .but.not.equal(base) + }) +}) diff --git a/src/utils/assign-with.ts b/src/utils/assign-with.ts index d1b6cc58..40bbb9a7 100644 --- a/src/utils/assign-with.ts +++ b/src/utils/assign-with.ts @@ -21,8 +21,10 @@ import properties from './properties' type AssignCustomizer = Fn<[unknown, unknown, string | symbol], unknown> /** - * Assigns own properties of one or more `source` objects to a cloned `base` - * object. A `customizer` is used to produce assigned values. + * Assigns own properties of one or more `source` objects to a target object. + * + * A `customizer` is used to produce assigned values. The initial `base` object + * **will not** be modified. * * Source objects are applied from left to right. Subsequent sources overwrite * property assignments of previous sources. diff --git a/src/utils/assign.ts b/src/utils/assign.ts index ba46539e..8b25d2ad 100644 --- a/src/utils/assign.ts +++ b/src/utils/assign.ts @@ -7,8 +7,8 @@ import type { Assign, ObjectCurly, Objectify } from '#src/types' import assignWith from './assign-with' /** - * Assigns own properties of one or more `source` objects to a cloned `base` - * object. + * Assigns own properties of one or more `source` objects to a target object. + * The initial `base` object **will not** be modified. * * Source objects are applied from left to right. Subsequent sources overwrite * property assignments of previous sources. diff --git a/src/utils/defaults.ts b/src/utils/defaults.ts new file mode 100644 index 00000000..6e5dea9d --- /dev/null +++ b/src/utils/defaults.ts @@ -0,0 +1,40 @@ +/** + * @file Utilities - defaults + * @module tutils/utils/defaults + */ + +import type { Defaults, ObjectCurly, Objectify } from '#src/types' +import assignWith from './assign-with' +import cast from './cast' +import isUndefined from './is-undefined' + +/** + * Assigns own properties of one or more `source` objects to a target object for + * all target properties that resolve to `undefined`. + * + * The initial `base` object **will not** be modified. + * + * Source objects are applied from left to right. Subsequent default values are + * ignored if a property no longer resolves to `undefined`. + * + * @see {@linkcode Defaults} + * + * @todo examples + * + * @template T - Base object + * @template U - Source object array + * + * @param {T} base - Base object + * @param {U} source - Source object array + * @return {Defaults} Merge result + */ +const defaults = , U extends readonly ObjectCurly[]>( + base: T, + ...source: U +): Defaults => { + return cast( + assignWith((curr, src) => (isUndefined(curr) ? src : curr), base, ...source) + ) +} + +export default defaults diff --git a/src/utils/index.ts b/src/utils/index.ts index 4b79130d..c1f8dcb0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -17,6 +17,7 @@ export { default as constant } from './constant' export { default as construct } from './construct' export { default as count } from './count' export { default as crush } from './crush' +export { default as defaults } from './defaults' export { default as define } from './define' export { default as descriptor } from './descriptor' export { default as desegment } from './desegment' diff --git a/src/utils/merge-with.ts b/src/utils/merge-with.ts index c7fddd20..c7727f39 100644 --- a/src/utils/merge-with.ts +++ b/src/utils/merge-with.ts @@ -23,7 +23,7 @@ type MergeCustomizer = Fn<[unknown, unknown, string | symbol], unknown> /** * Recursively merges own properties of one or more `source` objects into a - * cloned `base` object. + * target object. The initial `base` object **will not** be modified. * * A `customizer` is be used to produce merged values. Plain object properties * are merged recursively. Other objects and value types are overridden by diff --git a/src/utils/merge.ts b/src/utils/merge.ts index 251825d4..97710b9c 100644 --- a/src/utils/merge.ts +++ b/src/utils/merge.ts @@ -8,7 +8,7 @@ import mergeWith from './merge-with' /** * Recursively merges own properties of one or more `source` objects into a - * cloned `base` object. + * target object. The initial `base` object **will not** be modified. * * Plain object properties are merged recursively. Other objects and value types * are overridden by assignment.