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(defaults): add defaults in compat layer #641

Merged
merged 7 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions benchmarks/performance/defaults.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { bench, describe } from 'vitest';
import { defaults as defaultsToolkitCompat_ } from 'es-toolkit/compat';
import { defaults as defaultsLodash_ } from 'lodash';

const defaultsToolkitCompat = defaultsToolkitCompat_;
const defaultsLodash = defaultsLodash_;

describe('defaults', () => {
bench('es-toolkit/compat/defaults', () => {
defaultsToolkitCompat({ a: 1 }, { a: 2, b: 2 });
defaultsToolkitCompat({ a: 1, b: 2 }, { b: 3 }, { c: 3 });
});

bench('lodash/defaults', () => {
defaultsLodash({ a: 1 }, { a: 2, b: 2 });
defaultsLodash({ a: 1, b: 2 }, { b: 3 }, { c: 3 });
});
});
1 change: 1 addition & 0 deletions src/compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export { fromPairs } from './object/fromPairs.ts';
export { unset } from './object/unset.ts';
export { cloneDeep } from './object/cloneDeep.ts';
export { invertBy } from './object/invertBy.ts';
export { defaults } from './object/defaults.ts';

export { isPlainObject } from './predicate/isPlainObject.ts';
export { isArray } from './predicate/isArray.ts';
Expand Down
66 changes: 66 additions & 0 deletions src/compat/object/defaults.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { defaults } from './defaults';
import { objectProto } from '../_internal/objectProto';
import * as esToolkit from '../index';

describe('defaults', () => {
it('should assign source properties if missing on `object`', () => {
const actual = defaults({ a: 1 }, { a: 2, b: 2 });
expect(actual).toEqual({ a: 1, b: 2 });
});

it('should accept multiple sources', () => {
const expected = { a: 1, b: 2, c: 3 };
let actual = defaults({ a: 1, b: 2 }, { b: 3 }, { c: 3 });

expect(actual).toEqual(expected);

actual = defaults({ a: 1, b: 2 }, { b: 3, c: 3 }, { c: 2 });
expect(actual).toEqual(expected);
});

it('should not overwrite `null` values', () => {
const actual = defaults({ a: null }, { a: 1 });
expect((actual as any).a).toBe(null);
});

it('should overwrite `undefined` values', () => {
const actual = defaults({ a: undefined }, { a: 1 });
expect((actual as any).a).toBe(1);
});

it('should assign `undefined` values', () => {
const source = { a: undefined, b: 1 };
const actual = defaults({}, source);

expect(actual).toEqual({ a: undefined, b: 1 });
});

it('should assign properties that shadow those on `Object.prototype`', () => {
const object = {
constructor: objectProto.constructor,
hasOwnProperty: objectProto.hasOwnProperty,
isPrototypeOf: objectProto.isPrototypeOf,
propertyIsEnumerable: objectProto.propertyIsEnumerable,
toLocaleString: objectProto.toLocaleString,
toString: objectProto.toString,
valueOf: objectProto.valueOf,
};

const source = {
constructor: 1,
hasOwnProperty: 2,
isPrototypeOf: 3,
propertyIsEnumerable: 4,
toLocaleString: 5,
toString: 6,
valueOf: 7,
};

let expected = esToolkit.clone(source);
expect(defaults({}, source)).toEqual(expected);

expected = esToolkit.clone(object);
expect(defaults({}, object, source)).toEqual(expected);
});
});
172 changes: 172 additions & 0 deletions src/compat/object/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { eq } from '../util/eq.ts';

/**
* Assigns own and inherited enumerable string keyed properties of source objects to the destination object for all destination properties that resolve to `undefined`.
*
* Source objects are applied from left to right. Once a property is set, additional values of the same property are ignored.
* It ensures that the destination object is not `null` or `undefined`.
*
* Note: This method mutates a first argument `object`.
*
* @template T - The destination object type.
* @param {T} object - The destination object.
* @returns {NonNullable<T>} Returns `object`, ensuring that the destination object is not `null` or `undefined`.
*/
export function defaults<T extends object>(object: T): NonNullable<T>;

/**
* Assigns own and inherited enumerable string keyed properties of source objects to the destination object for all destination properties that resolve to `undefined`.
*
* Source objects are applied from left to right. Once a property is set, additional values of the same property are ignored.
* It ensures that the destination object is not `null` or `undefined`.
*
* Note: This method mutates a first argument `object`.
*
* @template T - The destination object type.
* @template S - The source object type.
* @param {T} object - The destination object.
* @param {S} source - The source object.
* @returns {NonNullable<T & S>} Returns the merged object, ensuring that the destination object is not `null` or `undefined`.
*/
export function defaults<T extends object, S extends object>(object: T, source: S): NonNullable<T & S>;

/**
* Assigns own and inherited enumerable string keyed properties of source objects to the destination object for all destination properties that resolve to `undefined`.
*
* Source objects are applied from left to right. Once a property is set, additional values of the same property are ignored.
* It ensures that the destination object is not `null` or `undefined`.
*
* Note: This method mutates a first argument `object`.
*
* @template T - The destination object type.
* @template S1 - The first source object type.
* @template S2 - The second source object type.
* @param {T} object - The destination object.
* @param {S1} source1 - The first source object.
* @param {S2} source2 - The second source object.
* @returns {NonNullable<T & S1 & S2>} Returns the merged object, ensuring that the destination object is not `null` or `undefined`.
*/
export function defaults<T extends object, S1 extends object, S2 extends object>(
object: T,
source1: S1,
source2: S2
): NonNullable<T & S1 & S2>;

/**
* Assigns own and inherited enumerable string keyed properties of source objects to the destination object for all destination properties that resolve to `undefined`.
*
* Source objects are applied from left to right. Once a property is set, additional values of the same property are ignored.
* It ensures that the destination object is not `null` or `undefined`.
*
* Note: This method mutates a first argument `object`.
*
* @template T - The destination object type.
* @template S1 - The first source object type.
* @template S2 - The second source object type.
* @template S3 - The third source object type.
* @param {T} object - The destination object.
* @param {S1} source1 - The first source object.
* @param {S2} source2 - The second source object.
* @param {S3} source3 - The third source object.
* @returns {NonNullable<T & S1 & S2 & S3>} Returns the merged object, ensuring that the destination object is not `null` or `undefined`.
*/
export function defaults<T extends object, S1 extends object, S2 extends object, S3 extends object>(
object: T,
source1: S1,
source2: S2,
source3: S3
): NonNullable<T & S1 & S2 & S3>;

/**
* Assigns own and inherited enumerable string keyed properties of source objects to the destination object for all destination properties that resolve to `undefined`.
*
* Source objects are applied from left to right. Once a property is set, additional values of the same property are ignored.
* It ensures that the destination object is not `null` or `undefined`.
*
* Note: This method mutates a first argument `object`.
*
* @template T - The destination object type.
* @template S1 - The first source object type.
* @template S2 - The second source object type.
* @template S3 - The third source object type.
* @template S4 - The fourth source object type.
* @param {T} object - The destination object.
* @param {S1} source1 - The first source object.
* @param {S2} source2 - The second source object.
* @param {S3} source3 - The third source object.
* @param {S4} source4 - The fourth source object.
* @returns {NonNullable<T & S1 & S2 & S3 & S4>} Returns the merged object, ensuring that the destination object is not `null` or `undefined`.
*/
export function defaults<T extends object, S1 extends object, S2 extends object, S3 extends object, S4 extends object>(
object: T,
source1: S1,
source2: S2,
source3: S3,
source4: S4
): NonNullable<T & S1 & S2 & S3 & S4>;

/**
* Assigns own and inherited enumerable string keyed properties of source objects to the destination object for all destination properties that resolve to `undefined`.
*
* Source objects are applied from left to right. Once a property is set, additional values of the same property are ignored.
* It ensures that the destination object is not `null` or `undefined`.
*
* Note: This method mutates a first argument `object`.
*
* @template T - The destination object type.
* @template S - The source object type.
* @param {T} object - The destination object.
* @param {S[]} sources - The source objects.
* @returns {object} Returns the merged object, ensuring that the destination object is not `null` or `undefined`.
*
* @example
* defaults({ a: 1 }, { a: 2, b: 2 }, { c: 3 }); // { a: 1, b: 2, c: 3 }
* defaults({ a: 1, b: 2 }, { b: 3 }, { c: 3 }); // { a: 1, b: 2, c: 3 }
* defaults({ a: null }, { a: 1 }); // { a: null }
* defaults({ a: undefined }, { a: 1 }); // { a: 1 }
*/
export function defaults<T extends object, S extends object>(object: T, ...sources: S[]): object;

raon0211 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Assigns own and inherited enumerable string keyed properties of source objects to the destination object for all destination properties that resolve to `undefined`.
*
* Source objects are applied from left to right. Once a property is set, additional values of the same property are ignored.
* It ensures that the destination object is not `null` or `undefined`.
*
* Note: This method mutates a first argument `object`.
*
* @template T - The destination object type.
* @template S - The source object type.
* @param {T} object - The destination object.
* @param {S[]} sources - The source objects.
* @returns {object} Returns the merged object, ensuring that the destination object is not `null` or `undefined`.
*
* @example
* defaults({ a: 1 }, { a: 2, b: 2 }, { c: 3 }); // { a: 1, b: 2, c: 3 }
* defaults({ a: 1, b: 2 }, { b: 3 }, { c: 3 }); // { a: 1, b: 2, c: 3 }
* defaults({ a: null }, { a: 1 }); // { a: null }
* defaults({ a: undefined }, { a: 1 }); // { a: 1 }
*/
export function defaults<T extends object, S extends object>(object: T, ...sources: S[]): object {
object = Object(object);
const objectProto = Object.prototype;

for (let i = 0; i < sources.length; i++) {
const source = sources[i];
const keys = Object.keys(source) as Array<keyof S>;

for (let j = 0; j < keys.length; j++) {
const key = keys[j];
const value = (object as any)[key];

if (
value === undefined ||
(!Object.hasOwn(object, key) && eq(value, objectProto[key as keyof typeof objectProto]))
) {
(object as any)[key] = source[key];
}
}
}

return object;
}