Skip to content

Commit d95b132

Browse files
committed
fix(utils): Ensure dropUndefinedKeys() does not break class instances
1 parent 76378af commit d95b132

File tree

3 files changed

+38
-2
lines changed

3 files changed

+38
-2
lines changed

packages/utils/src/is.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export function isPrimitive(wat: unknown): wat is Primitive {
106106
}
107107

108108
/**
109-
* Checks whether given value's type is an object literal
109+
* Checks whether given value's type is an object literal, or a class instance.
110110
* {@link isPlainObject}.
111111
*
112112
* @param wat A value to be checked.

packages/utils/src/object.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export function dropUndefinedKeys<T>(inputValue: T): T {
222222
}
223223

224224
function _dropUndefinedKeys<T>(inputValue: T, memoizationMap: Map<unknown, unknown>): T {
225-
if (isPlainObject(inputValue)) {
225+
if (isPojo(inputValue)) {
226226
// If this node has already been visited due to a circular reference, return the object it was mapped to in the new object
227227
const memoVal = memoizationMap.get(inputValue);
228228
if (memoVal !== undefined) {
@@ -263,6 +263,19 @@ function _dropUndefinedKeys<T>(inputValue: T, memoizationMap: Map<unknown, unkno
263263
return inputValue;
264264
}
265265

266+
function isPojo(input: unknown): input is Record<string, unknown> {
267+
if (!isPlainObject(input)) {
268+
return false;
269+
}
270+
271+
try {
272+
const name = (Object.getPrototypeOf(input) as { constructor: { name: string } }).constructor.name;
273+
return !name || name === 'Object';
274+
} catch {
275+
return true;
276+
}
277+
}
278+
266279
/**
267280
* Ensure that something is an object.
268281
*

packages/utils/test/object.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,29 @@ describe('dropUndefinedKeys()', () => {
210210
});
211211
});
212212

213+
describe('class instances', () => {
214+
class MyClass {
215+
public a = 'foo';
216+
public b = undefined;
217+
}
218+
219+
test('ignores class instance', () => {
220+
const instance = new MyClass();
221+
const result = dropUndefinedKeys(instance);
222+
expect(result).toEqual({ a: 'foo', b: undefined });
223+
expect(result).toBeInstanceOf(MyClass);
224+
expect(Object.prototype.hasOwnProperty.call(result, 'b')).toBe(true);
225+
});
226+
227+
test('ignores nested instances', () => {
228+
const instance = new MyClass();
229+
const result = dropUndefinedKeys({ a: [instance] });
230+
expect(result).toEqual({ a: [instance] });
231+
expect(result.a[0]).toBeInstanceOf(MyClass);
232+
expect(Object.prototype.hasOwnProperty.call(result.a[0], 'b')).toBe(true);
233+
});
234+
});
235+
213236
test('should not throw on objects with circular reference', () => {
214237
const chicken: any = {
215238
food: undefined,

0 commit comments

Comments
 (0)