diff --git a/src/index.ts b/src/index.ts index 166d66d7..33d25a3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,4 @@ export { default as set, merge, mergeWith } from './utils/set'; export { default as warning, noteOnce } from './warning'; export { default as omit } from './omit'; export { default as toArray } from './Children/toArray'; +export { default as mergeProps } from './mergeProps'; diff --git a/src/mergeProps.ts b/src/mergeProps.ts new file mode 100644 index 00000000..95973e2d --- /dev/null +++ b/src/mergeProps.ts @@ -0,0 +1,22 @@ +/** + * Merges multiple props objects into one. Unlike `Object.assign()` or `{ ...a, ...b }`, it skips + * properties whose value is explicitly set to `undefined`. + */ +function mergeProps(a: A, b: B): B & A; +function mergeProps(a: A, b: B, c: C): C & B & A; +function mergeProps(a: A, b: B, c: C, d: D): D & C & B & A; +function mergeProps(...items: any[]) { + const ret: any = {}; + for (const item of items) { + if (item) { + for (const key of Object.keys(item)) { + if (item[key] !== undefined) { + ret[key] = item[key]; + } + } + } + } + return ret; +} + +export default mergeProps; diff --git a/tests/mergeProps.test.ts b/tests/mergeProps.test.ts new file mode 100644 index 00000000..53a30e62 --- /dev/null +++ b/tests/mergeProps.test.ts @@ -0,0 +1,52 @@ +import mergeProps from '../src/mergeProps'; + +describe('mergeProps', () => { + it('merges two objects with later overriding earlier', () => { + const a = { foo: 1, bar: 2 }; + const b = { bar: 3, baz: 4 }; + expect(mergeProps(a, b)).toEqual({ foo: 1, bar: 3, baz: 4 }); + }); + + it('excludes keys with undefined values', () => { + const a = { foo: 1, bar: undefined }; + const b = { bar: 2 }; + expect(mergeProps(a, b)).toEqual({ foo: 1, bar: 2 }); + }); + + it('does not include key if value is undefined in last object', () => { + const a = { foo: 1 }; + const b = { bar: undefined }; + expect(mergeProps(a, b)).toEqual({ foo: 1 }); + }); + + it('skips null and undefined items', () => { + const a = { foo: 1 }; + expect(mergeProps(a, null as any)).toEqual({ foo: 1 }); + expect(mergeProps(a, undefined as any)).toEqual({ foo: 1 }); + expect(mergeProps(null as any, a)).toEqual({ foo: 1 }); + expect(mergeProps(undefined as any, a)).toEqual({ foo: 1 }); + }); + + it('merges three or more objects with rightmost winning', () => { + const a = { a: 1 }; + const b = { a: 2, b: 2 }; + const c = { a: 3, b: 3, c: 3 }; + expect(mergeProps(a, b, c)).toEqual({ a: 3, b: 3, c: 3 }); + }); + + it('returns empty object for no args', () => { + expect((mergeProps as (...items: any[]) => any)()).toEqual({}); + }); + + it('returns copy of single object', () => { + const a = { foo: 1, bar: 2 }; + expect((mergeProps as (...items: any[]) => any)(a)).toEqual({ + foo: 1, + bar: 2, + }); + }); + + it('handles empty objects', () => { + expect(mergeProps({}, { a: 1 }, {})).toEqual({ a: 1 }); + }); +});