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 });
+ });
+});